TopggEx.Webhook (topgg_ex v0.1.6)

View Source

Top.gg Webhook handler for Plug-based HTTP servers.

This module provides webhook handling functionality for receiving vote notifications from Top.gg, compatible with Phoenix and other Plug-based HTTP servers.

Examples

# In a 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

# In a standalone Plug application:
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} ->
          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
    end)
  end

  # Option 2: Define a helper function for reusability
  defp handle_webhook(conn) 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, "Error: #{inspect(reason)}")
    end
  end

  post "/webhook-custom" do
    handle_webhook(conn)
  end

  match _ do
    send_resp(conn, 404, "Not found")
  end
end

Using as Plug Middleware (Alternative)

# 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", YourController, :handle_vote
  end
end

# In your controller:
defmodule YourController do
  use MyAppWeb, :controller

  def handle_vote(conn, _params) do
    case conn.assigns.topgg_payload do
      %{"user" => user_id, "type" => "upvote"} ->
        # Handle the vote
        IO.puts("User #{user_id} voted!")
        send_resp(conn, 204, "")

      %{"user" => user_id, "type" => "test"} ->
        # Handle test webhook
        IO.puts("Test webhook from user #{user_id}")
        send_resp(conn, 204, "")

      _ ->
        send_resp(conn, 400, "Invalid payload")
    end
  end
end

Functional API

You can also use the functional API for more control:

# Verify and parse a webhook request
case TopggEx.Webhook.verify_and_parse(conn, "your_auth_token") do
  {:ok, payload} ->
    # Handle the vote payload
    IO.inspect(payload)

  {:error, :unauthorized} ->
    # Handle unauthorized request
    send_resp(conn, 403, Jason.encode!(%{error: "Unauthorized"}))

  {:error, :invalid_body} ->
    # Handle malformed request
    send_resp(conn, 400, Jason.encode!(%{error: "Invalid body"}))
end

Webhook Data Schema

The webhook payload typically contains:

  • user - The ID of the user who voted
  • type - The type of vote ("upvote" or "test")
  • bot - The ID of the bot that was voted for
  • isWeekend - Whether the vote was cast on a weekend (counts as 2 votes)
  • query - Query parameters from the vote page (if any)

Summary

Functions

Handles a webhook request in standalone Plug applications.

Creates a functional webhook handler that can be used in controllers or other contexts.

Verifies the authorization header and parses the webhook payload.

Types

webhook_options()

@type webhook_options() :: [
  authorization: String.t(),
  error_handler: (any() -> any()),
  assign_key: atom()
]

webhook_payload()

@type webhook_payload() :: %{required(String.t()) => String.t() | boolean() | map()}

Functions

handle_webhook(conn, auth_token \\ nil, handler_fun)

@spec handle_webhook(Plug.Conn.t(), String.t() | nil, (webhook_payload() -> any())) ::
  Plug.Conn.t()

Handles a webhook request in standalone Plug applications.

This is a convenience function for standalone Plug applications that want a simple way to handle webhook requests without defining their own helper functions.

Parameters

  • conn - The Plug connection
  • auth_token - The expected authorization token (optional)
  • handler_fun - Function that takes the payload as argument

Returns

The connection with appropriate response sent.

Examples

# In standalone Plug router
post "/webhook" do
  TopggEx.Webhook.handle_webhook(conn, "my_auth_token", fn 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
  end)
end

listener(handler_fun, opts \\ [])

@spec listener((webhook_payload(), Plug.Conn.t() -> Plug.Conn.t()), webhook_options()) ::
  (Plug.Conn.t(), any() -> Plug.Conn.t())

Creates a functional webhook handler that can be used in controllers or other contexts.

Parameters

  • handler_fun - Function that takes the payload and connection
  • opts - Options including :authorization and :error_handler

Returns

A function that can be used as a Plug or called directly.

Examples

webhook_handler = TopggEx.Webhook.listener(fn payload, conn ->
  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

  # Response is handled automatically
end, authorization: "my_auth_token")

# Use in Phoenix router
post "/webhook", webhook_handler

# Use in standalone Plug router with helper function
defp handle_webhook(conn) do
  case TopggEx.Webhook.verify_and_parse(conn, "my_auth_token") do
    {:ok, payload} ->
      # Handle payload...
      send_resp(conn, 204, "")
    {:error, reason} ->
      send_resp(conn, 400, "Error")
  end
end

post "/webhook" do
  handle_webhook(conn)
end

# Or use verify_and_parse directly inline
post "/webhook" do
  case TopggEx.Webhook.verify_and_parse(conn, "my_auth_token") do
    {:ok, payload} ->
      # Handle payload...
      send_resp(conn, 204, "")
    {:error, reason} ->
      send_resp(conn, 400, "Error")
  end
end

verify_and_parse(conn, expected_auth \\ nil)

@spec verify_and_parse(Plug.Conn.t(), String.t() | nil) ::
  {:ok, webhook_payload()}
  | {:error,
     :unauthorized
     | :invalid_body
     | :malformed_request
     | :invalid_payload_format}
  | {:error, {:missing_fields, [String.t()]}}
  | {:error, {:invalid_field_type, String.t()}}

Verifies the authorization header and parses the webhook payload.

Parameters

  • conn - The Plug connection
  • expected_auth - The expected authorization token (optional)

Returns

  • {:ok, payload} - Successfully parsed and validated webhook payload
  • {:error, :unauthorized} - Authorization header doesn't match
  • {:error, :invalid_body} - Request body is not valid JSON
  • {:error, :malformed_request} - Error reading request body
  • {:error, :invalid_payload_format} - Payload is not a map
  • {:error, {:missing_fields, fields}} - Required fields are missing
  • {:error, {:invalid_field_type, field}} - Field has incorrect type

Examples

case TopggEx.Webhook.verify_and_parse(conn, "my_auth_token") do
  {:ok, payload} ->
    case payload do
      %{"user" => user_id, "type" => "upvote", "bot" => bot_id} ->
        IO.puts("Received vote from user: #{user_id} for bot: #{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, reason} ->
    IO.puts("Webhook error: #{inspect(reason)}")
    send_resp(conn, 400, "Error")
end