TopStats.gg

Rust

Community maintained Rust client for the topstats.gg API.

A type-safe, ergonomic Rust client for the Topstats.gg API with async and blocking support.

Add the dependency to your Cargo.toml:

[dependencies]
topstats = "0.1"
tokio = { version = "1", features = ["full"] }

Initialize the Client

use topstats::Client;

let client = Client::new("YOUR_TOKEN")?;

Make Your First Request

// Get information about a bot
let bot = client.get_bot("583807014896140293").await?;
println!("{} has {:?} servers", bot.name, bot.server_count);

Usage Examples

use topstats::Client;

let client = Client::new("YOUR_TOKEN")?;

// Get detailed bot information
let bot = client.get_bot("583807014896140293").await?;
println!("{} has {:?} servers", bot.name, bot.server_count);
use topstats::{Client, TimeFrame, DataType};

let client = Client::new("YOUR_TOKEN")?;

// Get historical statistics
let history = client
    .get_bot_historical("583807014896140293", TimeFrame::ThirtyDays, DataType::MonthlyVotes)
    .await?;

for point in &history.data {
    println!("{}: {:?}", point.time, point.monthly_votes);
}
use topstats::{Client, RankingsQuery, SortBy};

let client = Client::new("YOUR_TOKEN")?;

// Get top bots
let rankings = client
    .get_rankings(
        RankingsQuery::default()
            .sort_by(SortBy::MonthlyVotes)
            .limit(10),
    )
    .await?;

for bot in &rankings.data {
    println!("#{}: {} ({} votes)", bot.monthly_votes_rank, bot.name, bot.monthly_votes);
}

To use blocking (synchronous) mode, update your Cargo.toml:

[dependencies]
topstats = { version = "0.1", default-features = false, features = ["blocking", "ureq-client"] }
use topstats::{Client, RankingsQuery, SortBy};

fn main() -> Result<(), topstats::Error> {
    let client = Client::new("YOUR_TOKEN")?;

    // No .await needed
    let bot = client.get_bot("583807014896140293")?;
    println!("{} has {:?} servers", bot.name, bot.server_count);

    let rankings = client
        .get_rankings(RankingsQuery::default().sort_by(SortBy::MonthlyVotes).limit(5))?;

    for bot in &rankings.data {
        println!("{}: {} votes", bot.name, bot.monthly_votes);
    }

    Ok(())
}

API Reference

Methods

async fn get_bot(&self, bot_id: &str) -> Result<Bot>

// Example
let bot = client.get_bot("583807014896140293").await?;
async fn get_bot_historical(
    &self,
    bot_id: &str,
    time_frame: TimeFrame,
    data_type: DataType,
) -> Result<HistoricalDataResponse>

// Available time frames
TimeFrame::AllTime     // "alltime"
TimeFrame::FiveYears   // "5y"
TimeFrame::ThreeYears  // "3y"
TimeFrame::OneYear     // "1y"
TimeFrame::NineMonths  // "270d"
TimeFrame::SixMonths   // "180d"
TimeFrame::NinetyDays  // "90d"
TimeFrame::ThirtyDays  // "30d"
TimeFrame::SevenDays   // "7d"
TimeFrame::ThreeDays   // "3d"
TimeFrame::OneDay      // "1d"
TimeFrame::TwelveHours // "12h"
TimeFrame::SixHours    // "6h"

// Available data types
DataType::MonthlyVotes // "monthly_votes"
DataType::TotalVotes   // "total_votes"
DataType::ServerCount  // "server_count"
DataType::ReviewCount  // "review_count"
async fn get_bot_recent(&self, bot_id: &str) -> Result<RecentDataResponse>

// Example
let recent = client.get_bot_recent("583807014896140293").await?;

if let Some(latest) = recent.latest_hourly() {
    println!("Latest hourly votes: {}", latest.monthly_votes);
}
async fn get_rankings(&self, query: RankingsQuery) -> Result<RankingsResponse>

// Example with builder pattern
let rankings = client
    .get_rankings(
        RankingsQuery::default()
            .sort_by(SortBy::MonthlyVotes)
            .sort_order(SortOrder::Descending)
            .limit(250) // 1-500, defaults to 100
            .offset(0),
    )
    .await?;
async fn search_bots(
    &self,
    query: &str,
    limit: Option<u32>,
    offset: Option<u32>,
    include_deleted: Option<bool>,
) -> Result<Vec<Bot>>

// Search by name
let results = client.search_bots("music", Some(10), None, None).await?;

// Search by tag
let tagged = client.search_by_tag("moderation", Some(10), None, None).await?;
async fn compare_bots(&self, bot_ids: &[&str]) -> Result<Vec<RankedBot>>

