diff --git a/config/config.exs b/config/config.exs
index e69d35f301..7cc6adde34 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -871,7 +871,8 @@
config :pleroma, ConcurrentLimiter, [
{Pleroma.Web.RichMedia.Helpers, [max_running: 5, max_waiting: 5]},
- {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}
+ {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]},
+ {Pleroma.Webhook.Notify, [max_running: 5, max_waiting: :infinity]}
]
import_config "soapbox.exs"
diff --git a/config/description.exs b/config/description.exs
index cc5a908167..26483ff0e7 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -3460,6 +3460,26 @@
suggestion: [5]
}
]
+ },
+ %{
+ key: Pleroma.Webhook.Notify,
+ type: :keyword,
+ description: "Concurrent limits configuration for webhooks.",
+ suggestions: [max_running: 5, max_waiting: 5],
+ children: [
+ %{
+ key: :max_running,
+ type: :integer,
+ description: "Max running concurrently jobs.",
+ suggestion: [5]
+ },
+ %{
+ key: :max_waiting,
+ type: :integer,
+ description: "Max waiting jobs.",
+ suggestion: [5]
+ }
+ ]
}
]
}
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index d808bc732b..e0be9f69c8 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -308,7 +308,11 @@ defp http_children(_, _), do: []
def limiters_setup do
config = Config.get(ConcurrentLimiter, [])
- [Pleroma.Web.RichMedia.Helpers, Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
+ [
+ Pleroma.Web.RichMedia.Helpers,
+ Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy,
+ Pleroma.Webhook.Notify
+ ]
|> Enum.each(fn module ->
mod_config = Keyword.get(config, module, [])
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index d3263b8378..0f29ac8a59 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.User do
import Ecto.Changeset
import Ecto.Query
import Ecto, only: [assoc: 2]
+ import Pleroma.Webhook.Notify, only: [trigger_webhooks: 2]
alias Ecto.Multi
alias Pleroma.Activity
@@ -36,6 +37,7 @@ defmodule Pleroma.User do
alias Pleroma.Web.Endpoint
alias Pleroma.Web.OAuth
alias Pleroma.Web.RelMe
+ alias Pleroma.Webhook
alias Pleroma.Workers.BackgroundWorker
require Logger
@@ -860,6 +862,7 @@ defp autofollowing_users(user) do
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset) do
post_register_action(user)
+ trigger_webhooks(user, :"account.created")
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 27581bf1fd..963818327b 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -24,6 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger
+ alias Pleroma.Webhook
alias Pleroma.Workers.BackgroundWorker
alias Pleroma.Workers.PollWorker
@@ -31,6 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
import Pleroma.Web.ActivityPub.Visibility
+ import Pleroma.Webhook.Notify, only: [trigger_webhooks: 2]
require Logger
require Pleroma.Constants
@@ -391,6 +393,7 @@ defp do_flag(
{:ok, activity} <- insert(flag_data, local),
{:ok, stripped_activity} <- strip_report_status_data(activity),
_ <- notify_and_stream(activity),
+ _ <- trigger_webhooks(activity, :"report.created"),
:ok <-
maybe_federate(stripped_activity) do
User.all_superusers()
diff --git a/lib/pleroma/web/admin_api/controllers/webhook_controller.ex b/lib/pleroma/web/admin_api/controllers/webhook_controller.ex
new file mode 100644
index 0000000000..054905cb4d
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/webhook_controller.ex
@@ -0,0 +1,91 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.WebhookController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Repo
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Webhook
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["admin:write"]}
+ when action in [:update, :create, :enable, :disable, :rotate_secret]
+ )
+
+ plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action in [:index, :show])
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.WebhookOperation
+
+ def index(conn, _) do
+ webhooks =
+ Webhook
+ |> Repo.all()
+
+ render(conn, "index.json", webhooks: webhooks)
+ end
+
+ def show(conn, %{id: id}) do
+ with %Webhook{} = webhook <- Webhook.get(id) do
+ render(conn, "show.json", webhook: webhook)
+ else
+ nil -> {:error, :not_found}
+ end
+ end
+
+ def create(%{body_params: params} = conn, _) do
+ with {:ok, webhook} <- Webhook.create(params) do
+ render(conn, "show.json", webhook: webhook)
+ # else
+ # nil -> {:error, :not_found}
+ end
+ end
+
+ def update(%{body_params: params} = conn, %{id: id}) do
+ with %Webhook{} = webhook <- Webhook.get(id),
+ changeset <- Webhook.update(webhook, params),
+ {:ok, webhook} <- Repo.update(changeset) do
+ render(conn, "show.json", webhook: webhook)
+ end
+ end
+
+ def delete(conn, %{id: id}) do
+ with %Webhook{} = webhook <- Webhook.get(id),
+ {:ok, webhook} <- Webhook.delete(webhook) do
+ render(conn, "show.json", webhook: webhook)
+ end
+ end
+
+ def enable(conn, %{id: id}) do
+ with %Webhook{} = webhook <- Webhook.get(id),
+ {:ok, webhook} <- Webhook.set_enabled(webhook, true) do
+ render(conn, "show.json", webhook: webhook)
+ else
+ nil -> {:error, :not_found}
+ end
+ end
+
+ def disable(conn, %{id: id}) do
+ with %Webhook{} = webhook <- Webhook.get(id),
+ {:ok, webhook} <- Webhook.set_enabled(webhook, false) do
+ render(conn, "show.json", webhook: webhook)
+ else
+ nil -> {:error, :not_found}
+ end
+ end
+
+ def rotate_secret(conn, %{id: id}) do
+ with %Webhook{} = webhook <- Webhook.get(id),
+ {:ok, webhook} <- Webhook.rotate_secret(webhook) do
+ render(conn, "show.json", webhook: webhook)
+ else
+ nil -> {:error, :not_found}
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/views/webhook_view.ex b/lib/pleroma/web/admin_api/views/webhook_view.ex
new file mode 100644
index 0000000000..725183029d
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/webhook_view.ex
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.WebhookView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.Web.CommonAPI.Utils
+
+ def render("index.json", %{webhooks: webhooks}) do
+ render_many(webhooks, __MODULE__, "show.json")
+ end
+
+ def render("show.json", %{webhook: webhook}) do
+ %{
+ id: webhook.id |> to_string(),
+ url: webhook.url,
+ events: webhook.events,
+ secret: webhook.secret,
+ enabled: webhook.enabled,
+ created_at: Utils.to_masto_date(webhook.inserted_at),
+ updated_at: Utils.to_masto_date(webhook.updated_at)
+ }
+ end
+
+ def render("event.json", %{type: type, object: object}) do
+ %{
+ type: type,
+ created_at: Utils.to_masto_date(NaiveDateTime.utc_now()),
+ object: object
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex
new file mode 100644
index 0000000000..0c4e1797fb
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex
@@ -0,0 +1,193 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Webhooks"],
+ summary: "Retrieve a list of webhooks",
+ operationId: "AdminAPI.WebhookController.index",
+ security: [%{"oAuth" => ["admin:show"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Array of webhooks", "application/json", %Schema{
+ type: :array,
+ items: webhook()
+ })
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Webhooks"],
+ summary: "Retrieve a webhook",
+ operationId: "AdminAPI.WebhookController.show",
+ security: [%{"oAuth" => ["admin:show"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 => Operation.response("Webhook", "application/json", webhook())
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Webhooks"],
+ summary: "Create a webhook",
+ operationId: "AdminAPI.WebhookController.create",
+ security: [%{"oAuth" => ["admin:write"]}],
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ description: "POST body for creating a webhook",
+ type: :object,
+ properties: %{
+ url: %Schema{type: :string, format: :uri, required: true},
+ events: event_type(true),
+ enabled: %Schema{type: :boolean}
+ }
+ }
+ ),
+ responses: %{
+ 200 => Operation.response("Webhook", "application/json", webhook())
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Webhooks"],
+ summary: "Update a webhook",
+ operationId: "AdminAPI.WebhookController.update",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [id_param()],
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ description: "POST body for updating a webhook",
+ type: :object,
+ properties: %{
+ url: %Schema{type: :string, format: :uri},
+ events: event_type(),
+ enabled: %Schema{type: :boolean}
+ }
+ }
+ ),
+ responses: %{
+ 200 => Operation.response("Webhook", "application/json", webhook())
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Webhooks"],
+ summary: "Delete a webhook",
+ operationId: "AdminAPI.WebhookController.delete",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 => Operation.response("Webhook", "application/json", webhook())
+ }
+ }
+ end
+
+ def enable_operation do
+ %Operation{
+ tags: ["Webhooks"],
+ summary: "Enable a webhook",
+ operationId: "AdminAPI.WebhookController.enable",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 => Operation.response("Webhook", "application/json", webhook())
+ }
+ }
+ end
+
+ def disable_operation do
+ %Operation{
+ tags: ["Webhooks"],
+ summary: "Disable a webhook",
+ operationId: "AdminAPI.WebhookController.disable",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 => Operation.response("Webhook", "application/json", webhook())
+ }
+ }
+ end
+
+ def rotate_secret_operation do
+ %Operation{
+ tags: ["Webhooks"],
+ summary: "Rotate webhook signing secret",
+ operationId: "AdminAPI.WebhookController.rotate_secret",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 => Operation.response("Webhook", "application/json", webhook())
+ }
+ }
+ end
+
+ defp webhook do
+ %Schema{
+ title: "Webhook",
+ description: "Schema for a webhook",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ url: %Schema{type: :string, format: :uri},
+ events: event_type(),
+ secret: %Schema{type: :string},
+ enabled: %Schema{type: :boolean},
+ created_at: %Schema{type: :string, format: :"date-time"},
+ updated_at: %Schema{type: :string, format: :"date-time"}
+ },
+ example: %{
+ "id" => "1",
+ "url" => "https://example.com/webhook",
+ "events" => ["report.created"],
+ "secret" => "D3D8CF4BC11FD9C41FD34DCC38D282E451C8BD34",
+ "enabled" => true,
+ "created_at" => "2022-06-24T16:19:38.523Z",
+ "updated_at" => "2022-06-24T16:19:38.523Z"
+ }
+ }
+ end
+
+ defp event_type(required \\ nil) do
+ %Schema{
+ type: :array,
+ items: %Schema{
+ title: "Event",
+ description: "Event type",
+ type: :string,
+ enum: ["account.created", "report.created"],
+ required: required
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, :string, "Webhook ID",
+ example: "123",
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
index d7e7ae24e3..46b545d482 100644
--- a/lib/pleroma/web/api_spec/operations/notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.NotificationOperation do
- alias OpenApiSpex.Operation
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index be8988a4a4..a9958fda11 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -238,6 +238,15 @@ defmodule Pleroma.Web.Router do
post("/rules", RuleController, :create)
patch("/rules/:id", RuleController, :update)
delete("/rules/:id", RuleController, :delete)
+
+ get("/webhooks", WebhookController, :index)
+ get("/webhooks/:id", WebhookController, :show)
+ post("/webhooks", WebhookController, :create)
+ patch("/webhooks/:id", WebhookController, :update)
+ delete("/webhooks/:id", WebhookController, :delete)
+ post("/webhooks/:id/enable", WebhookController, :enable)
+ post("/webhooks/:id/disable", WebhookController, :disable)
+ post("/webhooks/:id/rotate_secret", WebhookController, :rotate_secret)
end
# AdminAPI: admins and mods (staff) can perform these actions (if enabled by config)
diff --git a/lib/pleroma/webhook.ex b/lib/pleroma/webhook.ex
new file mode 100644
index 0000000000..d257f9d8cc
--- /dev/null
+++ b/lib/pleroma/webhook.ex
@@ -0,0 +1,100 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Webhook do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Repo
+
+ @event_types [:"account.created", :"report.created"]
+
+ schema "webhooks" do
+ field(:url, ObjectValidators.Uri, null: false)
+ field(:events, {:array, Ecto.Enum}, values: @event_types, null: false, default: [])
+ field(:secret, :string, null: false, default: "")
+ field(:enabled, :boolean, null: false, default: true)
+
+ timestamps()
+ end
+
+ def get(id), do: Repo.get(__MODULE__, id)
+
+ def get_by_type(type) do
+ __MODULE__
+ |> where([w], ^type in w.events)
+ |> Repo.all()
+ end
+
+ def changeset(%__MODULE__{} = webhook, params) do
+ webhook
+ |> cast(params, [:url, :events, :enabled])
+ |> validate_required([:url, :events])
+ |> unique_constraint(:url)
+ |> strip_events()
+ |> put_secret()
+ end
+
+ def update_changeset(%__MODULE__{} = webhook, params \\ %{}) do
+ webhook
+ |> cast(params, [:url, :events, :enabled])
+ |> unique_constraint(:url)
+ |> strip_events()
+ end
+
+ def create(params) do
+ {:ok, webhook} =
+ %__MODULE__{}
+ |> changeset(params)
+ |> Repo.insert()
+
+ webhook
+ end
+
+ def update(%__MODULE__{} = webhook, params) do
+ {:ok, webhook} =
+ webhook
+ |> update_changeset(params)
+ |> Repo.update()
+
+ webhook
+ end
+
+ def delete(webhook), do: webhook |> Repo.delete()
+
+ def rotate_secret(%__MODULE__{} = webhook) do
+ webhook
+ |> cast(%{}, [])
+ |> put_secret()
+ |> Repo.update()
+ end
+
+ def set_enabled(%__MODULE__{} = webhook, enabled) do
+ webhook
+ |> cast(%{enabled: enabled}, [:enabled])
+ |> Repo.update()
+ end
+
+ defp strip_events(params) do
+ if Map.has_key?(params, :events) do
+ params
+ |> Map.put(:events, Enum.filter(params[:events], &Enum.member?(@event_types, &1)))
+ else
+ params
+ end
+ end
+
+ defp put_secret(changeset) do
+ changeset
+ |> put_change(:secret, generate_secret())
+ end
+
+ defp generate_secret do
+ Base.encode16(:crypto.strong_rand_bytes(20))
+ |> String.downcase()
+ end
+end
diff --git a/lib/pleroma/webhook/notify.ex b/lib/pleroma/webhook/notify.ex
new file mode 100644
index 0000000000..d1b189ef0c
--- /dev/null
+++ b/lib/pleroma/webhook/notify.ex
@@ -0,0 +1,72 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Webhook.Notify do
+ alias Phoenix.View
+ alias Pleroma.Activity
+ alias Pleroma.User
+ alias Pleroma.Web.AdminAPI.Report
+ alias Pleroma.Webhook
+
+ def trigger_webhooks(%Activity{} = activity, :"report.created" = type) do
+ webhooks = Webhook.get_by_type(type)
+
+ Enum.each(webhooks, fn webhook ->
+ ConcurrentLimiter.limit(Webhook.Notify, fn ->
+ Task.start(fn -> report_created(webhook, activity) end)
+ end)
+ end)
+ end
+
+ def trigger_webhooks(%User{} = user, :"account.created" = type) do
+ webhooks = Webhook.get_by_type(type)
+
+ Enum.each(webhooks, fn webhook ->
+ ConcurrentLimiter.limit(Webhook.Notify, fn ->
+ Task.start(fn -> account_created(webhook, user) end)
+ end)
+ end)
+ end
+
+ def report_created(%Webhook{} = webhook, %Activity{} = report) do
+ object =
+ View.render(
+ Pleroma.Web.MastodonAPI.Admin.ReportView,
+ "show.json",
+ Report.extract_report_info(report)
+ )
+
+ deliver(webhook, object, :"report.created")
+ end
+
+ def account_created(%Webhook{} = webhook, %User{} = user) do
+ object =
+ View.render(
+ Pleroma.Web.MastodonAPI.Admin.AccountView,
+ "show.json",
+ user: user
+ )
+
+ deliver(webhook, object, :"account.created")
+ end
+
+ defp deliver(%Webhook{url: url, secret: secret}, object, type) do
+ body =
+ View.render_to_string(Pleroma.Web.AdminAPI.WebhookView, "event.json",
+ type: type,
+ object: object
+ )
+
+ headers = [
+ {"Content-Type", "application/json"},
+ {"X-Hub-Signature", "sha256=#{signature(body, secret)}"}
+ ]
+
+ Pleroma.HTTP.post(url, body, headers)
+ end
+
+ defp signature(body, secret) do
+ :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16()
+ end
+end
diff --git a/priv/repo/migrations/20220624104914_create_webhooks.exs b/priv/repo/migrations/20220624104914_create_webhooks.exs
new file mode 100644
index 0000000000..c7836fc0cb
--- /dev/null
+++ b/priv/repo/migrations/20220624104914_create_webhooks.exs
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Repo.Migrations.CreateWebhooks do
+ use Ecto.Migration
+
+ def change do
+ create_if_not_exists table(:webhooks) do
+ add(:url, :string, null: false)
+ add(:events, {:array, :string}, null: false, default: [])
+ add(:secret, :string, null: false, default: "")
+ add(:enabled, :boolean, null: false, default: true)
+
+ timestamps()
+ end
+
+ create_if_not_exists(unique_index(:webhooks, [:url]))
+ end
+end
diff --git a/test/pleroma/webhook_test.ex b/test/pleroma/webhook_test.ex
new file mode 100644
index 0000000000..21763f1e00
--- /dev/null
+++ b/test/pleroma/webhook_test.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.WebhookTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Repo
+ alias Pleroma.Webhook
+
+ test "creating a webhook" do
+ %{id: id} = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
+
+ assert %{url: "https://example.com/webhook"} = Webhook.get(id)
+ end
+
+ test "editing a webhook" do
+ %{id: id} =
+ webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
+
+ Webhook.update(webhook, %{events: [:"account.created"]})
+
+ assert %{events: [:"account.created"]} = Webhook.get(id)
+ end
+
+ test "filter webhooks by type" do
+ %{id: id1} =
+ Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
+
+ %{id: id2} =
+ Webhook.create(%{
+ url: "https://example.com/webhook2",
+ events: [:"account.created", :"report.created"]
+ })
+
+ Webhook.create(%{url: "https://example.com/webhook3", events: [:"account.created"]})
+
+ assert [%{id: ^id1}, %{id: ^id2}] = Webhook.get_by_type(:"report.created")
+ end
+
+ test "change webhook state" do
+ %{id: id, enabled: true} =
+ webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
+
+ Webhook.set_enabled(webhook, false)
+ assert %{enabled: false} = Webhook.get(id)
+ end
+
+ test "rotate webhook secrets" do
+ %{id: id, secret: secret} =
+ webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
+
+ Webhook.rotate_secret(webhook)
+ %{secret: new_secret} = Webhook.get(id)
+ assert secret != new_secret
+ end
+end