defmodule GitGud.Webhooks.Workers.DeliverWebhook do
@moduledoc """
Posts a signed payload to a webhook URL and records the result.
"""
use Oban.Worker, queue: :webhooks, max_attempts: 5
alias GitGud.Repo
alias GitGud.Webhooks
alias GitGud.Webhooks.Webhook
alias GitGud.Webhooks.WebhookDelivery
def new_job(%Webhook{id: id}, event, payload) do
__MODULE__.new(%{
"webhook_id" => id,
"event" => event,
"payload" => payload
})
end
@impl Oban.Worker
def perform(%Oban.Job{
args: %{"webhook_id" => wid, "event" => event, "payload" => payload},
attempt: attempt
}) do
case Repo.get(Webhook, wid) do
nil ->
:ok
%Webhook{} = hook ->
deliver(hook, event, payload, attempt)
end
end
defp deliver(%Webhook{} = hook, event, payload, attempt) do
body = encode(hook, payload)
sig = Webhooks.sign(hook.secret, body)
headers = [
{"content-type", hook.content_type},
{"user-agent", "gitgud-webhook"},
{"x-gitgud-event", event},
{"x-gitgud-delivery", Ecto.UUID.generate()},
{"x-gitgud-signature-256", sig}
]
started = System.monotonic_time(:millisecond)
result =
Req.post(hook.url,
body: body,
headers: headers,
retry: false,
receive_timeout: 10_000,
connect_options: connect_options(hook)
)
duration = System.monotonic_time(:millisecond) - started
{status, code, resp_body} =
case result do
{:ok, %Req.Response{status: c, body: b}} when c in 200..299 ->
{"success", c, to_iodata(b)}
{:ok, %Req.Response{status: c, body: b}} ->
{"failure", c, to_iodata(b)}
{:error, reason} ->
{"failure", nil, inspect(reason)}
end
%WebhookDelivery{webhook_id: hook.id}
|> WebhookDelivery.changeset(%{
event: event,
status: status,
status_code: code,
request_body: body,
response_body: resp_body,
duration_ms: duration,
attempts: attempt,
delivered_at: DateTime.utc_now(:second)
})
|> Repo.insert!()
if status == "success", do: :ok, else: {:error, "delivery failed: #{code}"}
end
defp encode(%Webhook{content_type: "application/json"}, payload),
do: Jason.encode!(payload)
defp encode(%Webhook{content_type: "application/x-www-form-urlencoded"}, payload) do
URI.encode_query(%{"payload" => Jason.encode!(payload)})
end
defp connect_options(%Webhook{insecure_ssl: true}),
do: [transport_opts: [verify: :verify_none]]
defp connect_options(_), do: []
defp to_iodata(b) when is_binary(b), do: b
defp to_iodata(b), do: inspect(b)
end
2.6 KiB · text
5af55d9