async fn compare_bots_historical(
    &self,
    bot_ids: &[&str],
    time_frame: TimeFrame,
    data_type: DataType,
) -> Result<CompareHistoricalResponse>

// Compare current stats
let comparison = client
    .compare_bots(&["432610292342587392", "646937666251915264"])
    .await?;

// Compare historical data
let history = client
    .compare_bots_historical(
        &["432610292342587392", "646937666251915264"],
        TimeFrame::ThirtyDays,
        DataType::ServerCount,
    )
    .await?;
async fn get_user_bots(&self, user_id: &str) -> Result<UserBotsResponse>

// Example
let user_bots = client.get_user_bots("205680187394752512").await?;
println!("User has {} bots", user_bots.count());
println!("Total monthly votes: {}", user_bots.total_monthly_votes());

Response Types

pub struct Bot {
    pub id: String,
    pub name: String,
    pub server_count: Option<i64>,
    pub monthly_votes: i64,
    pub total_votes: i64,
    pub short_description: String,
    pub tags: Vec<String>,
    pub owners: Vec<String>,
    pub deleted: bool,
    pub timestamp: DateTime<Utc>,
    pub percentage_changes: Option<PercentageChanges>,
    // ... and more
}
pub struct HistoricalDataResponse {
    pub data: Vec<HistoricalDataPoint>,
}

pub struct HistoricalDataPoint {
    pub time: DateTime<Utc>,
    pub id: String,
    pub monthly_votes: Option<i64>,
    pub total_votes: Option<i64>,
    pub server_count: Option<i64>,
    pub review_count: Option<i64>,
}
pub struct RecentDataResponse {
    pub hourly_data: Vec<RecentDataPoint>,
    pub daily_data: Vec<RecentDataPoint>,
}

impl RecentDataResponse {
    pub fn latest_hourly(&self) -> Option<&RecentDataPoint>;
    pub fn latest_daily(&self) -> Option<&RecentDataPoint>;
    pub fn total_hourly_votes_change(&self) -> i64;
    pub fn total_daily_votes_change(&self) -> i64;
}
pub struct RankingsResponse {
    pub total_bot_count: i64,
    pub data: Vec<RankedBot>,
}

pub struct RankedBot {
    pub id: String,
    pub name: String,
    pub monthly_votes: i64,
    pub monthly_votes_rank: i64,
    pub server_count: Option<i64>,
    pub server_count_rank: Option<i64>,
    pub total_votes: i64,
    pub total_votes_rank: i64,
    // ... and more
}

Feature Flags

The SDK uses Cargo feature flags to control async/blocking mode and the HTTP backend.

FeatureDefaultDescription
asyncYesEnable async mode
blockingNoEnable blocking (synchronous) mode
reqwest-clientYesUse reqwest as the HTTP backend
ureq-clientNoUse ureq as the HTTP backend (for blocking mode)
rustls-tlsYesUse rustls for TLS
native-tlsNoUse the platform's native TLS
tracingNoEnable tracing crate integration

The async and blocking features are mutually exclusive. Enabling both will result in a compile error.

Error Handling

use topstats::Error;

match client.get_bot("invalid-id").await {
    Ok(bot) => println!("Found: {}", bot.name),
    Err(Error::NotFound { message }) => {
        println!("Bot not found: {message}");
    }
    Err(Error::RateLimited { retry_after, .. }) => {
        println!("Rate limited, retry after {retry_after}s");
    }
    Err(e) => eprintln!("Error: {e}"),
}
use topstats::Error;

let result = client.get_bot("583807014896140293").await;

match result {
    Ok(bot) => println!("Found: {}", bot.name),
    Err(ref e) if e.is_rate_limited() => {
        if let Some(delay) = e.retry_after() {
            println!("Rate limited, retry after {delay}s");
        }
    }
    Err(Error::NotFound { message }) => {
        println!("Not found: {message}");
    }
    Err(Error::Forbidden { message }) => {
        println!("Forbidden: {message}");
    }
    Err(Error::InvalidBotId(id)) => {
        println!("Invalid bot ID: {id}");
    }
    Err(Error::Network(msg)) => {
        println!("Network error: {msg}");
    }
    Err(e) => eprintln!("Unexpected error: {e}"),
}

The client automatically retries rate-limited requests up to 3 times by default. You can configure this via the ClientBuilder.

Rate Limits

The API implements rate limiting to ensure fair usage. The Rust SDK handles rate limits automatically by retrying requests when a 429 response is received. You can configure retry behavior using the ClientBuilder:

let client = Client::builder()
    .token("YOUR_TOKEN")
    .auto_retry(true)          // enabled by default
    .max_retries(3)            // default: 3
    .max_delay_threshold(10.0) // max seconds to wait before retrying
    .build()?;

For detailed information about rate limits, see the rate limit documentation.

GitHubtop-stats/rust-sdk

1

On this page