diff --git a/Cargo.toml b/Cargo.toml index bf2c5e8..29b5d36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ cfg-if = "1" reqwest = { version = "0.11", features = ["json"], optional = true } serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["rt", "sync", "time"], optional = true } +urlencoding = { version = "2", optional = true } chrono = { version = "0.4", default-features = false, optional = true, features = ["serde"] } serde_json = { version = "1", optional = true } @@ -33,7 +34,7 @@ rustc-args = ["--cfg", "docsrs"] [features] default = ["api"] -api = ["chrono", "reqwest", "serde_json"] +api = ["chrono", "reqwest", "serde_json", "urlencoding"] autoposter = ["api", "tokio"] webhook = [] diff --git a/src/autoposter.rs b/src/autoposter.rs index 40fdcd7..8612221 100644 --- a/src/autoposter.rs +++ b/src/autoposter.rs @@ -1,5 +1,5 @@ use crate::{client::InnerClient, Stats}; -use core::{mem::MaybeUninit, ops::Deref, time::Duration}; +use core::{ops::Deref, time::Duration}; use std::sync::Arc; use tokio::{ sync::Mutex, @@ -18,7 +18,7 @@ pub struct AutoposterHandle { } impl AutoposterHandle { - /// Feeds new bot stats to this autoposter handle. The [autoposter itself][Autoposter] will automatically post it to the [Top.gg](https://top.gg) servers once appropiate. + /// Feeds new bot stats to this autoposter handle. The [autoposter itself][Autoposter] will automatically post it to [Top.gg](https://top.gg) servers once appropiate. /// /// # Examples /// @@ -36,7 +36,7 @@ impl AutoposterHandle { /// /// // ... then in some on ready/new guild event ... /// let server_count = 12345; - /// let stats = Stats::count_based(server_count, None); + /// let stats = Stats::from(server_count); /// autoposter.feed(stats).await; /// ``` /// @@ -54,7 +54,7 @@ impl AutoposterHandle { /// /// let server_count = 12345; /// autoposter - /// .feed(Stats::count_based(server_count, None)) + /// .feed(Stats::from(server_count)) /// .await; /// /// // this handle can be cloned and tossed around threads! @@ -62,14 +62,14 @@ impl AutoposterHandle { /// /// // do the same thing... /// new_handle - /// .feed(Stats::count_based(server_count, None)) + /// .feed(Stats::from(server_count)) /// .await; /// /// let another_handle = new_handle.clone(); /// /// // do the same thing... /// another_handle - /// .feed(Stats::count_based(server_count, None)) + /// .feed(Stats::from(server_count)) /// .await; /// ``` pub async fn feed(&self, new_stats: Stats) { @@ -90,7 +90,9 @@ impl Clone for AutoposterHandle { } } -/// A struct that lets you automate the process of posting bot statistics to the [Top.gg API](https://docs.top.gg) in intervals. +/// A struct that lets you automate the process of posting bot statistics to [Top.gg](https://top.gg) in intervals. +/// +/// **NOTE:** This struct owns the thread handle that executes the automatic posting. The autoposter thread will stop once it's dropped. #[must_use] pub struct Autoposter { thread: JoinHandle<()>, @@ -100,11 +102,10 @@ pub struct Autoposter { impl Autoposter { #[allow(invalid_value, clippy::uninit_assumed_init)] pub(crate) fn new(client: Arc, interval: Duration) -> Self { - // SAFETY: post_stats will be called ONLY when the ready flag is set to true. let handle = AutoposterHandle { data: Arc::new(Mutex::new(PendingData { ready: false, - stats: unsafe { MaybeUninit::uninit().assume_init() }, + stats: Stats::count_based(0, None), })), }; diff --git a/src/bot.rs b/src/bot.rs index c100afa..393ab2d 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -112,19 +112,6 @@ where impl Bot { /// Retrieves the creation date of this bot. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let bot = client.get_bot(264811613708746752).await.unwrap(); - /// - /// println!("{}", bot.created_at()); - /// ``` #[must_use] #[inline(always)] pub fn created_at(&self) -> DateTime { @@ -133,20 +120,7 @@ impl Bot { /// Retrieves the avatar URL of this bot. /// - /// It's format will be either PNG or GIF if animated. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let bot = client.get_bot(264811613708746752).await.unwrap(); - /// - /// println!("{}", bot.avatar()); - /// ``` + /// It's format will either be PNG or GIF if animated. #[must_use] #[inline(always)] pub fn avatar(&self) -> String { @@ -154,19 +128,6 @@ impl Bot { } /// The invite URL of this Discord bot. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let bot = client.get_bot(264811613708746752).await.unwrap(); - /// - /// println!("{}", bot.invite()); - /// ``` #[must_use] pub fn invite(&self) -> String { match &self.invite { @@ -179,19 +140,6 @@ impl Bot { } /// The amount of shards this Discord bot has according to posted stats. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let bot = client.get_bot(264811613708746752).await.unwrap(); - /// - /// println!("{}", bot.shard_count()); - /// ``` #[must_use] #[inline(always)] pub fn shard_count(&self) -> usize { @@ -199,19 +147,6 @@ impl Bot { } /// Retrieves the URL of this Discord bot's [Top.gg](https://top.gg) page. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let bot = client.get_bot(264811613708746752).await.unwrap(); - /// - /// println!("{}", bot.url()); - /// ``` #[must_use] #[inline(always)] pub fn url(&self) -> String { @@ -258,6 +193,35 @@ pub(crate) struct Bots { } /// A struct representing a Discord bot's statistics. +/// +/// # Examples +/// +/// Solely from a server count: +/// +/// ```rust,no_run +/// use topgg::Stats; +/// +/// let _stats = Stats::from(12345); +/// ``` +/// +/// Server count with a shard count: +/// +/// ```rust,no_run +/// use topgg::Stats; +/// +/// let server_count = 12345; +/// let shard_count = 10; +/// let _stats = Stats::count_based(server_count, Some(shard_count)); +/// ``` +/// +/// Solely from shards information: +/// +/// ```rust,no_run +/// use topgg::Stats; +/// +/// // the shard posting this data has 456 servers. +/// let _stats = Stats::shards_based([123, 456, 789], Some(1)); +/// ``` #[must_use] #[derive(Clone, Serialize, Deserialize)] pub struct Stats { @@ -271,16 +235,6 @@ pub struct Stats { impl Stats { /// Creates a [`Stats`] struct based on total server and optionally, shard count data. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Stats; - /// - /// let _stats = Stats::count_based(12345, Some(10)); - /// ``` pub const fn count_based(server_count: usize, shard_count: Option) -> Self { Self { server_count: Some(server_count), @@ -332,19 +286,6 @@ impl Stats { } /// An array of this Discord bot's server count for each shard. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let stats = client.get_stats().await.unwrap(); - /// - /// println!("{:?}", stats.shards()); - /// ``` #[must_use] #[inline(always)] pub fn shards(&self) -> &[usize] { @@ -355,19 +296,6 @@ impl Stats { } /// The amount of shards this Discord bot has. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let stats = client.get_stats().await.unwrap(); - /// - /// println!("{:?}", stats.shard_count()); - /// ``` #[must_use] #[inline(always)] pub fn shard_count(&self) -> usize { @@ -378,19 +306,6 @@ impl Stats { } /// The amount of servers this bot is in. `None` if such information is publicly unavailable. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let stats = client.get_stats().await.unwrap(); - /// - /// println!("{:?}", stats.server_count()); - /// ``` #[must_use] pub fn server_count(&self) -> Option { self @@ -405,6 +320,14 @@ impl Stats { } } +/// Creates a [`Stats`] struct solely from a server count. +impl From for Stats { + #[inline(always)] + fn from(server_count: usize) -> Self { + Self::count_based(server_count, None) + } +} + impl Debug for Stats { fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { fmt @@ -422,6 +345,20 @@ pub(crate) struct IsWeekend { } /// A struct for configuring the query in [`get_bots`][crate::Client::get_bots]. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```rust,no_run +/// use topgg::Query; +/// +/// let _query = Query::new() +/// .limit(250) +/// .skip(50) +/// .username("shiro") +/// .certified(true); +/// ``` #[must_use] #[derive(Clone)] pub struct Query { @@ -431,16 +368,6 @@ pub struct Query { impl Query { /// Initiates a new empty querying struct. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Query; - /// - /// let _query = Query::new(); - /// ``` #[inline(always)] pub fn new() -> Self { Self { @@ -449,17 +376,7 @@ impl Query { } } - /// Sets the maximum amount of bots to be queried - it can't exceed 500. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Query; - /// - /// let _query = Query::new().limit(250); - /// ``` + /// Sets the maximum amount of bots to be queried. pub fn limit(mut self, new_limit: u16) -> Self { self .query @@ -467,17 +384,7 @@ impl Query { self } - /// Sets the amount of bots to be skipped during the query - it can't exceed 499. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Query; - /// - /// let _query = Query::new().skip(100); - /// ``` + /// Sets the amount of bots to be skipped during the query. pub fn skip(mut self, skip_by: u16) -> Self { self .query @@ -486,16 +393,6 @@ impl Query { } /// Queries only Discord bots that matches this username. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Query; - /// - /// let _query = Query::new().username("shiro"); - /// ``` pub fn username(mut self, new_username: &str) -> Self { self.search.push_str(&format!( "username%3A%20{}%20", @@ -505,16 +402,6 @@ impl Query { } /// Queries only Discord bots that matches this discriminator. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Query; - /// - /// let _query = Query::new().discriminator("1536"); - /// ``` pub fn discriminator(mut self, new_discriminator: &str) -> Self { self .search @@ -523,16 +410,6 @@ impl Query { } /// Queries only Discord bots that matches this prefix. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Query; - /// - /// let _query = Query::new().prefix("!"); - /// ``` pub fn prefix(mut self, new_prefix: &str) -> Self { self.search.push_str(&format!( "prefix%3A%20{}%20", @@ -542,32 +419,12 @@ impl Query { } /// Queries only Discord bots that has this vote count. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Query; - /// - /// let _query = Query::new().votes(1000); - /// ``` pub fn votes(mut self, new_votes: usize) -> Self { self.search.push_str(&format!("points%3A%20{new_votes}%20")); self } /// Queries only Discord bots that has this monthly vote count. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Query; - /// - /// let _query = Query::new().monthly_votes(100); - /// ``` pub fn monthly_votes(mut self, new_monthly_votes: usize) -> Self { self .search @@ -576,16 +433,6 @@ impl Query { } /// Queries only [Top.gg](https://top.gg) certified Discord bots or not. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Query; - /// - /// let _query = Query::new().certified(true); - /// ``` pub fn certified(mut self, is_certified: bool) -> Self { self .search @@ -594,16 +441,6 @@ impl Query { } /// Queries only Discord bots that has this [Top.gg](https://top.gg) vanity URL. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Query; - /// - /// let _query = Query::new().vanity("mee6"); - /// ``` pub fn vanity(mut self, new_vanity: &str) -> Self { self.search.push_str(&format!( "vanity%3A%20{}%20", diff --git a/src/client.rs b/src/client.rs index 3697560..44be711 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,8 +3,7 @@ use crate::{ user::{User, Voted, Voter}, Error, Query, Result, Snowflake, }; -use core::{mem::transmute, str}; -use reqwest::{RequestBuilder, Response, StatusCode, Version}; +use reqwest::{IntoUrl, Method, Response, StatusCode, Version}; use serde::{de::DeserializeOwned, Deserialize}; cfg_if::cfg_if! { @@ -26,11 +25,11 @@ pub(crate) struct Ratelimit { } macro_rules! api { - ($e:expr) => { + ($e:literal) => { concat!("https://top.gg/api", $e) }; - ($e:expr,$($rest:tt)*) => { + ($e:literal, $($rest:tt)*) => { format!(api!($e), $($rest)*) }; } @@ -40,18 +39,19 @@ pub(crate) struct InnerClient { token: String, } -// this is implemented here because autoposter needs to access this function from a different thread. +// this is implemented here because autoposter needs to access this struct from a different thread. impl InnerClient { - async fn send_inner(&self, request: RequestBuilder, body: Vec) -> Result { + async fn send_inner(&self, method: Method, url: impl IntoUrl, body: Vec) -> Result { 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 - .execute(unsafe { - builder + .execute( + self + .http + .request(method, url) .header("Authorization", &auth) .header("Connection", "close") .header("Content-Length", body.len()) @@ -63,8 +63,8 @@ impl InnerClient { .version(Version::HTTP_11) .body(body) .build() - .unwrap_unchecked() - }) + .unwrap(), + ) .await { Ok(response) => { @@ -76,8 +76,8 @@ impl InnerClient { Err(match status { 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 { + StatusCode::TOO_MANY_REQUESTS => match response.json::().await { + Ok(ratelimit) => Error::Ratelimit { retry_after: ratelimit.retry_after, }, _ => Error::InternalServerError, @@ -92,23 +92,28 @@ impl InnerClient { } #[inline(always)] - pub(crate) async fn send(&self, request: RequestBuilder, body: Option>) -> Result + pub(crate) async fn send( + &self, + method: Method, + url: impl IntoUrl, + body: Option>, + ) -> Result where T: DeserializeOwned, { - self - .send_inner(request, body.unwrap_or_default()) - .await - .json() - .map_err(|_| Error::InternalServerError) + match self.send_inner(method, url, body.unwrap_or_default()).await { + Ok(out) => out.json().await.map_err(|_| Error::InternalServerError), + Err(err) => Err(err), + } } 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(self.http.post(api!("/bots/stats")), body) + .send_inner( + Method::POST, + api!("/bots/stats"), + serde_json::to_vec(new_stats).unwrap(), + ) .await .map(|_| ()) } @@ -124,16 +129,6 @@ impl Client { /// Creates a brand new client instance from a [Top.gg](https://top.gg) token. /// /// You can get a [Top.gg](https://top.gg) token if you own a listed Discord bot on [Top.gg](https://top.gg) (open the edit page, see in `Webhooks` section) - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let _client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// ``` #[inline(always)] pub fn new(token: String) -> Self { let inner = InnerClient { @@ -158,36 +153,17 @@ impl Client { /// # 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 the [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError]) + /// - 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]) - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let user = client.get_user(661200758510977084).await.unwrap(); - /// - /// assert_eq!(user.username, "null"); - /// assert_eq!(user.id, 661200758510977084); - /// - /// println!("{:?}", user); - /// ``` pub async fn get_user(&self, id: I) -> Result where I: Snowflake, { self .inner - .send( - self.inner.http.get(api!("/users/{}", id.as_snowflake())), - None, - ) + .send(Method::GET, api!("/users/{}", id.as_snowflake()), None) .await } @@ -202,37 +178,17 @@ impl Client { /// # 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 the [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError]) + /// - 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]) - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let bot = client.get_bot(264811613708746752).await.unwrap(); - /// - /// assert_eq!(bot.username, "Luca"); - /// assert_eq!(bot.discriminator, "1375"); - /// assert_eq!(bot.id, 264811613708746752); - /// - /// println!("{:?}", bot); - /// ``` pub async fn get_bot(&self, id: I) -> Result where I: Snowflake, { self .inner - .send( - self.inner.http.get(api!("/bots/{}", id.as_snowflake())), - None, - ) + .send(Method::GET, api!("/bots/{}", id.as_snowflake()), None) .await } @@ -245,26 +201,13 @@ impl Client { /// # 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 the [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError]) + /// - 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]) - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let stats = client.get_stats().await.unwrap(); - /// - /// println!("{:?}", stats); - /// ``` pub async fn get_stats(&self) -> Result { self .inner - .send(self.inner.http.get(api!("/bots/stats")), None) + .send(Method::GET, api!("/bots/stats"), None) .await } @@ -277,31 +220,15 @@ impl Client { /// # 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 the [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError]) + /// - 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]) - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// - /// let server_count = 12345; - /// client - /// .post_stats(Stats::count_based(server_count, None)) - /// .await - /// .unwrap(); - /// ``` #[inline(always)] pub async fn post_stats(&self, new_stats: Stats) -> Result<()> { self.inner.post_stats(&new_stats).await } - /// Creates a new autoposter instance for this client which lets you automate the process of posting your Discord bot's statistics to the [Top.gg API](https://docs.top.gg) in intervals. + /// Creates a new autoposter instance for this client which lets you automate the process of posting your Discord bot's statistics to [Top.gg](https://top.gg) in intervals. /// /// # Panics /// @@ -325,7 +252,7 @@ impl Client { /// /// // ... then in some on ready/new guild event ... /// let server_count = 12345; - /// let stats = Stats::count_based(server_count, None); + /// let stats = Stats::from(server_count); /// autoposter.feed(stats).await; /// ``` #[cfg(feature = "autoposter")] @@ -352,27 +279,13 @@ impl Client { /// # 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 the [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError]) + /// - 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]) - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// - /// for voter in client.get_voters().await.unwrap() { - /// println!("{:?}", voter); - /// } - /// ``` pub async fn get_voters(&self) -> Result> { self .inner - .send(self.inner.http.get(api!("/bots/votes")), None) + .send(Method::GET, api!("/bots/votes"), None) .await } @@ -387,7 +300,7 @@ impl Client { /// # 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 the [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError]) + /// - 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]) /// @@ -422,10 +335,8 @@ impl Client { self .inner .send::( - self - .inner - .http - .get(api!("/bots{}", query.into().query_string())), + Method::GET, + api!("/bots{}", query.into().query_string()), None, ) .await @@ -443,40 +354,23 @@ impl Client { /// # 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 the [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError]) + /// - 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]) - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// - /// if client.has_voted(661200758510977084).await.unwrap() { - /// println!("checks out"); - /// } - /// ``` #[allow(clippy::transmute_int_to_bool)] pub async fn has_voted(&self, user_id: I) -> Result where I: Snowflake, { - // SAFETY: res.voted will always be either 0 or 1. self .inner .send::( - self - .inner - .http - .get(api!("/bots/votes?userId={}", user_id.as_snowflake())), + Method::GET, + api!("/bots/votes?userId={}", user_id.as_snowflake()), None, ) .await - .map(|res| unsafe { transmute(res.voted) }) + .map(|res| res.voted != 0) } /// Checks if the weekend multiplier is active. @@ -488,27 +382,13 @@ impl Client { /// # 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 the [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError]) + /// - 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]) - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// - /// if client.is_weekend().await.unwrap() { - /// println!("guess what? it's the weekend! woohoo! 🎉🎉🎉🎉"); - /// } - /// ``` pub async fn is_weekend(&self) -> Result { self .inner - .send::(self.inner.http.get(api!("/weekend")), None) + .send::(Method::GET, api!("/weekend"), None) .await .map(|res| res.is_weekend) } diff --git a/src/error.rs b/src/error.rs index cd2df7f..53ec251 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,7 +4,7 @@ use std::error; /// A struct representing an error coming from this SDK - unexpected or not. #[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). + /// An unexpected internal error coming from the client itself, preventing it from sending a request to [Top.gg](https://top.gg). InternalClientError(reqwest::Error), /// An unexpected error coming from [Top.gg](https://top.gg)'s servers themselves. diff --git a/src/user.rs b/src/user.rs index 54fb728..0ee384e 100644 --- a/src/user.rs +++ b/src/user.rs @@ -6,23 +6,23 @@ use serde::{Deserialize, Deserializer}; /// A struct representing a user's social links. #[derive(Clone, Debug, Deserialize)] pub struct Socials { - /// A URL to this user's GitHub account. + /// A URL of this user's GitHub account. #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub github: Option, - /// A URL to this user's Instagram account. + /// A URL of this user's Instagram account. #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub instagram: Option, - /// A URL to this user's Reddit account. + /// A URL of this user's Reddit account. #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub reddit: Option, - /// A URL to this user's Twitter account. + /// A URL of this user's Twitter account. #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub twitter: Option, - /// A URL to this user's YouTube channel. + /// A URL of this user's YouTube channel. #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub youtube: Option, } @@ -49,7 +49,7 @@ pub struct User { #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub bio: Option, - /// A URL to this user's profile banner image. + /// A URL of this user's profile banner image. #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub banner: Option, @@ -91,19 +91,6 @@ where impl User { /// Retrieves the creation date of this user. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let user = client.get_user(661200758510977084).await.unwrap(); - /// - /// println!("{}", user.created_at()); - /// ``` #[must_use] #[inline(always)] pub fn created_at(&self) -> DateTime { @@ -112,20 +99,7 @@ impl User { /// Retrieves the Discord avatar URL of this user. /// - /// It's format will be either PNG or GIF if animated. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// let user = client.get_user(661200758510977084).await.unwrap(); - /// - /// println!("{}", user.avatar()); - /// ``` + /// It's format will either be PNG or GIF if animated. #[must_use] #[inline(always)] pub fn avatar(&self) -> String { @@ -174,20 +148,6 @@ pub struct Voter { impl Voter { /// Retrieves the creation date of this user. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// - /// for voter in client.get_voters().await.unwrap() { - /// println!("{}", voter.created_at()); - /// } - /// ``` #[must_use] #[inline(always)] pub fn created_at(&self) -> DateTime { @@ -196,21 +156,7 @@ impl Voter { /// Retrieves the Discord avatar URL of this user. /// - /// It's format will be either PNG or GIF if animated. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Client; - /// - /// let client = Client::new(env!("TOPGG_TOKEN").to_string()); - /// - /// for voter in client.get_voters().await.unwrap() { - /// println!("{}", voter.avatar()); - /// } - /// ``` + /// It's format will either be PNG or GIF if animated. #[must_use] #[inline(always)] pub fn avatar(&self) -> String { diff --git a/src/util.rs b/src/util.rs index 8b74a7e..6f81428 100644 --- a/src/util.rs +++ b/src/util.rs @@ -34,13 +34,10 @@ where #[inline(always)] pub(crate) fn get_creation_date(id: u64) -> DateTime { - // SAFETY: Discord IDs are guaranteed to be valid UNIX timestamps. - unsafe { - Utc - .timestamp_millis_opt(((id >> 22) + DISCORD_EPOCH) as _) - .single() - .unwrap_unchecked() - } + Utc + .timestamp_millis_opt(((id >> 22) + DISCORD_EPOCH) as _) + .single() + .unwrap() } pub(crate) fn get_avatar(hash: &Option, id: u64) -> String { diff --git a/src/webhook/axum.rs b/src/webhook/axum.rs index 4daa9a1..00af8d5 100644 --- a/src/webhook/axum.rs +++ b/src/webhook/axum.rs @@ -1,43 +1,3 @@ -//! # Examples -//! -//! Basic usage: -//! -//! ```rust,no_run -//! use axum::{routing::get, Router, Server}; -//! use std::{net::SocketAddr, sync::Arc}; -//! use topgg::{Vote, VoteHandler}; -//! -//! struct MyVoteHandler {} -//! -//! #[axum::async_trait] -//! impl VoteHandler for MyVoteHandler { -//! async fn voted(&self, vote: Vote) { -//! println!("{:?}", vote); -//! } -//! } -//! -//! async fn index() -> &'static str { -//! "Hello, World!" -//! } -//! -//! #[tokio::main] -//! async fn main() { -//! let state = Arc::new(MyVoteHandler {}); -//! -//! let app = Router::new().route("/", get(index)).nest( -//! "/webhook", -//! topgg::axum::webhook(env!("TOPGG_WEBHOOK_PASSWORD").to_string(), Arc::clone(&state)), -//! ); -//! -//! let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap(); -//! -//! Server::bind(&addr) -//! .serve(app.into_make_service()) -//! .await -//! .unwrap(); -//! } -//! ``` - use crate::VoteHandler; use axum::{ extract::State, diff --git a/src/webhook/vote.rs b/src/webhook/vote.rs index 33c31ca..90de91a 100644 --- a/src/webhook/vote.rs +++ b/src/webhook/vote.rs @@ -102,76 +102,21 @@ cfg_if::cfg_if! { /// /// # Examples /// - /// Basic usage with [`actix-web`](https://actix.rs/): + /// Basic usage: /// /// ```rust,no_run - /// use actix_web::{ - /// error::{Error, ErrorUnauthorized}, - /// get, post, App, HttpServer, - /// }; - /// use std::io; - /// use topgg::IncomingVote; - /// - /// #[get("/")] - /// async fn index() -> &'static str { - /// "Hello, World!" - /// } - /// - /// #[post("/webhook")] - /// async fn webhook(vote: IncomingVote) -> Result<&'static str, Error> { - /// match vote.authenticate(env!("TOPGG_WEBHOOK_PASSWORD")) { - /// Some(vote) => { - /// println!("{:?}", vote); - /// - /// Ok("ok") - /// } - /// _ => Err(ErrorUnauthorized("401")), - /// } - /// } - /// - /// #[actix_web::main] - /// async fn main() -> io::Result<()> { - /// HttpServer::new(|| App::new().service(index).service(webhook)) - /// .bind("127.0.0.1:8080")? - /// .run() - /// .await - /// } - /// ``` - /// - /// Basic usage with [`rocket`](https://rocket.rs): - /// - /// ```rust,no_run - /// #![feature(decl_macro)] - /// - /// use rocket::{get, http::Status, post, routes}; - /// use topgg::IncomingVote; - /// - /// #[get("/")] - /// fn index() -> &'static str { - /// "Hello, World!" - /// } - /// - /// #[post("/webhook", data = "")] - /// fn webhook(vote: IncomingVote) -> Status { - /// match vote.authenticate(env!("TOPGG_WEBHOOK_PASSWORD")) { - /// Some(vote) => { - /// println!("{:?}", vote); + /// match incoming_vote.authenticate(env!("TOPGG_WEBHOOK_PASSWORD")) { + /// Some(vote) => { + /// println!("{:?}", vote); /// - /// Status::Ok - /// }, - /// _ => { - /// println!("found an unauthorized attacker."); + /// // respond with 200 OK... + /// }, + /// _ => { + /// println!("found an unauthorized attacker."); /// - /// Status::Unauthorized - /// } + /// // respond with 401 UNAUTHORIZED... /// } /// } - /// - /// fn main() { - /// rocket::ignite() - /// .mount("/", routes![index, webhook]) - /// .launch(); - /// } /// ``` #[must_use] #[inline(always)] @@ -201,81 +146,6 @@ cfg_if::cfg_if! { #[async_trait::async_trait] pub trait VoteHandler: Send + Sync + 'static { /// Your vote handler's on-vote async callback. The endpoint will always return a 200 (OK) HTTP status code after running this method. - /// - /// # Examples - /// - /// Basic usage with [`axum`](https://crates.io/crates/axum): - /// - /// ```rust,no_run - /// use axum::{routing::get, Router, Server}; - /// use std::{net::SocketAddr, sync::Arc}; - /// use topgg::{Vote, VoteHandler}; - /// - /// struct MyVoteHandler {} - /// - /// #[axum::async_trait] - /// impl VoteHandler for MyVoteHandler { - /// async fn voted(&self, vote: Vote) { - /// println!("{:?}", vote); - /// } - /// } - /// - /// async fn index() -> &'static str { - /// "Hello, World!" - /// } - /// - /// #[tokio::main] - /// async fn main() { - /// let state = Arc::new(MyVoteHandler {}); - /// - /// let app = Router::new().route("/", get(index)).nest( - /// "/webhook", - /// topgg::axum::webhook(env!("TOPGG_WEBHOOK_PASSWORD").to_string(), Arc::clone(&state)), - /// ); - /// - /// let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap(); - /// - /// Server::bind(&addr) - /// .serve(app.into_make_service()) - /// .await - /// .unwrap(); - /// } - /// ``` - /// - /// Basic usage with [`warp`](https://crates.io/crates/warp): - /// - /// ```rust,no_run - /// use std::{net::SocketAddr, sync::Arc}; - /// use topgg::{Vote, VoteHandler}; - /// use warp::Filter; - /// - /// struct MyVoteHandler {} - /// - /// #[async_trait::async_trait] - /// impl VoteHandler for MyVoteHandler { - /// async fn voted(&self, vote: Vote) { - /// println!("{:?}", vote); - /// } - /// } - /// - /// #[tokio::main] - /// async fn main() { - /// let state = Arc::new(MyVoteHandler {}); - /// - /// // POST /webhook - /// let webhook = topgg::warp::webhook( - /// "webhook", - /// env!("TOPGG_WEBHOOK_PASSWORD").to_string(), - /// Arc::clone(&state), - /// ); - /// - /// let routes = warp::get().map(|| "Hello, World!").or(webhook); - /// - /// let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap(); - /// - /// warp::serve(routes).run(addr).await - /// } - /// ``` async fn voted(&self, vote: Vote); } } diff --git a/src/webhook/warp.rs b/src/webhook/warp.rs index 61acd9e..13e1238 100644 --- a/src/webhook/warp.rs +++ b/src/webhook/warp.rs @@ -1,40 +1,3 @@ -//! # Examples -//! -//! Basic usage: -//! -//! ```rust,no_run -//! use std::{net::SocketAddr, sync::Arc}; -//! use topgg::{Vote, VoteHandler}; -//! use warp::Filter; -//! -//! struct MyVoteHandler {} -//! -//! #[async_trait::async_trait] -//! impl VoteHandler for MyVoteHandler { -//! async fn voted(&self, vote: Vote) { -//! println!("{:?}", vote); -//! } -//! } -//! -//! #[tokio::main] -//! async fn main() { -//! let state = Arc::new(MyVoteHandler {}); -//! -//! // POST /webhook -//! let webhook = topgg::warp::webhook( -//! "webhook", -//! env!("TOPGG_WEBHOOK_PASSWORD").to_string(), -//! Arc::clone(&state), -//! ); -//! -//! let routes = warp::get().map(|| "Hello, World!").or(webhook); -//! -//! let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap(); -//! -//! warp::serve(routes).run(addr).await -//! } -//! ``` - use crate::{Vote, VoteHandler}; use std::sync::Arc; use warp::{body, header, http::StatusCode, path, Filter, Rejection, Reply};