TopggEx.Webhook (topgg_ex v0.1.6)
View SourceTop.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
Using the Functional Listener (Recommended)
# 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 votedtype
- The type of vote ("upvote" or "test")bot
- The ID of the bot that was voted forisWeekend
- Whether the vote was cast on a weekend (counts as 2 votes)query
- Query parameters from the vote page (if any)
Links
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
Functions
@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 connectionauth_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
@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 connectionopts
- 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
@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 connectionexpected_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