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