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