Packages
Event-driven notification system for Ash Framework with multiple transport types
Current section
Files
Jump to
Current section
Files
lib/transports/discord.ex
defmodule AshDispatch.Transports.Discord do
use AshDispatch.Transport, atom: :discord, skip_receipt?: false
@moduledoc """
Discord webhook transport.
Sends messages to Discord channels via webhooks (async via Oban).
## Configuration
Requires `webhook_url` in channel metadata or via callback:
# Inline configuration
event :order_created,
channels: [
[
transport: :discord,
audience: :team,
metadata: [
webhook_url: "https://discord.com/api/webhooks/..."
]
]
]
# Or via callback module
def channels(context) do
[
%Channel{
transport: :discord,
audience: :team,
metadata: %{
webhook_url: get_discord_webhook_url(:orders)
}
}
]
end
## Content
Discord messages support:
- `notification_message` - Simple text content
- `discord_embed` - Rich embed with title, description, color, fields
- `discord_username` - Override webhook username
- `discord_avatar_url` - Override webhook avatar
Example with embed:
content: [
notification_message: "New order received!",
discord_embed: %{
title: "Order {{id}} Created",
description: "Customer: {{user.name}}",
color: 5814783, # Blue color (hex: 58B9FF)
fields: [
%{name: "Total", value: "${{total}}", inline: true},
%{name: "Items", value: "{{item_count}}", inline: true}
]
}
]
## Discord Embed Colors
- Success (Green): 5763719 (0x57F287)
- Info (Blue): 5814783 (0x58B9FF)
- Warning (Yellow): 16776960 (0xFFFF00)
- Error (Red): 15548997 (0xED4245)
"""
import AshDispatch.ContentMap
alias AshDispatch.Channel
alias AshDispatch.Workers.SendWebhook
require Logger
@doc """
Delivers Discord notification by enqueueing SendWebhook worker.
## Receipt Content
Expected content structure:
- `notification_message` - Message text (required)
- `discord_embed` - Embed object (optional)
- `discord_username` - Webhook username override (optional)
- `discord_avatar_url` - Webhook avatar override (optional)
## Channel Metadata
Expected metadata:
- `webhook_url` - Discord webhook URL (required)
"""
def deliver(receipt, context, channel, _event_config) do
webhook_url = get_webhook_url(channel)
if webhook_url do
# Build Discord payload
payload = build_discord_payload(receipt.content, context)
# Enqueue webhook worker
job_args = %{
receipt_id: receipt.id,
webhook_url: webhook_url,
payload: payload,
headers: %{"Content-Type" => "application/json"}
}
case SendWebhook.new(job_args) |> Oban.insert() do
{:ok, _job} ->
# Mark receipt as scheduled
updated_receipt =
receipt
|> Ash.Changeset.for_update(:schedule, %{})
|> Ash.update!()
Logger.info("Discord webhook job enqueued for receipt #{receipt.id}")
{:ok, updated_receipt}
{:error, reason} ->
# Failed to enqueue - mark receipt as failed
Logger.error("Failed to enqueue Discord webhook job: #{inspect(reason)}")
updated_receipt =
receipt
|> Ash.Changeset.for_update(:mark_failed, %{
error_message: "Failed to enqueue: #{inspect(reason)}"
})
|> Ash.update!()
{:ok, updated_receipt}
end
else
# No webhook URL configured - skip
Logger.warning("Discord transport missing webhook_url for receipt #{receipt.id}, skipping")
updated_receipt =
receipt
|> Ash.Changeset.for_update(:skip, %{
error_message: "No webhook_url configured"
})
|> Ash.update!()
{:ok, updated_receipt}
end
end
# Private helpers
defp get_webhook_url(%Channel{webhook_url: url}) when is_binary(url), do: url
defp get_webhook_url(%Channel{opts: opts}) when is_map(opts) do
opts["webhook_url"] || opts[:webhook_url]
end
defp get_webhook_url(_), do: nil
defp build_discord_payload(content, _context) when is_map(content) do
# Base payload with content
payload = %{
"content" => get_content(content, :notification_message)
}
# Add embed if provided
payload =
case get_content(content, :discord_embed) do
nil -> payload
embed -> Map.put(payload, "embeds", [embed])
end
# Add username override if provided
payload =
case get_content(content, :discord_username) do
nil -> payload
username -> Map.put(payload, "username", username)
end
# Add avatar override if provided
payload =
case get_content(content, :discord_avatar_url) do
nil -> payload
avatar_url -> Map.put(payload, "avatar_url", avatar_url)
end
payload
end
defp build_discord_payload(_content, _context) do
# Fallback for non-map content
%{"content" => "Notification"}
end
end