defmodule PolymarketNotify do @moduledoc """ Notification **controls + channels** for the Polymarket bot. Two layers: * **Controls** (`PolymarketNotify.Controls`) — the core: *what* the user wants to see and *where*, as per-**category** defaults (`:fills`, `:resolutions`, `:errors`, `:daily_summary`, …) with per-**strategy** overrides. So you can route `fills` for the AI strategy to Telegram while muting `fills` for the surge bots, etc. * **Channels** (`PolymarketNotify.Channel`) — pluggable delivery: `Telegram`, `Discord`, `Webhook`, `Log`. A channel just ships text. Strategy is identified by its **tag** (any string) — built-in or a user-written strategy alike — so custom strategies get their own notification settings with no code change here. `notify/3` is **best-effort**: it consults the controls, and if the (category, strategy, value) passes, fans the message out to that category's channels. Nothing configured / not allowed ⇒ silent no-op. It never raises into the caller. Config: config :polymarket_notify, channels: %{ telegram: {PolymarketNotify.Telegram, [bot_token: {:system, "TELEGRAM_BOT_TOKEN"}, chat_id: ...]}, log: {PolymarketNotify.Log, []} }, controls: PolymarketNotify.Controls.default() """ alias PolymarketNotify.Controls require Logger @doc """ Deliver `message` for `category`. Options: * `:strategy` — strategy tag (for per-strategy overrides). * `:value` — magnitude (e.g. fill notional, P&L) for threshold filters. * `:controls` / `:channels` — inject (tests); else app config / defaults. Returns `:ok` (always; best-effort). """ @spec notify(atom(), String.t(), keyword()) :: :ok def notify(category, message, opts \\ []) when is_atom(category) and is_binary(message) do controls = Keyword.get(opts, :controls) || configured_controls() channels = Keyword.get(opts, :channels) || configured_channels() strategy = Keyword.get(opts, :strategy) value = Keyword.get(opts, :value) if Controls.allow?(controls, category, strategy, value) do controls |> Controls.channels_for(category, strategy) |> Enum.each(&deliver(&1, channels, message)) end :ok rescue e -> Logger.debug(fn -> "PolymarketNotify: #{Exception.message(e)}" end) :ok catch kind, reason -> Logger.debug(fn -> "PolymarketNotify: #{inspect(kind)} #{inspect(reason)}" end) :ok end @doc "Resolve a literal, `{:system, \"VAR\"}` (env), or `nil`. Channels use this for secrets." @spec resolve(term()) :: term() def resolve({:system, var}) when is_binary(var), do: System.get_env(var) def resolve(value), do: value defp configured_controls, do: Application.get_env(:polymarket_notify, :controls) || Controls.default() defp configured_channels, do: Application.get_env(:polymarket_notify, :channels, %{log: {PolymarketNotify.Log, []}}) defp deliver(channel_name, channels, message) do case Map.get(channels, channel_name) do {mod, opts} when is_atom(mod) -> try do mod.deliver(message, opts) rescue _ -> :ok catch _, _ -> :ok end _ -> :ok end end end