Skip to main content

lib/hex_wan30.ex

defmodule HexWan30 do
  @moduledoc """
  Small helpers for generating tracked `wan30.video` URLs and validating
  lightweight integrations.
  """

  @base_url "https://wan30.video"

  @doc """
  Returns the canonical website URL.
  """
  @spec website_url() :: String.t()
  def website_url, do: @base_url

  @doc """
  Builds a URL for a path under `wan30.video`.

  Empty and root-like paths both resolve to the website root.
  """
  @spec url(String.t() | nil) :: String.t()
  def url(path \\ nil)

  def url(nil), do: @base_url
  def url(""), do: @base_url
  def url("/"), do: @base_url
  def url(path), do: @base_url <> normalize_path(path)

  @doc """
  Builds a campaign URL with standard UTM parameters.

  Supported keys:
  `:source`, `:medium`, `:campaign`, `:term`, `:content`
  """
  @spec campaign_url(String.t() | nil, keyword()) :: String.t()
  def campaign_url(path \\ nil, params) when is_list(params) do
    query =
      params
      |> Enum.flat_map(fn
        {:source, value} -> [{"utm_source", value}]
        {:medium, value} -> [{"utm_medium", value}]
        {:campaign, value} -> [{"utm_campaign", value}]
        {:term, value} -> [{"utm_term", value}]
        {:content, value} -> [{"utm_content", value}]
        _ -> []
      end)
      |> encode_sorted_query()

    append_query(url(path), query)
  end

  @doc """
  Builds a referral URL using the `ref` query parameter.
  """
  @spec referral_url(String.t(), String.t() | nil) :: String.t()
  def referral_url(ref, path \\ nil) when is_binary(ref) do
    append_query(url(path), URI.encode_query(%{"ref" => ref}))
  end

  @doc """
  Builds a shareable URL with optional campaign metadata.

  Supported params:
  `:ref`, `:source`, `:medium`, `:campaign`
  """
  @spec generate_share_url(String.t() | nil, keyword()) :: String.t()
  def generate_share_url(path \\ nil, params \\ []) when is_list(params) do
    query =
      params
      |> Enum.flat_map(fn
        {:ref, value} -> [{"ref", value}]
        {:source, value} -> [{"utm_source", value}]
        {:medium, value} -> [{"utm_medium", value}]
        {:campaign, value} -> [{"utm_campaign", value}]
        _ -> []
      end)
      |> encode_sorted_query()

    append_query(url(path), query)
  end

  @doc """
  Builds an embeddable URL under `/embed`.

  Example:
  `embed_url("demo-video", autoplay: true)`
  """
  @spec embed_url(String.t(), keyword()) :: String.t()
  def embed_url(asset_id, opts \\ []) when is_binary(asset_id) and is_list(opts) do
    query =
      %{
        "asset" => asset_id,
        "autoplay" => truthy_param(Keyword.get(opts, :autoplay, false)),
        "theme" => Keyword.get(opts, :theme)
      }
      |> Enum.reject(fn {_key, value} -> is_nil(value) end)
      |> encode_sorted_query()

    append_query(url("/embed"), query)
  end

  @doc """
  Produces a SHA-256 HMAC signature for webhook payloads.
  """
  @spec sign_webhook(binary(), binary()) :: String.t()
  def sign_webhook(payload, secret) when is_binary(payload) and is_binary(secret) do
    :hmac
    |> :crypto.mac(:sha256, secret, payload)
    |> Base.encode16(case: :lower)
  end

  @doc """
  Verifies a webhook payload against an expected signature.
  """
  @spec verify_webhook_signature(binary(), binary(), binary()) :: boolean()
  def verify_webhook_signature(payload, signature, secret)
      when is_binary(payload) and is_binary(signature) and is_binary(secret) do
    payload
    |> sign_webhook(secret)
    |> secure_compare(signature)
  end

  defp normalize_path("/" <> _ = path), do: path
  defp normalize_path(path), do: "/" <> path

  defp append_query(base, ""), do: base
  defp append_query(base, query), do: base <> "?" <> query

  defp encode_sorted_query(pairs) do
    pairs
    |> Enum.sort_by(fn {key, _value} -> key end)
    |> URI.encode_query()
  end

  defp truthy_param(true), do: "1"
  defp truthy_param(false), do: "0"

  # Constant-time comparison to avoid leaking signature mismatch position.
  defp secure_compare(left, right) when byte_size(left) == byte_size(right) do
    left
    |> :binary.bin_to_list()
    |> Enum.zip(:binary.bin_to_list(right))
    |> Enum.reduce(0, fn {a, b}, acc -> Bitwise.bor(acc, Bitwise.bxor(a, b)) end)
    |> Kernel.==(0)
  end

  defp secure_compare(_left, _right), do: false
end