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.
Crates.io
View the crate page for TopStats' Rust SDK
docs.rs Documentation
Read the full API documentation on docs.rs
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.
| Feature | Default | Description |
|---|---|---|
async | Yes | Enable async mode |
blocking | No | Enable blocking (synchronous) mode |
reqwest-client | Yes | Use reqwest as the HTTP backend |
ureq-client | No | Use ureq as the HTTP backend (for blocking mode) |
rustls-tls | Yes | Use rustls for TLS |
native-tls | No | Use the platform's native TLS |
tracing | No | Enable 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.
top-stats/rust-sdk
1