Packages

Elixir client SDK for the Surge API

Current section

Files

Jump to
surge_api lib surge webhook_plug.ex
Raw

lib/surge/webhook_plug.ex

if Code.ensure_loaded?(Plug) do
defmodule Surge.WebhookPlug do
@moduledoc """
Helper `Plug` to process webhook events and send them to a custom handler.
## Installation
To handle webhook events, you must first configure your application's endpoint.
Add the following to `endpoint.ex`, **before** `Plug.Parsers` is loaded.
```elixir
plug Surge.WebhookPlug,
at: "/webhook/surge",
handler: MyAppWeb.SurgeHandler,
secret: "whsec_******"
```
If you have not yet added a webhook to your Surge account, you can do so
through the Surge dashboard. Use the route you configured in the endpoint
above and copy the webhook secret into your app's configuration.
### Supported options
- `at`: The URL path your application should listen for Surge webhooks on.
Configure this to match whatever you set in the webhook.
- `handler`: Custom event handler module that accepts `Surge.Events.Event` structs
and processes them within your application. You must create this module.
- `secret`: Webhook secret starting with `whsec_` obtained from Surge.
This can also be a function or a tuple for runtime configuration.
- `tolerance`: Maximum age (in seconds) allowed for the webhook event.
Default is 300 seconds (5 minutes). See `Surge.Webhook.construct_event/4` for more information.
## Handling events
You will need to create a custom event handler module to handle events.
Your event handler module should implement the `Surge.WebhookHandler`
behavior, defining a `handle_event/1` function which takes a `Surge.Events.Event`
struct and returns either `{:ok, term}` or `:ok`. This will mark the event as
successfully processed. Alternatively handler can signal an error by returning
`:error` or `{:error, reason}` tuple, where reason is an atom or a string.
HTTP status code 400 will be used for errors.
### Example
```elixir
# lib/myapp_web/surge_handler.ex
defmodule MyAppWeb.SurgeHandler do
@behaviour Surge.WebhookHandler
@impl true
def handle_event(%Surge.Events.Event{type: :message_received} = event) do
# TODO: handle incoming message
IO.inspect(event.data.body, label: "Received")
:ok
end
@impl true
def handle_event(%Surge.Events.Event{type: :message_failed} = event) do
# TODO: handle failed message
{:error, :retry_later}
end
# Return HTTP 200 for unhandled events
@impl true
def handle_event(_event), do: :ok
end
```
### Runtime configuration
If you're loading config dynamically at runtime (eg with `runtime.exs`
or an OTP app) you must pass a tuple or function as the secret.
```elixir
# With a tuple
plug Surge.WebhookPlug,
at: "/webhook/surge",
handler: MyAppWeb.SurgeHandler,
secret: {Application, :get_env, [:myapp, :surge_webhook_secret]}
# Or, with a function
plug Surge.WebhookPlug,
at: "/webhook/surge",
handler: MyAppWeb.SurgeHandler,
secret: fn -> Application.get_env(:myapp, :surge_webhook_secret) end
```
"""
import Plug.Conn
alias Plug.Conn
alias Surge.Events.Event
@behaviour Plug
@impl true
def init(opts) do
path_info = String.split(opts[:at], "/", trim: true)
opts
|> Enum.into(%{})
|> Map.put_new(:path_info, path_info)
end
@impl true
def call(
%Conn{method: "POST", path_info: path_info} = conn,
%{
path_info: path_info,
secret: secret,
handler: handler
} = opts
) do
secret = parse_secret!(secret)
with [signature] <- get_req_header(conn, "surge-signature"),
{:ok, payload, conn} <- Conn.read_body(conn),
{:ok, %Event{} = event} <- construct_event(payload, signature, secret, opts),
:ok <- handle_event!(handler, event) do
send_resp(conn, 200, "Webhook received.") |> halt()
else
{:handle_error, reason} -> send_resp(conn, 400, reason) |> halt()
_ -> send_resp(conn, 400, "Bad request.") |> halt()
end
end
@impl true
def call(%Conn{path_info: path_info} = conn, %{path_info: path_info}) do
send_resp(conn, 400, "Bad request.") |> halt()
end
@impl true
def call(conn, _), do: conn
defp construct_event(payload, signature, secret, %{tolerance: tolerance}) do
Surge.Webhook.construct_event(payload, signature, secret, tolerance: tolerance)
end
defp construct_event(payload, signature, secret, _opts) do
Surge.Webhook.construct_event(payload, signature, secret)
end
defp handle_event!(handler, %Event{} = event) do
case handler.handle_event(event) do
{:ok, _} ->
:ok
:ok ->
:ok
{:error, reason} when is_binary(reason) ->
{:handle_error, reason}
{:error, reason} when is_atom(reason) ->
{:handle_error, Atom.to_string(reason)}
:error ->
{:handle_error, ""}
resp ->
raise """
#{inspect(handler)}.handle_event/1 returned an invalid response. Expected {:ok, term}, :ok, {:error, reason} or :error
Got: #{inspect(resp)}
Event data: #{inspect(event)}
"""
end
end
defp parse_secret!({m, f, a}), do: apply(m, f, a)
defp parse_secret!(fun) when is_function(fun), do: fun.()
defp parse_secret!(secret) when is_binary(secret), do: secret
defp parse_secret!(secret) do
raise """
The Surge webhook secret is invalid. Expected a string, tuple, or function.
Got: #{inspect(secret)}
If you're setting the secret at runtime, you need to pass a tuple or function.
For example:
plug Surge.WebhookPlug,
at: "/webhook/surge",
handler: MyAppWeb.SurgeHandler,
secret: {Application, :get_env, [:myapp, :surge_webhook_secret]}
"""
end
end
end