TopggEx
View SourceA community Elixir SDK for the Top.gg API, allowing you to interact with Discord bot statistics, user votes, and bot information.
Features
- ๐ Complete API Coverage: All Top.gg API endpoints supported
- ๐ฏ Webhook Support: Built-in webhook handler for vote notifications
- ๐ Type Safety: Full typespecs and structured data
- โก HTTP/2 Support: Built on Finch for modern HTTP performance
- ๐งช Well Tested: Comprehensive test suite with 95%+ coverage
- ๐ Excellent Documentation: Detailed docs with examples
- ๐๏ธ Clean Architecture: Separated HTTP client for maintainability
Installation
Add topgg_ex
to your list of dependencies in mix.exs
:
def deps do
[
{:topgg_ex, "~> 0.1.0"},
{:finch, "~> 0.19"} # Required HTTP client
]
end
Then run:
mix deps.get
Quick Start
1. Setup
First, add Finch to your application's supervision tree:
# In your application.ex
children = [
{Finch, name: :topgg_finch}
]
2. Create API Client
# Get your token from https://top.gg/api/docs#mybots
{:ok, api} = TopggEx.Api.new("your_topgg_token_here")
3. Post Bot Statistics
# Update your bot's server count
{:ok, stats} = TopggEx.Api.post_stats(api, %{server_count: 1250})
Usage Examples
Bot Statistics
# Post bot stats
{:ok, _} = TopggEx.Api.post_stats(api, %{
server_count: 1250,
shard_count: 2,
shards: [625, 625]
})
# Get your bot's current stats
{:ok, stats} = TopggEx.Api.get_stats(api)
# => %{server_count: 1250, shard_count: 2, shards: [625, 625]}
Bot Information
# Get information about any bot
{:ok, bot} = TopggEx.Api.get_bot(api, "461521980492087297")
# => %{"id" => "461521980492087297", "username" => "Shiro", ...}
# Search for bots
{:ok, results} = TopggEx.Api.get_bots(api, %{
search: %{username: "music"},
limit: 10,
fields: ["id", "username", "short_description"]
})
Vote Checking
# Check if a user has voted
{:ok, has_voted?} = TopggEx.Api.has_voted(api, "205680187394752512")
# => true or false
# Get recent voters
{:ok, voters} = TopggEx.Api.get_votes(api)
# => [%{"username" => "Example", "id" => "123...", "avatar" => "https://..."}, ...]
# Check weekend multiplier status
{:ok, is_weekend?} = TopggEx.Api.is_weekend(api)
# => true or false
Webhook Handling
TopggEx includes a built-in webhook handler for receiving vote notifications from Top.gg.
Using the Functional Listener (Recommended)
The easiest way to handle webhooks is using the listener
function:
# In your Phoenix router
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# Create the webhook handler
webhook_handler = TopggEx.Webhook.listener(fn payload, conn ->
case payload do
%{"user" => user_id, "type" => "upvote", "bot" => bot_id} ->
# Handle the vote
MyApp.handle_user_vote(user_id, bot_id)
IO.puts("User #{user_id} voted for bot #{bot_id}!")
%{"user" => user_id, "type" => "test"} ->
# Handle test webhook
IO.puts("Test webhook from user: #{user_id}")
end
# Response is handled automatically by the listener
end, authorization: "your_webhook_auth_token")
scope "/webhooks" do
pipe_through :api
post "/topgg", webhook_handler
end
end
For standalone Plug applications:
# Define your router module
defmodule MyApp.Router do
use Plug.Router
plug :match
plug :dispatch
# Option 1: Use the convenient handle_webhook function (Recommended)
post "/webhook" do
TopggEx.Webhook.handle_webhook(conn, "your_webhook_auth_token", fn payload ->
case payload do
%{"user" => user_id, "type" => "upvote", "bot" => bot_id} ->
# Handle the vote
MyApp.handle_user_vote(user_id, bot_id)
IO.puts("User #{user_id} voted for bot #{bot_id}!")
%{"user" => user_id, "type" => "test"} ->
# Handle test webhook
IO.puts("Test webhook from user: #{user_id}")
end
end)
end
# Option 2: Use verify_and_parse directly (more explicit)
post "/webhook-direct" do
case TopggEx.Webhook.verify_and_parse(conn, "your_webhook_auth_token") do
{:ok, payload} ->
case payload do
%{"user" => user_id, "type" => "upvote", "bot" => bot_id} ->
MyApp.handle_user_vote(user_id, bot_id)
IO.puts("User #{user_id} voted for bot #{bot_id}!")
%{"user" => user_id, "type" => "test"} ->
IO.puts("Test webhook from user: #{user_id}")
end
send_resp(conn, 204, "")
{:error, reason} ->
send_resp(conn, 400, "Webhook error: #{inspect(reason)}")
end
end
match _ do
send_resp(conn, 404, "Not found")
end
end
# Start your application
{:ok, _} = Plug.Cowboy.http(MyApp.Router, [])
Using as Plug Middleware (Alternative)
You can also use the webhook handler as Plug middleware:
# In your Phoenix router
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :webhook do
plug :accepts, ["json"]
plug TopggEx.Webhook, authorization: "your_webhook_auth_token"
end
scope "/webhooks" do
pipe_through :webhook
post "/topgg", MyAppWeb.WebhookController, :handle_vote
end
end
# In your controller
defmodule MyAppWeb.WebhookController do
use MyAppWeb, :controller
def handle_vote(conn, _params) do
case conn.assigns.topgg_payload do
%{"user" => user_id, "type" => "upvote", "bot" => bot_id} ->
# Handle the vote
MyApp.handle_user_vote(user_id, bot_id)
send_resp(conn, 204, "")
%{"user" => user_id, "type" => "test"} ->
# Handle test webhook
IO.puts("Test webhook from user: #{user_id}")
send_resp(conn, 204, "")
end
end
end
Manual Webhook Processing
For maximum control, you can use the functional API to manually parse webhooks:
def handle_webhook(conn) do
case TopggEx.Webhook.verify_and_parse(conn, "your_auth_token") do
{:ok, payload} ->
case payload do
%{"user" => user_id, "type" => "upvote", "bot" => bot_id} ->
MyApp.process_vote(user_id, bot_id)
send_resp(conn, 204, "")
%{"user" => user_id, "type" => "test"} ->
IO.puts("Test webhook from user: #{user_id}")
send_resp(conn, 204, "")
end
{:error, :unauthorized} ->
send_resp(conn, 403, Jason.encode!(%{error: "Unauthorized"}))
{:error, :invalid_body} ->
send_resp(conn, 400, Jason.encode!(%{error: "Invalid body"}))
{:error, reason} ->
send_resp(conn, 400, Jason.encode!(%{error: "Webhook error: #{inspect(reason)}"}))
end
end
Advanced Listener Usage
You can create more sophisticated webhook handlers with error handling and custom logic:
webhook_handler = TopggEx.Webhook.listener(fn payload, conn ->
%{"user" => user_id, "type" => vote_type, "bot" => bot_id} = payload
case vote_type do
"upvote" ->
# Record the vote and handle weekend multiplier
is_weekend = Map.get(payload, "isWeekend", false)
vote_count = if is_weekend, do: 2, else: 1
MyApp.record_vote(user_id, bot_id, vote_count)
MyApp.send_thank_you(user_id)
IO.puts("User #{user_id} voted! (#{vote_count} votes)")
"test" ->
IO.puts("Test webhook received from user #{user_id}!")
end
# Response is handled automatically by the listener
# No need to call send_resp/3
end, authorization: "your_webhook_auth_token")
# Use in router
post "/webhook", webhook_handler
Advanced Usage
# Custom Finch instance
{:ok, api} = TopggEx.Api.new("your_token", %{
finch_name: :my_custom_finch,
base_url: "https://top.gg/api" # Optional custom base URL
})
# Complex bot search
{:ok, results} = TopggEx.Api.get_bots(api, %{
search: %{
username: "music bot",
tags: "music"
},
sort: "server_count",
limit: 50,
fields: ["id", "username", "short_description", "server_count"]
})
API Reference
Core Functions
API Client (TopggEx.Api
)
Function | Description | Parameters |
---|---|---|
new/2 | Create API client | token , options |
post_stats/2 | Update bot statistics | api , stats |
get_stats/1 | Get bot statistics | api |
get_bot/2 | Get bot information | api , bot_id |
get_bots/2 | Search bots | api , query |
get_votes/2 | Get recent voters | api , page |
has_voted/2 | Check user vote status | api , user_id |
is_weekend/1 | Check weekend multiplier | api |
Webhook Handler (TopggEx.Webhook
)
Function | Description | Parameters |
---|---|---|
verify_and_parse/2 | Parse webhook payload | conn , auth_token |
listener/2 | Create functional handler | handler_fun , opts |
Plug behavior | Use as Phoenix/Plug middleware | authorization , opts |
Error Handling
All functions return {:ok, result}
on success or {:error, reason}
on failure:
case TopggEx.Api.post_stats(api, %{server_count: 100}) do
{:ok, stats} ->
IO.puts("Stats updated successfully!")
{:error, %{status: 401}} ->
IO.puts("Invalid API token")
{:error, %{status: 429}} ->
IO.puts("Rate limited - try again later")
{:error, reason} ->
IO.puts("Network error: #{inspect(reason)}")
end
Configuration
Environment Variables
You can set your Top.gg token via environment variables:
# config/runtime.exs
config :my_app, :topgg_token, System.get_env("TOPGG_TOKEN")
# In your application
token = Application.get_env(:my_app, :topgg_token)
{:ok, api} = TopggEx.Api.new(token)
Custom HTTP Client Options
{:ok, api} = TopggEx.Api.new("your_token", %{
finch_name: :my_finch, # Custom Finch instance name
base_url: "https://top.gg/api" # Custom API base URL
})
Rate Limiting
Top.gg API has rate limits. The library will return appropriate errors:
- 429 Too Many Requests: You've hit the rate limit
- 403 Forbidden: Invalid token or insufficient permissions
Implement exponential backoff for production applications:
defmodule MyBot.Stats do
def update_stats_with_retry(api, stats, retries \\ 3) do
case TopggEx.Api.post_stats(api, stats) do
{:ok, result} -> {:ok, result}
{:error, %{status: 429}} when retries > 0 ->
Process.sleep(1000 * (4 - retries)) # Exponential backoff
update_stats_with_retry(api, stats, retries - 1)
error -> error
end
end
end
Testing
The library includes comprehensive tests. Run them with:
mix test
For testing your own applications, you can mock the HTTP client:
# In your tests
setup do
bypass = Bypass.open()
{:ok, api} = TopggEx.Api.new("test_token", %{
base_url: "http://localhost:#{bypass.port}/api"
})
{:ok, %{bypass: bypass, api: api}}
end
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Development
# Get dependencies
mix deps.get
# Run tests
mix test
# Generate documentation
mix docs
# Check formatting
mix format --check-formatted
# Run static analysis
mix dialyzer
License
This project is licensed under the MIT License - see the LICENSE file for details.