Hex.pm Documentation License

A 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)

FunctionDescriptionParameters
new/2Create API clienttoken, options
post_stats/2Update bot statisticsapi, stats
get_stats/1Get bot statisticsapi
get_bot/2Get bot informationapi, bot_id
get_bots/2Search botsapi, query
get_votes/2Get recent votersapi, page
has_voted/2Check user vote statusapi, user_id
is_weekend/1Check weekend multiplierapi

Webhook Handler (TopggEx.Webhook)

FunctionDescriptionParameters
verify_and_parse/2Parse webhook payloadconn, auth_token
listener/2Create functional handlerhandler_fun, opts
Plug behaviorUse as Phoenix/Plug middlewareauthorization, 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

  1. Fork the repository
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. 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.

Acknowledgments

  • Thanks to the Top.gg team for providing the API
  • Built with Finch for modern HTTP performance
  • Inspired by the JavaScript topgg.js library