defmodule GitGud.Webhooks do
@moduledoc """
Outbound webhook notifications.
Every domain event that listeners care about goes through `notify/3`,
which fans out to every enabled webhook on the repository that
subscribes to the event. Each (webhook, payload) pair becomes an Oban
job that signs + delivers.
Payloads use the GitHub-style shape (`{"action": ..., "repository":
{...}, …}`) so existing tooling slots in without translation.
"""
import Ecto.Query, warn: false
alias GitGud.Organizations.Organization
alias GitGud.Repo
alias GitGud.Repositories.Repository
alias GitGud.Webhooks.Webhook
alias GitGud.Webhooks.WebhookDelivery
alias GitGud.Webhooks.Workers.DeliverWebhook
def list_webhooks(%Repository{id: rid}) do
Webhook
|> where([w], w.repository_id == ^rid)
|> order_by([w], asc: w.inserted_at)
|> Repo.all()
end
def list_webhooks(%Organization{id: oid}) do
Webhook
|> where([w], w.organization_id == ^oid)
|> order_by([w], asc: w.inserted_at)
|> Repo.all()
end
def get_webhook!(id), do: Repo.get!(Webhook, id)
def create_webhook(%Repository{id: rid}, attrs) do
%Webhook{repository_id: rid}
|> Webhook.changeset(attrs)
|> Repo.insert()
end
def create_webhook(%Organization{id: oid}, attrs) do
%Webhook{organization_id: oid}
|> Webhook.changeset(attrs)
|> Repo.insert()
end
def update_webhook(%Webhook{} = hook, attrs) do
hook
|> Webhook.changeset(attrs)
|> Repo.update()
end
def delete_webhook(%Webhook{} = hook), do: Repo.delete(hook)
def list_deliveries(%Webhook{id: wid}, opts \\ []) do
limit = Keyword.get(opts, :limit, 50)
WebhookDelivery
|> where([d], d.webhook_id == ^wid)
|> order_by([d], desc: d.inserted_at)
|> limit(^limit)
|> Repo.all()
end
@doc """
Most-recent deliveries for a set of webhooks, grouped by webhook id.
Used to render an inline delivery log next to each hook on the
settings pages. `per` caps the rows shown per webhook.
"""
def recent_deliveries_by_hook(hooks, per \\ 5) when is_list(hooks) do
ids = Enum.map(hooks, & &1.id)
if ids == [] do
%{}
else
WebhookDelivery
|> where([d], d.webhook_id in ^ids)
|> order_by([d], desc: d.inserted_at)
|> Repo.all()
|> Enum.group_by(& &1.webhook_id)
|> Map.new(fn {wid, ds} -> {wid, Enum.take(ds, per)} end)
end
end
@doc """
Enqueue deliveries for every webhook that subscribes to `event` on
`repo` — both the repo's own hooks and the hooks of the org that owns
it. Returns `:ok` regardless; failures are visible via the
`webhook_deliveries` table.
"""
def notify(%Repository{} = repo, event, payload) when is_binary(event) and is_map(payload) do
repo
|> hooks_for(event)
|> Enum.each(fn hook ->
{:ok, _} = DeliverWebhook.new_job(hook, event, payload) |> Oban.insert()
end)
:ok
end
# Enabled hooks subscribed to `event` from both the repo and (if the
# repo is org-owned) the owning org.
defp hooks_for(%Repository{id: rid, organization_id: oid}, event) do
owner_clause =
if oid do
dynamic([w], w.repository_id == ^rid or w.organization_id == ^oid)
else
dynamic([w], w.repository_id == ^rid)
end
Webhook
|> where([w], w.enabled == true)
|> where(^owner_clause)
|> Repo.all()
|> Enum.filter(&Webhook.listens_to?(&1, event))
end
@doc "HMAC-SHA256 hex signature of `body` using the webhook's secret."
def sign(secret, body) when is_binary(secret) and is_binary(body) do
"sha256=" <>
(:crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16(case: :lower))
end
end
3.6 KiB · text
5af55d9