From d2bf33cd521b8bae528f55dfcd3b3b9c0ecd84b4 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 27 Apr 2023 14:16:06 +0700 Subject: [PATCH 01/13] [fix,refactor]: ACTUALLY fix #6, bug-squashed a lot of things, and optimized the codebase --- Cargo.toml | 2 +- README.md | 4 +- src/bot.rs | 225 ++++++++++++++++++++++++++++++++++++++------ src/client.rs | 4 +- src/http.rs | 14 ++- src/user.rs | 31 +++++- src/util.rs | 27 ++++++ src/webhook/vote.rs | 27 +++--- 8 files changed, 278 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b8cb29b..d9f3bea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "topgg" -version = "1.0.4" +version = "1.1.0" edition = "2021" authors = ["null (https://github.com/null8626)", "Top.gg (https://top.gg)"] description = "The official Rust wrapper for the Top.gg API" diff --git a/README.md b/README.md index 2603830..8a33a87 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,8 @@ async fn main() { .certified(true); let query = Query::new() - .limit(250) - .skip(50) + .limit(250u16) + .skip(50u16) .filter(filter); for bot in client.get_bots(query).await.unwrap() { diff --git a/src/bot.rs b/src/bot.rs index c9c8075..b6e39b4 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,13 +1,13 @@ use crate::{snowflake, util}; use chrono::{offset::Utc, DateTime}; -use core::cmp::min; -use serde::{ - de::{self, Deserializer}, - Deserialize, Serialize, +use core::{ + cmp::min, + fmt::{self, Debug, Formatter}, }; +use serde::{de::Deserializer, Deserialize, Serialize}; /// A struct representing a Discord Bot listed on [Top.gg](https://top.gg). -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Deserialize)] pub struct Bot { /// The ID of this Discord bot. #[serde(deserialize_with = "snowflake::deserialize")] @@ -27,16 +27,23 @@ pub struct Bot { pub short_description: String, /// The long description of this Discord bot. It can contain HTML and/or Markdown. - #[serde(rename = "longdesc")] + #[serde( + default, + deserialize_with = "util::deserialize_optional_string", + rename = "longdesc" + )] pub long_description: Option, /// The tags of this Discord bot. + #[serde(default, deserialize_with = "util::deserialize_default")] pub tags: Vec, /// The website URL of this Discord bot. + #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub website: Option, /// The link to this Discord bot's GitHub repository. + #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub github: Option, /// A list of IDs of this Discord bot's owners. The main owner is the first ID in the array. @@ -44,14 +51,15 @@ pub struct Bot { pub owners: Vec, /// A list of IDs of the guilds featured on this Discord bot's page. - #[serde(deserialize_with = "snowflake::deserialize_vec")] + #[serde(default, deserialize_with = "snowflake::deserialize_vec")] pub guilds: Vec, - /// The custom bot invite URL of this Discord bot. - pub invite: Option, - /// The URL for this Discord bot's banner image. - #[serde(rename = "bannerUrl")] + #[serde( + default, + deserialize_with = "util::deserialize_optional_string", + rename = "bannerUrl" + )] pub banner_url: Option, /// The date when this Discord bot was approved on [Top.gg](https://top.gg). @@ -62,10 +70,8 @@ pub struct Bot { pub is_certified: bool, /// A list of this Discord bot's shards. - pub shards: Option>, - - /// The amount of shards this Discord bot has according to posted stats. - pub shard_count: Option, + #[serde(default, deserialize_with = "util::deserialize_default")] + pub shards: Vec, /// The amount of upvotes this Discord bot has. #[serde(rename = "points")] @@ -79,19 +85,29 @@ pub struct Bot { #[serde(default, deserialize_with = "deserialize_support_server")] pub support: Option, + #[serde(default, deserialize_with = "util::deserialize_optional_string")] avatar: Option, + + #[serde(default, deserialize_with = "util::deserialize_optional_string")] + invite: Option, + + shard_count: Option, + + #[serde(default, deserialize_with = "util::deserialize_optional_string")] vanity: Option, } +#[inline(always)] pub(crate) fn deserialize_support_server<'de, D>( deserializer: D, ) -> Result, D::Error> where D: Deserializer<'de>, { - let s: Option<&str> = de::Deserialize::deserialize(deserializer)?; - - Ok(s.map(|support| format!("https://discord.com/invite/{support}"))) + Ok( + unsafe { util::deserialize_optional_string(deserializer).unwrap_unchecked() } + .map(|support| format!("https://discord.com/invite/{support}")), + ) } impl Bot { @@ -122,6 +138,61 @@ impl Bot { util::get_avatar(&self.avatar, &self.discriminator, self.id) } + /// The invite URL of this Discord bot. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust,no_run + /// use topgg::Client; + /// + /// #[tokio::main] + /// async fn main() { + /// let token = env!("TOPGG_TOKEN").to_owned(); + /// let client = Client::new(token); + /// + /// let bot = client.get_bot(264811613708746752u64).await.unwrap(); + /// + /// println!("{}", bot.invite()); + /// } + /// ``` + #[must_use] + pub fn invite(&self) -> String { + match self.invite.as_ref() { + Some(inv) => inv.to_owned(), + _ => format!( + "https://discord.com/oauth2/authorize?scope=bot&client_id={}", + self.id + ), + } + } + + /// The amount of shards this Discord bot has according to posted stats. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust,no_run + /// use topgg::Client; + /// + /// #[tokio::main] + /// async fn main() { + /// let token = env!("TOPGG_TOKEN").to_owned(); + /// let client = Client::new(token); + /// + /// let bot = client.get_bot(264811613708746752u64).await.unwrap(); + /// + /// println!("{}", bot.shard_count()); + /// } + /// ``` + #[must_use] + #[inline(always)] + pub fn shard_count(&self) -> u64 { + self.shard_count.unwrap_or(self.shards.len() as _) + } + /// Retrieves the URL of this Discord bot's [Top.gg](https://top.gg) page. /// /// # Examples @@ -151,22 +222,116 @@ impl Bot { } } +impl Debug for Bot { + fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { + fmt + .debug_struct("Bot") + .field("id", &self.id) + .field("username", &self.username) + .field("discriminator", &self.discriminator) + .field("prefix", &self.prefix) + .field("short_description", &self.short_description) + .field("long_description", &self.long_description) + .field("tags", &self.tags) + .field("website", &self.website) + .field("github", &self.github) + .field("owners", &self.owners) + .field("guilds", &self.guilds) + .field("banner_url", &self.banner_url) + .field("date", &self.date) + .field("is_certified", &self.is_certified) + .field("shards", &self.shards) + .field("votes", &self.votes) + .field("monthly_votes", &self.monthly_votes) + .field("support", &self.support) + .field("avatar", &self.avatar()) + .field("invite", &self.invite()) + .field("shard_count", &self.shard_count()) + .field("url", &self.url()) + .finish() + } +} + #[derive(Deserialize)] pub(crate) struct Bots { pub(crate) results: Vec, } /// A struct representing a Discord bot's statistics returned from the API. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Deserialize)] pub struct Stats { - /// The bot's server count. - pub server_count: Option, - /// The bot's server count per shard. - pub shards: Option>, + #[serde(default, deserialize_with = "util::deserialize_default")] + pub shards: Vec, + + shard_count: Option, + server_count: Option, +} + +impl Stats { + /// The amount of shards this Discord bot has according to posted stats. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust,no_run + /// use topgg::Client; + /// + /// #[tokio::main] + /// async fn main() { + /// let token = env!("TOPGG_TOKEN").to_owned(); + /// let client = Client::new(token); + /// + /// let stats = client.get_stats().await.unwrap(); + /// + /// println!("{:?}", stats.shard_count()); + /// } + /// ``` + #[must_use] + #[inline(always)] + pub fn shard_count(&self) -> u64 { + self.shard_count.unwrap_or(self.shards.len() as _) + } - /// The bot's shard count. - pub shard_count: Option, + /// The amount of servers this bot is in. `None` if such information is publicly unavailable. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust,no_run + /// use topgg::Client; + /// + /// #[tokio::main] + /// async fn main() { + /// let token = env!("TOPGG_TOKEN").to_owned(); + /// let client = Client::new(token); + /// + /// let stats = client.get_stats().await.unwrap(); + /// + /// println!("{:?}", stats.server_count()); + /// } + /// ``` + #[must_use] + pub fn server_count(&self) -> Option { + self.server_count.or(if self.shards.is_empty() { + None + } else { + Some(self.shards.iter().copied().sum()) + }) + } +} + +impl Debug for Stats { + fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { + fmt + .debug_struct("Stats") + .field("shards", &self.shards) + .field("shard_count", &self.shard_count()) + .field("server_count", &self.server_count()) + .finish() + } } /// A struct representing a Discord bot's statistics [to be posted][crate::Client::post_stats] to the API. @@ -492,7 +657,7 @@ impl Query { /// use topgg::Query; /// /// let _query = Query::new() - /// .limit(250); + /// .limit(250u16); /// ``` #[must_use] pub fn limit(mut self, new_limit: N) -> Self @@ -515,8 +680,8 @@ impl Query { /// use topgg::Query; /// /// let _query = Query::new() - /// .limit(250) - /// .skip(100); + /// .limit(250u16) + /// .skip(100u16); /// ``` #[must_use] pub fn skip(mut self, skip_by: S) -> Self @@ -543,8 +708,8 @@ impl Query { /// .certified(true); /// /// let _query = Query::new() - /// .limit(250) - /// .skip(100) + /// .limit(250u16) + /// .skip(100u16) /// .filter(filter); /// ``` #[must_use] diff --git a/src/client.rs b/src/client.rs index d39b3f0..5fda97c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -355,8 +355,8 @@ impl Client { /// .certified(true); /// /// let query = Query::new() - /// .limit(250) - /// .skip(50) + /// .limit(250u16) + /// .skip(50u16) /// .filter(filter); /// /// for bot in client.get_bots(query).await.unwrap() { diff --git a/src/http.rs b/src/http.rs index 10c98e3..71bfa48 100644 --- a/src/http.rs +++ b/src/http.rs @@ -43,8 +43,9 @@ impl Http { let payload = format!( "\ - {predicate} /api{path} HTTP/1.0\r\n\ + {predicate} /api{path} HTTP/1.1\r\n\ Authorization: Bearer {}\r\n\ + Connection: close\r\n\ Content-Type: application/json\r\n\ Host: top.gg\r\n\ User-Agent: topgg (https://github.com/top-gg/rust-sdk) Rust/\r\n\ @@ -100,9 +101,12 @@ impl Http { where D: DeserializeOwned, { - self - .send(predicate, path, body) - .await - .and_then(|response| serde_json::from_str(&response).map_err(|_| Error::InternalServerError)) + self.send(predicate, path, body).await.and_then(|response| { + serde_json::from_str(&response).map_err(|err| { + println!("json:\n{response}\n\nerr:\n{:?}\n\n", err); + + Error::InternalServerError + }) + }) } } diff --git a/src/user.rs b/src/user.rs index 1776f4d..fbd1e1e 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,27 +1,33 @@ use crate::{snowflake, util}; +use core::fmt::{self, Debug, Formatter}; use serde::Deserialize; /// A struct representing a user's social links. #[derive(Clone, Debug, Deserialize)] pub struct Socials { /// A URL to this user's GitHub account. + #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub github: Option, /// A URL to this user's Instagram account. + #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub instagram: Option, /// A URL to this user's Reddit account. + #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub reddit: Option, /// A URL to this user's Twitter account. + #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub twitter: Option, /// A URL to this user's YouTube channel. + #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub youtube: Option, } /// A struct representing a user logged into [Top.gg](https://top.gg). -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Deserialize)] pub struct User { /// The Discord ID of this user. #[serde(deserialize_with = "snowflake::deserialize")] @@ -34,9 +40,11 @@ pub struct User { pub discriminator: String, /// The user's bio. + #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub bio: Option, /// A URL to this user's profile banner image. + #[serde(default, deserialize_with = "util::deserialize_optional_string")] pub banner: Option, /// A struct of this user's social links. @@ -63,6 +71,7 @@ pub struct User { #[serde(rename = "admin")] pub is_admin: bool, + #[serde(default, deserialize_with = "util::deserialize_optional_string")] avatar: Option, } @@ -94,6 +103,26 @@ impl User { } } +impl Debug for User { + fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { + fmt + .debug_struct("User") + .field("id", &self.id) + .field("username", &self.username) + .field("discriminator", &self.discriminator) + .field("bio", &self.bio) + .field("banner", &self.banner) + .field("socials", &self.socials) + .field("is_supporter", &self.is_supporter) + .field("is_certified_dev", &self.is_certified_dev) + .field("is_moderator", &self.is_moderator) + .field("is_web_moderator", &self.is_web_moderator) + .field("is_admin", &self.is_admin) + .field("avatar", &self.avatar()) + .finish() + } +} + #[derive(Deserialize)] pub(crate) struct Voted { pub(crate) voted: u8, diff --git a/src/util.rs b/src/util.rs index 7b3e56e..188e8ef 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,30 @@ +use serde::de::{Deserialize, Deserializer}; + +#[inline(always)] +pub(crate) fn deserialize_optional_string<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Ok(Deserialize::deserialize(deserializer).ok().map(|s: &str| { + if s.is_empty() { + None + } else { + Some(s.to_owned()) + } + })) +} + +#[inline(always)] +pub(crate) fn deserialize_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: Deserializer<'de>, +{ + Option::deserialize(deserializer).map(|res| res.unwrap_or_default()) +} + pub(crate) fn get_avatar(hash: &Option, discriminator: &str, id: u64) -> String { match hash { Some(hash) => { diff --git a/src/webhook/vote.rs b/src/webhook/vote.rs index c0e2361..2ec8fa1 100644 --- a/src/webhook/vote.rs +++ b/src/webhook/vote.rs @@ -40,9 +40,7 @@ fn deserialize_is_test<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let s: &str = de::Deserialize::deserialize(deserializer)?; - - Ok(s == "test") + de::Deserialize::deserialize(deserializer).map(|s: &str| s == "test") } fn deserialize_query_string<'de, D>( @@ -51,23 +49,22 @@ fn deserialize_query_string<'de, D>( where D: Deserializer<'de>, { - let s: Result<&str, D::Error> = de::Deserialize::deserialize(deserializer); - Ok( - s.map(|s| { - let mut output = HashMap::new(); + de::Deserialize::deserialize(deserializer) + .ok() + .map(|s: &str| { + let mut output = HashMap::new(); - for mut it in s.split('&').map(|pair| pair.split('=')) { - if let (Some(k), Some(v)) = (it.next(), it.next()) { - if let Ok(v) = urlencoding::decode(v) { - output.insert(k.to_owned(), v.into_owned()); + for mut it in s.split('&').map(|pair| pair.split('=')) { + if let (Some(k), Some(v)) = (it.next(), it.next()) { + if let Ok(v) = urlencoding::decode(v) { + output.insert(k.to_owned(), v.into_owned()); + } } } - } - output - }) - .ok(), + output + }), ) } From f5d84e19c21c3c013f86e2fb94427276ec8b0c7e Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 27 Apr 2023 14:19:20 +0700 Subject: [PATCH 02/13] fix: forgot to remove println debug --- src/http.rs | 11 ++++------- src/util.rs | 18 +++++++++++------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/http.rs b/src/http.rs index 71bfa48..57de0a4 100644 --- a/src/http.rs +++ b/src/http.rs @@ -101,12 +101,9 @@ impl Http { where D: DeserializeOwned, { - self.send(predicate, path, body).await.and_then(|response| { - serde_json::from_str(&response).map_err(|err| { - println!("json:\n{response}\n\nerr:\n{:?}\n\n", err); - - Error::InternalServerError - }) - }) + self + .send(predicate, path, body) + .await + .and_then(|response| serde_json::from_str(&response).map_err(|_| Error::InternalServerError)) } } diff --git a/src/util.rs b/src/util.rs index 188e8ef..519bd8e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -7,13 +7,17 @@ pub(crate) fn deserialize_optional_string<'de, D>( where D: Deserializer<'de>, { - Ok(Deserialize::deserialize(deserializer).ok().map(|s: &str| { - if s.is_empty() { - None - } else { - Some(s.to_owned()) - } - })) + Ok( + Deserialize::deserialize(deserializer) + .ok() + .and_then(|s: &str| { + if s.is_empty() { + None + } else { + Some(s.to_owned()) + } + }), + ) } #[inline(always)] From cfbdb974d0673f2c0707da07846d8f298b3ff497 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 27 Apr 2023 14:20:45 +0700 Subject: [PATCH 03/13] doc: update readme --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8a33a87..f2ba193 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# topgg [![crates.io][crates-io-image]][crates-io-url] [![crates.io downloads][crates-io-downloads-image]][crates-io-url] [![license][github-license-image]][github-license-url] [![BLAZINGLY FAST!!!][blazingly-fast-image]][blazingly-fast-url] +# [topgg](https://crates.io/crates/topgg) [![crates.io][crates-io-image]][crates-io-url] [![crates.io downloads][crates-io-downloads-image]][crates-io-url] [![license][github-license-image]][github-license-url] [![BLAZINGLY FAST!!!][blazingly-fast-image]][blazingly-fast-url] [crates-io-image]: https://img.shields.io/crates/v/topgg?style=flat-square [crates-io-downloads-image]: https://img.shields.io/crates/d/topgg?style=flat-square @@ -14,7 +14,7 @@ The official Rust SDK for the [Top.gg API](https://docs.top.gg). Make sure to have a [Top.gg](https://top.gg) API token handy, you can have an API token if you own a listed Discord bot on [Top.gg](https://top.gg) (open the edit page, see in `Webhooks` section) then add the following to your `Cargo.toml`'s dependencies: ```toml -topgg = "1" +topgg = "1.1" ``` ## Features @@ -156,7 +156,7 @@ In your `Cargo.toml`: ```toml [dependencies] -topgg = { version = "1", features = ["autoposter"] } +topgg = { version = "1.1", features = ["autoposter"] } ``` In your code: @@ -188,7 +188,7 @@ In your `Cargo.toml`: ```toml [dependencies] -topgg = { version = "1", default-features = false, features = ["actix"] } +topgg = { version = "1.1", default-features = false, features = ["actix"] } ``` In your code: @@ -224,7 +224,7 @@ In your `Cargo.toml`: ```toml [dependencies] -topgg = { version = "1", default-features = false, features = ["axum"] } +topgg = { version = "1.1", default-features = false, features = ["axum"] } ``` In your code: @@ -267,7 +267,7 @@ In your `Cargo.toml`: ```toml [dependencies] -topgg = { version = "1", default-features = false, features = ["rocket"] } +topgg = { version = "1.1", default-features = false, features = ["rocket"] } ``` In your code: @@ -303,7 +303,7 @@ In your `Cargo.toml`: ```toml [dependencies] -topgg = { version = "1", default-features = false, features = ["warp"] } +topgg = { version = "1.1", default-features = false, features = ["warp"] } ``` In your code: From 8389e21ac025a0971d801451b3c8a36d9962a3da Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 27 Apr 2023 21:55:05 +0700 Subject: [PATCH 04/13] feat: add Error::Forbidden --- src/client.rs | 1 + src/error.rs | 4 ++++ src/http.rs | 15 +++++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 5fda97c..3df0df9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -290,6 +290,7 @@ impl Client { /// # Errors /// /// Errors if the following conditions are met: + /// - Your bot receives more than 1000 votes monthly. Please use webhooks instead. ([`Forbidden`][crate::Error::Forbidden]) /// - 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 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]) diff --git a/src/error.rs b/src/error.rs index 3265b60..0673129 100644 --- a/src/error.rs +++ b/src/error.rs @@ -48,6 +48,9 @@ impl error::Error for InternalError { /// A struct representing an error coming from this SDK - unexpected or not. #[derive(Debug)] pub enum Error { + /// The client is not allowed to send a HTTP request to this endpoint. + Forbidden, + /// An unexpected internal error coming from the client itself, preventing it from sending a request to the [Top.gg](https://top.gg) API. InternalClientError(InternalError), @@ -67,6 +70,7 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::Forbidden => write!(f, "forbidden"), Self::InternalClientError(err) => write!(f, "internal client error: {err}"), Self::InternalServerError => write!(f, "internal server error"), Self::NotFound => write!(f, "not found"), diff --git a/src/http.rs b/src/http.rs index 57de0a4..fa6cafb 100644 --- a/src/http.rs +++ b/src/http.rs @@ -55,15 +55,17 @@ impl Http { body.unwrap_or_default() ); - if let Err(err) = socket.write_all(payload.as_bytes()).await { - return Err(Error::InternalClientError(InternalError::WriteRequest(err))); - } + socket + .write_all(payload.as_bytes()) + .await + .map_err(|err| Error::InternalClientError(InternalError::WriteRequest(err)))?; let mut response = String::new(); - if socket.read_to_string(&mut response).await.is_err() { - return Err(Error::InternalServerError); - } + socket + .read_to_string(&mut response) + .await + .map_err(|_| Error::InternalServerError)?; // we should never receive invalid raw HTTP responses - so unwrap_unchecked() is okay to use here let status_code = unsafe { @@ -77,6 +79,7 @@ impl Http { match status_code { 401 => panic!("unauthorized"), + 403 => Err(Error::Forbidden), 404 => Err(Error::NotFound), 429 => Err(Error::Ratelimit { retry_after: serde_json::from_str::(&response) From 207d4fa72f8669cca4a6445a705bb2b74c6a6e43 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 2 May 2023 13:15:30 +0700 Subject: [PATCH 05/13] refactor: fix serde imports --- src/bot.rs | 2 +- src/snowflake.rs | 2 +- src/util.rs | 2 +- src/webhook/vote.rs | 31 ++++++++++++------------------- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index b6e39b4..231b344 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -4,7 +4,7 @@ use core::{ cmp::min, fmt::{self, Debug, Formatter}, }; -use serde::{de::Deserializer, Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; /// A struct representing a Discord Bot listed on [Top.gg](https://top.gg). #[derive(Clone, Deserialize)] diff --git a/src/snowflake.rs b/src/snowflake.rs index fed5490..a3b48d8 100644 --- a/src/snowflake.rs +++ b/src/snowflake.rs @@ -1,4 +1,4 @@ -use serde::de::{Deserialize, Deserializer, Error}; +use serde::{de::Error, Deserialize, Deserializer}; pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result where diff --git a/src/util.rs b/src/util.rs index 519bd8e..fee26de 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,4 @@ -use serde::de::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer}; #[inline(always)] pub(crate) fn deserialize_optional_string<'de, D>( diff --git a/src/webhook/vote.rs b/src/webhook/vote.rs index 2ec8fa1..75e2034 100644 --- a/src/webhook/vote.rs +++ b/src/webhook/vote.rs @@ -1,8 +1,5 @@ use crate::snowflake; -use serde::{ - de::{self, Deserializer}, - Deserialize, -}; +use serde::{Deserialize, Deserializer}; use std::collections::HashMap; /// A struct representing a dispatched [Top.gg](https://top.gg) bot/server vote event. @@ -40,7 +37,7 @@ fn deserialize_is_test<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - de::Deserialize::deserialize(deserializer).map(|s: &str| s == "test") + Deserialize::deserialize(deserializer).map(|s: &str| s == "test") } fn deserialize_query_string<'de, D>( @@ -49,23 +46,19 @@ fn deserialize_query_string<'de, D>( where D: Deserializer<'de>, { - Ok( - de::Deserialize::deserialize(deserializer) - .ok() - .map(|s: &str| { - let mut output = HashMap::new(); + Ok(Deserialize::deserialize(deserializer).ok().map(|s: &str| { + let mut output = HashMap::new(); - for mut it in s.split('&').map(|pair| pair.split('=')) { - if let (Some(k), Some(v)) = (it.next(), it.next()) { - if let Ok(v) = urlencoding::decode(v) { - output.insert(k.to_owned(), v.into_owned()); - } - } + for mut it in s.split('&').map(|pair| pair.split('=')) { + if let (Some(k), Some(v)) = (it.next(), it.next()) { + if let Ok(v) = urlencoding::decode(v) { + output.insert(k.to_owned(), v.into_owned()); } + } + } - output - }), - ) + output + })) } cfg_if::cfg_if! { From 26f2b3b889a52f2e37359ddc697253c619fe67a9 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 3 May 2023 22:12:57 +0700 Subject: [PATCH 06/13] style: refactors and prettier --- src/client.rs | 8 ++++---- src/http.rs | 4 ++-- src/snowflake.rs | 19 ++++++------------- src/util.rs | 2 +- src/webhook/actix.rs | 2 +- src/webhook/axum.rs | 2 +- 6 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/client.rs b/src/client.rs index 3df0df9..b2df4d5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -421,9 +421,9 @@ impl Client { self .inner .http - .request::(GET, &path, None) + .request(GET, &path, None) .await - .map(|res| unsafe { transmute(res.voted) }) + .map(|res: Voted| unsafe { transmute(res.voted) }) } /// Checks if the weekend multiplier is active. @@ -461,8 +461,8 @@ impl Client { self .inner .http - .request::(GET, "/weekend", None) + .request(GET, "/weekend", None) .await - .map(|res| res.is_weekend) + .map(|res: IsWeekend| res.is_weekend) } } diff --git a/src/http.rs b/src/http.rs index fa6cafb..e51ba77 100644 --- a/src/http.rs +++ b/src/http.rs @@ -68,12 +68,12 @@ impl Http { .map_err(|_| Error::InternalServerError)?; // we should never receive invalid raw HTTP responses - so unwrap_unchecked() is okay to use here - let status_code = unsafe { + let status_code: u16 = unsafe { response .split_ascii_whitespace() .nth(1) .unwrap_unchecked() - .parse::() + .parse() .unwrap_unchecked() }; diff --git a/src/snowflake.rs b/src/snowflake.rs index a3b48d8..c17ffa2 100644 --- a/src/snowflake.rs +++ b/src/snowflake.rs @@ -1,28 +1,21 @@ use serde::{de::Error, Deserialize, Deserializer}; +#[inline(always)] pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let s: &str = Deserialize::deserialize(deserializer)?; - - s.parse::().map_err(D::Error::custom) + Deserialize::deserialize(deserializer) + .and_then(|s: &str| s.parse::().map_err(D::Error::custom)) } +#[inline(always)] pub(crate) fn deserialize_vec<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { - let s: Vec<&str> = Deserialize::deserialize(deserializer)?; - let out = Vec::with_capacity(s.len()); - - Ok(s.into_iter().fold(out, |mut acc, next| { - if let Ok(next) = next.parse::() { - acc.push(next); - } - - acc - })) + Deserialize::deserialize(deserializer) + .map(|s: Vec<&str>| s.into_iter().filter_map(|next| next.parse().ok()).collect()) } /// A trait that represents any data type that can be interpreted as a snowflake/ID. diff --git a/src/util.rs b/src/util.rs index fee26de..a14a46e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -38,7 +38,7 @@ pub(crate) fn get_avatar(hash: &Option, discriminator: &str, id: u64) -> } None => format!("https://cdn.discordapp.com/embed/avatars/{}.png", unsafe { - discriminator.parse::().unwrap_unchecked() % 5 + discriminator.parse::().unwrap_unchecked() % 5u16 }), } } diff --git a/src/webhook/actix.rs b/src/webhook/actix.rs index 9e3d030..3da1e2c 100644 --- a/src/webhook/actix.rs +++ b/src/webhook/actix.rs @@ -13,7 +13,7 @@ impl FromRequest for IncomingVote { type Future = Pin>>>; fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - let json = Json::::from_request(req, payload); + let json: Vote = Json::from_request(req, payload); let req = req.clone(); Box::pin(async move { diff --git a/src/webhook/axum.rs b/src/webhook/axum.rs index d1266e8..2bbf09c 100644 --- a/src/webhook/axum.rs +++ b/src/webhook/axum.rs @@ -65,7 +65,7 @@ where (StatusCode::UNAUTHORIZED, ()).into_response() } -/// Creates a new `axum` [`Router`] for adding an on-vote event handler to your application logic. +/// Creates a new [`axum`] [`Router`] for adding an on-vote event handler to your application logic. /// `state` here is your webhook handler. /// /// # Examples From 7944c2e32251e6ae59c351d8e2a8c7acf68e4838 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 13 May 2023 10:44:11 +0700 Subject: [PATCH 07/13] refactor: better things for actix --- src/snowflake.rs | 18 +++++++++++----- src/webhook/actix.rs | 49 ++++++++++++++++++++++++++++++------------- src/webhook/rocket.rs | 2 +- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/snowflake.rs b/src/snowflake.rs index c17ffa2..db5dd5b 100644 --- a/src/snowflake.rs +++ b/src/snowflake.rs @@ -18,18 +18,24 @@ where .map(|s: Vec<&str>| s.into_iter().filter_map(|next| next.parse().ok()).collect()) } +mod private { + pub trait Sealed {} +} + /// A trait that represents any data type that can be interpreted as a snowflake/ID. -pub trait SnowflakeLike { +pub trait SnowflakeLike: private::Sealed { #[doc(hidden)] fn as_snowflake(&self) -> u64; } -macro_rules! impl_snowflake_tryfrom( +macro_rules! impl_snowflake_as( ($($t:ty),+) => {$( + impl private::Sealed for $t {} + impl SnowflakeLike for $t { #[inline(always)] fn as_snowflake(&self) -> u64 { - (*self).try_into().unwrap() + *self as _ } } )+} @@ -37,14 +43,16 @@ macro_rules! impl_snowflake_tryfrom( macro_rules! impl_snowflake_fromstr( ($($t:ty),+) => {$( + impl private::Sealed for $t {} + impl SnowflakeLike for $t { #[inline(always)] fn as_snowflake(&self) -> u64 { - self.parse().expect("invalid snowflake") + self.parse().unwrap() } } )+} ); -impl_snowflake_tryfrom!(u64, i128, u128, isize, usize); +impl_snowflake_as!(u64, i128, u128, isize, usize); impl_snowflake_fromstr!(str, String); diff --git a/src/webhook/actix.rs b/src/webhook/actix.rs index 3da1e2c..f4f77a0 100644 --- a/src/webhook/actix.rs +++ b/src/webhook/actix.rs @@ -5,30 +5,49 @@ use actix_web::{ web::Json, FromRequest, HttpRequest, }; -use core::{future::Future, pin::Pin}; +use core::{ + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; -#[cfg_attr(docsrs, doc(cfg(feature = "actix")))] -impl FromRequest for IncomingVote { - type Error = Error; - type Future = Pin>>>; +#[doc(hidden)] +pub struct IncomingVoteFut { + req: HttpRequest, + json_fut: as FromRequest>::Future, +} - fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - let json: Vote = Json::from_request(req, payload); - let req = req.clone(); +impl Future for IncomingVoteFut { + type Output = Result; - Box::pin(async move { - let headers = req.headers(); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if let Ok(json) = ready!(Pin::new(&mut self.json_fut).poll(cx)) { + let headers = self.req.headers(); if let Some(authorization) = headers.get("Authorization") { if let Ok(authorization) = authorization.to_str() { - return Ok(Self { + return Poll::Ready(Ok(IncomingVote { authorization: authorization.to_owned(), - vote: json.await?.into_inner(), - }); + vote: json.into_inner(), + })); } } + } + + Poll::Ready(Err(ErrorUnauthorized("401"))) + } +} - Err(ErrorUnauthorized("401")) - }) +#[cfg_attr(docsrs, doc(cfg(feature = "actix")))] +impl FromRequest for IncomingVote { + type Error = Error; + type Future = IncomingVoteFut; + + #[inline(always)] + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + IncomingVoteFut { + req: req.clone(), + json_fut: Json::from_request(req, payload), + } } } diff --git a/src/webhook/rocket.rs b/src/webhook/rocket.rs index 43ed186..b9f44c9 100644 --- a/src/webhook/rocket.rs +++ b/src/webhook/rocket.rs @@ -10,7 +10,7 @@ use rocket::{ impl FromDataSimple for IncomingVote { type Error = (); - fn from_data(request: &Request<'_>, data: Data) -> data::Outcome { + fn from_data(request: &Request<'_>, data: Data) -> Outcome { let headers = request.headers(); if let Some(authorization) = headers.get_one("Authorization") { From b1621388dbd5b398dd12d35313ad95e764b8f4d2 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 13 May 2023 11:13:49 +0700 Subject: [PATCH 08/13] revert: remove 403 support --- src/client.rs | 1 - src/error.rs | 4 ---- src/http.rs | 26 +++++++++++++++----------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/client.rs b/src/client.rs index b2df4d5..09d3e28 100644 --- a/src/client.rs +++ b/src/client.rs @@ -290,7 +290,6 @@ impl Client { /// # Errors /// /// Errors if the following conditions are met: - /// - Your bot receives more than 1000 votes monthly. Please use webhooks instead. ([`Forbidden`][crate::Error::Forbidden]) /// - 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 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]) diff --git a/src/error.rs b/src/error.rs index 0673129..3265b60 100644 --- a/src/error.rs +++ b/src/error.rs @@ -48,9 +48,6 @@ impl error::Error for InternalError { /// A struct representing an error coming from this SDK - unexpected or not. #[derive(Debug)] pub enum Error { - /// The client is not allowed to send a HTTP request to this endpoint. - Forbidden, - /// An unexpected internal error coming from the client itself, preventing it from sending a request to the [Top.gg](https://top.gg) API. InternalClientError(InternalError), @@ -70,7 +67,6 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Forbidden => write!(f, "forbidden"), Self::InternalClientError(err) => write!(f, "internal client error: {err}"), Self::InternalServerError => write!(f, "internal server error"), Self::NotFound => write!(f, "not found"), diff --git a/src/http.rs b/src/http.rs index e51ba77..737a8c7 100644 --- a/src/http.rs +++ b/src/http.rs @@ -78,19 +78,23 @@ impl Http { }; match status_code { - 401 => panic!("unauthorized"), - 403 => Err(Error::Forbidden), - 404 => Err(Error::NotFound), - 429 => Err(Error::Ratelimit { - retry_after: serde_json::from_str::(&response) - .map_err(|_| Error::InternalServerError)? - .retry_after, - }), - 500.. => Err(Error::InternalServerError), _ => { - response.drain(unsafe { ..response.find("\r\n\r\n").unwrap_unchecked() + 4 }); + if status_code >= 400 { + Err(match status_code { + 401 => panic!("unauthorized"), + 404 => Error::NotFound, + 429 => Error::Ratelimit { + retry_after: serde_json::from_str::(&response) + .map_err(|_| Error::InternalServerError)? + .retry_after, + }, + _ => Error::InternalServerError, + }) + } else { + response.drain(unsafe { ..response.find("\r\n\r\n").unwrap_unchecked() + 4 }); - Ok(response) + Ok(response) + } } } } From 1963dfd015f49bfabc923015811469dbc0128a20 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 13 May 2023 11:29:19 +0700 Subject: [PATCH 09/13] doc: fix grammar errors and rename delay to interval --- README.md | 2 +- src/autoposter.rs | 8 ++++---- src/client.rs | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f2ba193..6be2d72 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ This library provides several feature flags that can be enabled/disabled in `Car ## Examples -More things can be read on the [documentation](https://docs.rs/topgg). +More things can be read in the [documentation](https://docs.rs/topgg).
api: Fetching a single Discord user from it's Discord ID diff --git a/src/autoposter.rs b/src/autoposter.rs index 62a0143..18e09e1 100644 --- a/src/autoposter.rs +++ b/src/autoposter.rs @@ -6,21 +6,21 @@ use tokio::{ time::{sleep, Duration}, }; -/// A struct that lets you automate the process of posting bot statistics to the [Top.gg](https://top.gg) API. +/// A struct that lets you automate the process of posting bot statistics to the [Top.gg](https://top.gg) API in intervals. pub struct Autoposter { thread: JoinHandle<()>, data: Arc>>, } impl Autoposter { - pub(crate) fn new(client: Arc, delay: u64) -> Self { + pub(crate) fn new(client: Arc, interval: u64) -> Self { let current_thread_data = Arc::new(Mutex::new(None)); let thread_data = Arc::clone(¤t_thread_data); Self { thread: spawn(async move { loop { - sleep(Duration::from_secs(delay)).await; + sleep(Duration::from_secs(interval)).await; let lock = thread_data.lock().await; @@ -33,7 +33,7 @@ impl Autoposter { } } - /// Feeds new bot stats to the autoposter. The autoposter will automatically post it to the [Top.gg](https://top.gg) servers once the delay is complete. + /// Feeds new bot stats to the autoposter. The autoposter will automatically post it to the [Top.gg](https://top.gg) servers in intervals. /// /// # Examples /// diff --git a/src/client.rs b/src/client.rs index 09d3e28..ef13d99 100644 --- a/src/client.rs +++ b/src/client.rs @@ -237,11 +237,11 @@ impl Client { 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](https://top.gg) API. + /// 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](https://top.gg) API in intervals. /// /// # Panics /// - /// Panics if the delay argument is shorter than 15 minutes (900 seconds) + /// Panics if the interval argument is shorter than 15 minutes (900 seconds) /// /// # Examples /// @@ -268,17 +268,17 @@ impl Client { #[cfg(feature = "autoposter")] #[cfg_attr(docsrs, doc(cfg(feature = "autoposter")))] #[must_use] - pub fn new_autoposter(&self, seconds_delay: D) -> Autoposter + pub fn new_autoposter(&self, seconds_interval: D) -> Autoposter where D: Into, { - let seconds_delay = seconds_delay.into(); + let seconds_interval = seconds_interval.into(); - if seconds_delay < 900 { - panic!("the delay mustn't be shorter than 15 minutes (900 seconds)"); + if seconds_interval < 900 { + panic!("the interval mustn't be shorter than 15 minutes (900 seconds)"); } - Autoposter::new(Arc::clone(&self.inner), seconds_delay) + Autoposter::new(Arc::clone(&self.inner), seconds_interval) } /// Fetches your Discord bot's last 1000 voters. From b01396a594e2be12515147947eb077be5bdb66f0 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 13 May 2023 18:48:50 +0700 Subject: [PATCH 10/13] feat: new default avatar method + deprecate discriminators --- README.md | 2 -- src/bot.rs | 19 +++---------------- src/client.rs | 4 ++-- src/http.rs | 1 + src/user.rs | 18 +++++------------- src/util.rs | 9 +++++---- 6 files changed, 16 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 6be2d72..b77f4a0 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ async fn main() { let user = client.get_user(661200758510977084u64).await.unwrap(); assert_eq!(user.username, "null"); - assert_eq!(user.discriminator, "8626"); assert_eq!(user.id, 661200758510977084u64); println!("{:?}", user); @@ -69,7 +68,6 @@ async fn main() { let bot = client.get_bot(264811613708746752u64).await.unwrap(); assert_eq!(bot.username, "Luca"); - assert_eq!(bot.discriminator, "1375"); assert_eq!(bot.id, 264811613708746752u64); println!("{:?}", bot); diff --git a/src/bot.rs b/src/bot.rs index 231b344..b42b4eb 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -16,7 +16,7 @@ pub struct Bot { /// The username of this Discord bot. pub username: String, - /// The discriminator of this Discord bot. + #[deprecated(since = "1.1.0")] pub discriminator: String, /// The prefix of this Discord bot. @@ -135,7 +135,7 @@ impl Bot { #[must_use] #[inline(always)] pub fn avatar(&self) -> String { - util::get_avatar(&self.avatar, &self.discriminator, self.id) + util::get_avatar(&self.avatar, self.id) } /// The invite URL of this Discord bot. @@ -228,7 +228,6 @@ impl Debug for Bot { .debug_struct("Bot") .field("id", &self.id) .field("username", &self.username) - .field("discriminator", &self.discriminator) .field("prefix", &self.prefix) .field("short_description", &self.short_description) .field("long_description", &self.long_description) @@ -471,19 +470,7 @@ impl Filter { self } - /// Filters only Discord bots that matches this discriminator. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// use topgg::Filter; - /// - /// let _filter = Filter::new() - /// .discriminator("1536"); - /// ``` - #[must_use] + #[deprecated(since = "1.1.0")] pub fn discriminator(mut self, new_discriminator: &D) -> Self where D: AsRef + ?Sized, diff --git a/src/client.rs b/src/client.rs index ef13d99..7cc693e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -17,6 +17,7 @@ cfg_if::cfg_if! { } } +#[derive(Clone)] pub(crate) struct InnerClient { http: Http, } @@ -36,6 +37,7 @@ impl InnerClient { } /// A struct representing a [Top.gg](https://top.gg) API client instance. +#[derive(Clone)] pub struct Client { inner: SyncedClient, } @@ -102,7 +104,6 @@ impl Client { /// let user = client.get_user(661200758510977084u64).await.unwrap(); /// /// assert_eq!(user.username, "null"); - /// assert_eq!(user.discriminator, "8626"); /// assert_eq!(user.id, 661200758510977084u64); /// /// println!("{:?}", user); @@ -148,7 +149,6 @@ impl Client { /// let bot = client.get_bot(264811613708746752u64).await.unwrap(); /// /// assert_eq!(bot.username, "Luca"); - /// assert_eq!(bot.discriminator, "1375"); /// assert_eq!(bot.id, 264811613708746752u64); /// /// println!("{:?}", bot); diff --git a/src/http.rs b/src/http.rs index 737a8c7..72263de 100644 --- a/src/http.rs +++ b/src/http.rs @@ -13,6 +13,7 @@ pub(crate) struct Ratelimit { pub(crate) retry_after: u16, } +#[derive(Clone)] pub(crate) struct Http { token: String, } diff --git a/src/user.rs b/src/user.rs index fbd1e1e..0c062f2 100644 --- a/src/user.rs +++ b/src/user.rs @@ -36,7 +36,7 @@ pub struct User { /// The username of this user. pub username: String, - /// The Discord discriminator of this user. + #[deprecated(since = "1.1.0")] pub discriminator: String, /// The user's bio. @@ -99,7 +99,7 @@ impl User { /// ``` #[inline(always)] pub fn avatar(&self) -> String { - util::get_avatar(&self.avatar, &self.discriminator, self.id) + util::get_avatar(&self.avatar, self.id) } } @@ -109,7 +109,6 @@ impl Debug for User { .debug_struct("User") .field("id", &self.id) .field("username", &self.username) - .field("discriminator", &self.discriminator) .field("bio", &self.bio) .field("banner", &self.banner) .field("socials", &self.socials) @@ -159,19 +158,12 @@ impl Voter { /// let client = Client::new(token); /// /// for voter in client.get_voters().await.unwrap() { - /// println!("{}", voter.avatar().unwrap_or(String::from("No avatar :("))); + /// println!("{}", voter.avatar()); /// } /// } /// ``` #[must_use] - pub fn avatar(&self) -> Option { - self.avatar.as_ref().map(|hash| { - let ext = if hash.starts_with("a_") { "gif" } else { "png" }; - - format!( - "https://cdn.discordapp.com/avatars/{}/{hash}.{ext}?size=1024", - self.id - ) - }) + pub fn avatar(&self) -> String { + util::get_avatar(&self.avatar, self.id) } } diff --git a/src/util.rs b/src/util.rs index a14a46e..050334b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -29,7 +29,7 @@ where Option::deserialize(deserializer).map(|res| res.unwrap_or_default()) } -pub(crate) fn get_avatar(hash: &Option, discriminator: &str, id: u64) -> String { +pub(crate) fn get_avatar(hash: &Option, id: u64) -> String { match hash { Some(hash) => { let ext = if hash.starts_with("a_") { "gif" } else { "png" }; @@ -37,8 +37,9 @@ pub(crate) fn get_avatar(hash: &Option, discriminator: &str, id: u64) -> format!("https://cdn.discordapp.com/avatars/{id}/{hash}.{ext}?size=1024") } - None => format!("https://cdn.discordapp.com/embed/avatars/{}.png", unsafe { - discriminator.parse::().unwrap_unchecked() % 5u16 - }), + _ => format!( + "https://cdn.discordapp.com/embed/avatars/{}.png", + (id >> 22) as u16 % 5 + ), } } From cfd151df3311fed7852d9fe2d0e0ef7858d389c3 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 13 May 2023 20:41:25 +0700 Subject: [PATCH 11/13] refactor: #[must_use] in structs instead --- src/autoposter.rs | 1 + src/bot.rs | 75 +++++++++++++++++++++++-------------------- src/client.rs | 2 +- src/http.rs | 32 ++++++++---------- src/user.rs | 16 ++++++++- src/webhook/rocket.rs | 3 +- src/webhook/vote.rs | 35 +++++++++++--------- 7 files changed, 92 insertions(+), 72 deletions(-) diff --git a/src/autoposter.rs b/src/autoposter.rs index 18e09e1..82fc784 100644 --- a/src/autoposter.rs +++ b/src/autoposter.rs @@ -7,6 +7,7 @@ use tokio::{ }; /// A struct that lets you automate the process of posting bot statistics to the [Top.gg](https://top.gg) API in intervals. +#[must_use] pub struct Autoposter { thread: JoinHandle<()>, data: Arc>>, diff --git a/src/bot.rs b/src/bot.rs index b42b4eb..541fb96 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -7,6 +7,7 @@ use core::{ use serde::{Deserialize, Deserializer, Serialize}; /// A struct representing a Discord Bot listed on [Top.gg](https://top.gg). +#[must_use] #[derive(Clone, Deserialize)] pub struct Bot { /// The ID of this Discord bot. @@ -257,6 +258,7 @@ pub(crate) struct Bots { } /// A struct representing a Discord bot's statistics returned from the API. +#[must_use] #[derive(Clone, Deserialize)] pub struct Stats { /// The bot's server count per shard. @@ -334,6 +336,7 @@ impl Debug for Stats { } /// A struct representing a Discord bot's statistics [to be posted][crate::Client::post_stats] to the API. +#[must_use] #[derive(Serialize)] pub struct NewStats { server_count: u64, @@ -354,7 +357,6 @@ impl NewStats { /// /// let _stats = NewStats::count_based(12345, Some(10)); /// ``` - #[must_use] #[inline(always)] pub fn count_based(server_count: A, shard_count: Option) -> Self where @@ -385,7 +387,6 @@ impl NewStats { /// // The shard posting this data has 456 servers. /// let _stats = NewStats::shards_based([123, 456, 789], Some(1)); /// ``` - #[must_use] pub fn shards_based(shards: A, shard_index: Option) -> Self where A: IntoIterator, @@ -427,6 +428,8 @@ pub(crate) struct IsWeekend { } /// A struct for filtering the query in [`get_bots`][crate::Client::get_bots]. +#[must_use] +#[derive(Clone)] pub struct Filter(String); impl Filter { @@ -441,7 +444,6 @@ impl Filter { /// /// let _filter = Filter::new(); /// ``` - #[must_use] #[inline(always)] pub fn new() -> Self { Self(String::new()) @@ -459,14 +461,14 @@ impl Filter { /// let _filter = Filter::new() /// .username("shiro"); /// ``` - #[must_use] pub fn username(mut self, new_username: &U) -> Self where U: AsRef + ?Sized, { - self - .0 - .push_str(&format!("username: {} ", new_username.as_ref())); + self.0.push_str(&format!( + "username%3A%20{}%20", + urlencoding::encode(new_username.as_ref()) + )); self } @@ -475,9 +477,10 @@ impl Filter { where D: AsRef + ?Sized, { - self - .0 - .push_str(&format!("discriminator: {} ", new_discriminator.as_ref())); + self.0.push_str(&format!( + "discriminator%3A%20{}%20", + new_discriminator.as_ref() + )); self } @@ -493,14 +496,14 @@ impl Filter { /// let _filter = Filter::new() /// .prefix("!"); /// ``` - #[must_use] pub fn prefix

(mut self, new_prefix: &P) -> Self where P: AsRef + ?Sized, { - self - .0 - .push_str(&format!("prefix: {} ", new_prefix.as_ref())); + self.0.push_str(&format!( + "prefix%3A%20{}%20", + urlencoding::encode(new_prefix.as_ref()) + )); self } @@ -516,12 +519,13 @@ impl Filter { /// let _filter = Filter::new() /// .votes(1000); /// ``` - #[must_use] pub fn votes(mut self, new_votes: V) -> Self where V: Into, { - self.0.push_str(&format!("points: {} ", new_votes.into())); + self + .0 + .push_str(&format!("points%3A%20{}%20", new_votes.into())); self } @@ -537,14 +541,14 @@ impl Filter { /// let _filter = Filter::new() /// .monthly_votes(100); /// ``` - #[must_use] pub fn monthly_votes(mut self, new_monthly_votes: M) -> Self where M: Into, { - self - .0 - .push_str(&format!("monthlyPoints: {} ", new_monthly_votes.into())); + self.0.push_str(&format!( + "monthlyPoints%3A%20{}%20", + new_monthly_votes.into() + )); self } @@ -560,14 +564,13 @@ impl Filter { /// let _filter = Filter::new() /// .certified(true); /// ``` - #[must_use] pub fn certified(mut self, is_certified: C) -> Self where C: Into, { self .0 - .push_str(&format!("certifiedBot: {} ", is_certified.into())); + .push_str(&format!("certifiedBot%3A%20{}%20", is_certified.into())); self } @@ -583,14 +586,14 @@ impl Filter { /// let _filter = Filter::new() /// .vanity("mee6"); /// ``` - #[must_use] pub fn vanity(mut self, new_vanity: &V) -> Self where V: AsRef + ?Sized, { - self - .0 - .push_str(&format!("vanity: {} ", new_vanity.as_ref())); + self.0.push_str(&format!( + "vanity%3A%20{}%20", + urlencoding::encode(new_vanity.as_ref()) + )); self } } @@ -614,6 +617,8 @@ impl Default for Filter { } /// A struct for configuring the query in [`get_bots`][crate::Client::get_bots]. +#[must_use] +#[derive(Clone)] pub struct Query(String); impl Query { @@ -628,7 +633,6 @@ impl Query { /// /// let _query = Query::new(); /// ``` - #[must_use] #[inline(always)] pub fn new() -> Self { Self(String::from("?")) @@ -646,7 +650,6 @@ impl Query { /// let _query = Query::new() /// .limit(250u16); /// ``` - #[must_use] pub fn limit(mut self, new_limit: N) -> Self where N: Into, @@ -670,7 +673,6 @@ impl Query { /// .limit(250u16) /// .skip(100u16); /// ``` - #[must_use] pub fn skip(mut self, skip_by: S) -> Self where S: Into, @@ -699,12 +701,10 @@ impl Query { /// .skip(100u16) /// .filter(filter); /// ``` - #[must_use] - pub fn filter(mut self, mut new_filter: Filter) -> Self { - new_filter.0.pop(); + pub fn filter(mut self, new_filter: Filter) -> Self { self .0 - .push_str(&format!("search={}&", urlencoding::encode(&new_filter.0))); + .push_str(&format!("search={}&", new_filter.into_query_string())); self } } @@ -733,8 +733,13 @@ impl QueryLike for Query { impl QueryLike for Filter { #[inline(always)] fn into_query_string(mut self) -> String { - self.0.pop(); - format!("?search={}", urlencoding::encode(&self.0)) + if self.0.is_empty() { + String::new() + } else { + self.0.truncate(self.0.len() - 3); + + format!("?search={}", self.0) + } } } diff --git a/src/client.rs b/src/client.rs index 7cc693e..8074cfd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -37,6 +37,7 @@ impl InnerClient { } /// A struct representing a [Top.gg](https://top.gg) API client instance. +#[must_use] #[derive(Clone)] pub struct Client { inner: SyncedClient, @@ -60,7 +61,6 @@ impl Client { /// let _client = Client::new(token); /// } /// ``` - #[must_use] #[inline(always)] pub fn new(token: String) -> Self { let inner = InnerClient { diff --git a/src/http.rs b/src/http.rs index 72263de..c6e8a48 100644 --- a/src/http.rs +++ b/src/http.rs @@ -78,25 +78,21 @@ impl Http { .unwrap_unchecked() }; - match status_code { - _ => { - if status_code >= 400 { - Err(match status_code { - 401 => panic!("unauthorized"), - 404 => Error::NotFound, - 429 => Error::Ratelimit { - retry_after: serde_json::from_str::(&response) - .map_err(|_| Error::InternalServerError)? - .retry_after, - }, - _ => Error::InternalServerError, - }) - } else { - response.drain(unsafe { ..response.find("\r\n\r\n").unwrap_unchecked() + 4 }); + if status_code >= 400 { + Err(match status_code { + 401 => panic!("unauthorized"), + 404 => Error::NotFound, + 429 => Error::Ratelimit { + retry_after: serde_json::from_str::(&response) + .map_err(|_| Error::InternalServerError)? + .retry_after, + }, + _ => Error::InternalServerError, + }) + } else { + response.drain(unsafe { ..response.find("\r\n\r\n").unwrap_unchecked() + 4 }); - Ok(response) - } - } + Ok(response) } } diff --git a/src/user.rs b/src/user.rs index 0c062f2..fcccd32 100644 --- a/src/user.rs +++ b/src/user.rs @@ -27,6 +27,7 @@ pub struct Socials { } /// A struct representing a user logged into [Top.gg](https://top.gg). +#[must_use] #[derive(Clone, Deserialize)] pub struct User { /// The Discord ID of this user. @@ -97,6 +98,7 @@ impl User { /// println!("{}", user.avatar()); /// } /// ``` + #[must_use] #[inline(always)] pub fn avatar(&self) -> String { util::get_avatar(&self.avatar, self.id) @@ -128,7 +130,8 @@ pub(crate) struct Voted { } /// A struct representing a user who has voted on a Discord bot listed on [Top.gg](https://top.gg). (See [crate::Client::get_voters`]) -#[derive(Clone, Debug, Deserialize)] +#[must_use] +#[derive(Clone, Deserialize)] pub struct Voter { /// The Discord ID of this user. #[serde(deserialize_with = "snowflake::deserialize")] @@ -167,3 +170,14 @@ impl Voter { util::get_avatar(&self.avatar, self.id) } } + +impl Debug for Voter { + fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { + fmt + .debug_struct("Voter") + .field("id", &self.id) + .field("username", &self.username) + .field("avatar", &self.avatar()) + .finish() + } +} diff --git a/src/webhook/rocket.rs b/src/webhook/rocket.rs index b9f44c9..0735f75 100644 --- a/src/webhook/rocket.rs +++ b/src/webhook/rocket.rs @@ -1,9 +1,8 @@ use crate::IncomingVote; use rocket::{ - data::{self, Data, FromDataSimple}, + data::{Data, FromDataSimple, Outcome}, http::Status, request::Request, - Outcome, }; #[cfg_attr(docsrs, doc(cfg(feature = "rocket")))] diff --git a/src/webhook/vote.rs b/src/webhook/vote.rs index 75e2034..df71749 100644 --- a/src/webhook/vote.rs +++ b/src/webhook/vote.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Deserializer}; use std::collections::HashMap; /// A struct representing a dispatched [Top.gg](https://top.gg) bot/server vote event. +#[must_use] #[cfg_attr(docsrs, doc(cfg(feature = "webhook")))] #[derive(Clone, Debug, Deserialize)] pub struct Vote { @@ -27,9 +28,9 @@ pub struct Vote { #[serde(default, rename = "isWeekend")] pub is_weekend: bool, - /// Query strings found on the vote page, if any. + /// Query strings found on the vote page. #[serde(default, deserialize_with = "deserialize_query_string")] - pub query: Option>, + pub query: HashMap, } #[inline(always)] @@ -40,31 +41,35 @@ where Deserialize::deserialize(deserializer).map(|s: &str| s == "test") } -fn deserialize_query_string<'de, D>( - deserializer: D, -) -> Result>, D::Error> +fn deserialize_query_string<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { - Ok(Deserialize::deserialize(deserializer).ok().map(|s: &str| { - let mut output = HashMap::new(); + Ok( + Deserialize::deserialize(deserializer) + .map(|s: &str| { + let mut output = HashMap::new(); - for mut it in s.split('&').map(|pair| pair.split('=')) { - if let (Some(k), Some(v)) = (it.next(), it.next()) { - if let Ok(v) = urlencoding::decode(v) { - output.insert(k.to_owned(), v.into_owned()); + for mut it in s.split('&').map(|pair| pair.split('=')) { + if let (Some(k), Some(v)) = (it.next(), it.next()) { + if let Ok(v) = urlencoding::decode(v) { + output.insert(k.to_owned(), v.into_owned()); + } + } } - } - } - output - })) + output + }) + .unwrap_or_default(), + ) } cfg_if::cfg_if! { if #[cfg(any(feature = "actix", feature = "rocket"))] { /// A struct that represents an unauthenticated request containing a [`Vote`] data. + #[must_use] #[cfg_attr(docsrs, doc(cfg(any(feature = "actix", feature = "rocket"))))] + #[derive(Clone)] pub struct IncomingVote { pub(crate) authorization: String, pub(crate) vote: Vote, From da35c70c969082678e4418fc7e9ef12b2b4e677b Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 13 May 2023 20:43:45 +0700 Subject: [PATCH 12/13] refactor: remove must_use in Client#new_autoposter --- src/client.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 8074cfd..9784a7b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -267,7 +267,6 @@ impl Client { /// ``` #[cfg(feature = "autoposter")] #[cfg_attr(docsrs, doc(cfg(feature = "autoposter")))] - #[must_use] pub fn new_autoposter(&self, seconds_interval: D) -> Autoposter where D: Into, From 247f4318da1b3fa75c2ce3046610818294729698 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Sat, 13 May 2023 20:49:20 +0700 Subject: [PATCH 13/13] style: refactor imports --- src/http.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/http.rs b/src/http.rs index c6e8a48..a3a542d 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,7 +1,6 @@ use crate::{Error, InternalError, Result}; use serde::{de::DeserializeOwned, Deserialize}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; +use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream}; use tokio_native_tls::{native_tls, TlsConnector}; pub(crate) const GET: &str = "GET";