Skip to content

Commit

Permalink
refactor: change get_bots and remove async_trait dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
null8626 committed Jan 6, 2024
1 parent f861315 commit 49e2f12
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 200 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ rustc-args = ["--cfg", "docsrs"]
[features]
default = ["api"]
api = ["chrono", "reqwest", "serde_json", "urlencoding"]
autoposter = ["api", "async-trait", "tokio"]
autoposter = ["api", "tokio"]

serenity = ["dep:serenity"]
serenity-cached = ["serenity", "serenity/cache"]
Expand Down
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,9 @@ impl EventHandler for Handler {
#[tokio::main]
async fn main() {
// if you also want to communicate with the Top.gg API, you can do this
let topgg_client = topgg::Client::new(env!("TOPGG_TOKEN").to_string());
let autoposter = Autoposter::serenity(&topgg_client, Duration::from_secs(1800));
// otherwise you can do this
let topgg_token = env!("TOPGG_TOKEN").to_string();
let autoposter = Autoposter::serenity(&topgg_token, Duration::from_secs(1800));
let bot_token = env!("DISCORD_TOKEN").to_string();
let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::GUILDS | GatewayIntents::MESSAGE_CONTENT;
Expand Down
26 changes: 7 additions & 19 deletions src/autoposter/client.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
use crate::{InnerClient, Stats};
use crate::InnerClient;
use std::sync::Arc;

#[async_trait::async_trait]
pub trait IntoClientSealed {
type ArcInner: Send + Sync + 'static;

fn get_arc(&self) -> Arc<Self::ArcInner>;
async fn post_stats(arc: &Self::ArcInner, stats: &Stats);
pub trait AsClientSealed {
fn as_client(&self) -> Arc<InnerClient>;
}

/// A private trait that represents any datatype that can be interpreted as a [Top.gg API](https://docs.top.gg) Client.
///
/// This can either be a reference to an existing [`Client`][crate::Client] or a [`&str`][core::str] representing a [Top.gg API](https://docs.top.gg) token.
pub trait IntoClient: IntoClientSealed {}

#[async_trait::async_trait]
impl IntoClientSealed for str {
type ArcInner = InnerClient;
pub trait AsClient: AsClientSealed {}

impl AsClientSealed for str {
#[inline(always)]
fn get_arc(&self) -> Arc<Self::ArcInner> {
fn as_client(&self) -> Arc<InnerClient> {
Arc::new(InnerClient::new(String::from(self)))
}

#[inline(always)]
async fn post_stats(arc: &Self::ArcInner, stats: &Stats) {
let _ = arc.post_stats(stats).await;
}
}

impl IntoClient for str {}
impl AsClient for str {}
37 changes: 22 additions & 15 deletions src/autoposter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use tokio::{

mod client;

pub use client::IntoClient;
pub(crate) use client::IntoClientSealed;
pub use client::AsClient;
pub(crate) use client::AsClientSealed;

cfg_if::cfg_if! {
if #[cfg(feature = "serenity")] {
Expand Down Expand Up @@ -44,6 +44,13 @@ pub struct SharedStatsGuard<'a> {
}

impl SharedStatsGuard<'_> {
/// Directly replaces the current [`Stats`] inside with the other.
#[inline(always)]
pub fn replace(&mut self, other: Stats) {
let ref_mut = self.guard.deref_mut();
*ref_mut = other;
}

/// Sets the current [`Stats`] server count.
#[inline(always)]
pub fn set_server_count(&mut self, server_count: usize) {
Expand Down Expand Up @@ -130,22 +137,22 @@ where
{
/// Creates an [`Autoposter`] struct as well as immediately starting the thread. The thread will never stop until this struct gets dropped.
///
/// - `into_client` can either be a reference to an existing [`Client`][crate::Client] or a [`&str`][core::str] representing a [Top.gg API](https://docs.top.gg) token.
/// - `client` can either be a reference to an existing [`Client`][crate::Client] or a [`&str`][core::str] representing a [Top.gg API](https://docs.top.gg) token.
/// - `handler` is a struct that handles the *retrieving stats* part before being sent to the [`Autoposter`]. This datatype is essentially the bridge between an external third-party Discord Bot library between this library.
///
/// # Panics
///
/// Panics if the interval argument is shorter than 15 minutes (900 seconds).
pub fn new<C>(into_client: &C, interval: Duration, handler: H) -> Self
pub fn new<C>(client: &C, interval: Duration, handler: H) -> Self
where
C: IntoClient,
C: AsClient,
{
assert!(
interval.as_secs() >= 900,
"The interval mustn't be shorter than 15 minutes."
);

let client = into_client.get_arc();
let client = client.as_client();
let handler = Arc::new(handler);

Self {
Expand All @@ -157,7 +164,7 @@ where
{
let stats = handler.stats().stats.read().await;

<C as IntoClientSealed>::post_stats(client.deref(), stats.deref()).await;
let _ = client.post_stats(&stats).await;
};

sleep(interval).await;
Expand Down Expand Up @@ -186,35 +193,35 @@ impl<H> Deref for Autoposter<H> {
impl Autoposter<Serenity> {
/// Creates an [`Autoposter`] struct from an existing built-in *[serenity]* [`Handler`] as well as immediately starting the thread. The thread will never stop until this struct gets dropped.
///
/// - `into_client` can either be a reference to an existing [`Client`][crate::Client] or a [`&str`][core::str] representing a [Top.gg API](https://docs.top.gg) token.
/// - `client` can either be a reference to an existing [`Client`][crate::Client] or a [`&str`][core::str] representing a [Top.gg API](https://docs.top.gg) token.
///
/// # Panics
///
/// Panics if the interval argument is shorter than 15 minutes (900 seconds).
#[inline(always)]
pub fn serenity<C>(into_client: &C, interval: Duration) -> Self
pub fn serenity<C>(client: &C, interval: Duration) -> Self
where
C: IntoClient,
C: AsClient,
{
Self::new(into_client, interval, Serenity::new())
Self::new(client, interval, Serenity::new())
}
}

#[cfg(feature = "twilight")]
impl Autoposter<Twilight> {
/// Creates an [`Autoposter`] struct from an existing built-in *twilight* [`Handler`] as well as immediately starting the thread. The thread will never stop until this struct gets dropped.
///
/// - `into_client` can either be a reference to an existing [`Client`][crate::Client] or a [`&str`][core::str] representing a [Top.gg API](https://docs.top.gg) token.
/// - `client` can either be a reference to an existing [`Client`][crate::Client] or a [`&str`][core::str] representing a [Top.gg API](https://docs.top.gg) token.
///
/// # Panics
///
/// Panics if the interval argument is shorter than 15 minutes (900 seconds).
#[inline(always)]
pub fn twilight<C>(into_client: &C, interval: Duration) -> Self
pub fn twilight<C>(client: &C, interval: Duration) -> Self
where
C: IntoClient,
C: AsClient,
{
Self::new(into_client, interval, Twilight::new())
Self::new(client, interval, Twilight::new())
}
}

Expand Down
173 changes: 55 additions & 118 deletions src/bot.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::{snowflake, util};
use crate::{snowflake, util, Client};
use chrono::{DateTime, Utc};
use core::{
cmp::min,
fmt::{self, Debug, Formatter},
future::{Future, IntoFuture},
};
use serde::{Deserialize, Deserializer, Serialize};
use std::pin::Pin;

/// A struct representing a Discord Bot listed on [Top.gg](https://top.gg).
#[must_use]
Expand Down Expand Up @@ -234,10 +236,10 @@ pub struct Stats {
}

impl Stats {
/// Creates a [`Stats`] struct from a serenity [`Context`][serenity::client::Context].
/// Creates a [`Stats`] struct from the cache of a serenity [`Context`][serenity::client::Context].
#[inline(always)]
#[cfg(feature = "serenity")]
#[cfg_attr(docsrs, doc(cfg(feature = "serenity")))]
#[cfg(feature = "serenity-cached")]
#[cfg_attr(docsrs, doc(cfg(feature = "serenity-cached")))]
pub fn from_context(context: &serenity::client::Context) -> Self {
Self::from_count(
context.cache.guilds().len(),
Expand Down Expand Up @@ -355,145 +357,80 @@ pub(crate) struct IsWeekend {
pub(crate) is_weekend: bool,
}

/// 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);
/// ```
/// A struct for configuring the query for [`get_bots`][crate::Client::get_bots] before being sent to the [Top.gg API](https://docs.top.gg) by `await`ing it.
#[must_use]
#[derive(Clone)]
pub struct Query {
pub struct GetBots<'a> {
client: &'a Client,
query: String,
search: String,
}

impl Query {
/// Initiates a new empty querying struct.
macro_rules! get_bots_method {
($(
$(#[doc = $doc:literal])*
$input_name:ident: $input_type:ty = $property:ident($($format:tt)*);
)*) => {$(
$(#[doc = $doc])*
pub fn $input_name(mut self, $input_name: $input_type) -> Self {
self.$property.push_str(&format!($($format)*));
self
}
)*};
}

impl<'a> GetBots<'a> {
#[inline(always)]
pub fn new() -> Self {
pub(crate) fn new(client: &'a Client) -> Self {
Self {
client,
query: String::from('?'),
search: String::new(),
}
}

/// Sets the maximum amount of bots to be queried.
pub fn limit(mut self, new_limit: u16) -> Self {
self
.query
.push_str(&format!("limit={}&", min(new_limit, 500)));
self
}
get_bots_method! {
/// Sets the maximum amount of bots to be queried.
limit: u16 = query("limit={}&", min(limit, 500));

/// Sets the amount of bots to be skipped during the query.
pub fn skip(mut self, skip_by: u16) -> Self {
self
.query
.push_str(&format!("offset={}&", min(skip_by, 499)));
self
}
/// Sets the amount of bots to be skipped during the GetBots.
skip: u16 = query("offset={}&", min(skip, 499));

/// Queries only Discord bots that matches this username.
pub fn username(mut self, new_username: &str) -> Self {
self.search.push_str(&format!(
"username%3A%20{}%20",
urlencoding::encode(new_username)
));
self
}
/// Queries only Discord bots that matches this username.
username: &str = search("username%3A%20{}%20", urlencoding::encode(username));

/// Queries only Discord bots that matches this discriminator.
pub fn discriminator(mut self, new_discriminator: &str) -> Self {
self
.search
.push_str(&format!("discriminator%3A%20{new_discriminator}%20"));
self
}
/// Queries only Discord bots that matches this discriminator.
discriminator: &str = search("discriminator%3A%20{discriminator}%20");

/// Queries only Discord bots that matches this prefix.
pub fn prefix(mut self, new_prefix: &str) -> Self {
self.search.push_str(&format!(
"prefix%3A%20{}%20",
urlencoding::encode(new_prefix)
));
self
}
/// Queries only Discord bots that matches this prefix.
prefix: &str = search("prefix%3A%20{}%20", urlencoding::encode(prefix));

/// Queries only Discord bots that has this vote count.
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 vote count.
votes: usize = search("points%3A%20{votes}%20");

/// Queries only Discord bots that has this monthly vote count.
pub fn monthly_votes(mut self, new_monthly_votes: usize) -> Self {
self
.search
.push_str(&format!("monthlyPoints%3A%20{new_monthly_votes}%20"));
self
}
/// Queries only Discord bots that has this monthly vote count.
monthly_votes: usize = search("monthlyPoints%3A%20{monthly_votes}%20");

/// Queries only [Top.gg](https://top.gg) certified Discord bots or not.
pub fn certified(mut self, is_certified: bool) -> Self {
self
.search
.push_str(&format!("certifiedBot%3A%20{is_certified}%20"));
self
}
/// Queries only [Top.gg](https://top.gg) certified Discord bots or not.
certified: bool = search("certifiedBot%3A%20{certified}%20");

/// Queries only Discord bots that has this [Top.gg](https://top.gg) vanity URL.
pub fn vanity(mut self, new_vanity: &str) -> Self {
self.search.push_str(&format!(
"vanity%3A%20{}%20",
urlencoding::encode(new_vanity)
));
self
}

pub(crate) fn query_string(mut self) -> String {
if !self.search.is_empty() {
self.query.push_str(&format!("search={}", self.search));
} else {
self.query.pop();
}

self.query
/// Queries only Discord bots that has this [Top.gg](https://top.gg) vanity URL.
vanity: &str = search("vanity%3A%20{}%20", urlencoding::encode(vanity));
}
}

impl Default for Query {
#[inline(always)]
fn default() -> Self {
Self::new()
}
}
impl<'a> IntoFuture for GetBots<'a> {
type Output = crate::Result<Vec<Bot>>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;

impl<S> From<&S> for Query
where
S: AsRef<str> + ?Sized,
{
#[inline(always)]
fn from(other: &S) -> Self {
Self {
search: format!("username%3A%20{}", urlencoding::encode(other.as_ref())),
..Default::default()
fn into_future(self) -> Self::IntoFuture {
let mut query = self.query;

if !self.search.is_empty() {
query.push_str(&format!("search={}", self.search));
} else {
query.pop();
}
}
}

impl From<String> for Query {
#[inline(always)]
fn from(other: String) -> Self {
Self::from(&other)
Box::pin(self.client.get_bots_inner(query))
}
}
Loading

0 comments on commit 49e2f12

Please sign in to comment.