Skip to main content

lib/gelotv_bot/apis/twitch.ex

defmodule GelotvBot.APIs.Twitch do
  @moduledoc """
  Generic Twitch Helix API client.

  This module intentionally exposes a generic request function so the package
  can reach current and future Helix endpoints without depending on external
  HTTP or JSON libraries.
  """

  alias GelotvBot.{API, OAuth, Pagination}

  @base_url "https://api.twitch.tv/helix"
  @oauth_token_url "https://id.twitch.tv/oauth2/token"

  @type credentials :: %{
          optional(:access_token) => String.t(),
          optional(:client_id) => String.t()
        }

  @spec request(atom(), String.t(), credentials(), keyword()) :: GelotvBot.HTTPClient.response()
  def request(method, path, credentials, opts \\ []) do
    credentials = Map.new(credentials || %{})

    url =
      API.build_url(Keyword.get(opts, :base_url, @base_url), path, Keyword.get(opts, :params, []))

    headers = twitch_headers(credentials) ++ Keyword.get(opts, :headers, [])

    API.request(method, url, Keyword.merge(opts, headers: headers))
  end

  @spec request_decoded(atom(), String.t(), credentials(), keyword()) ::
          {:ok, map()} | {:error, term()}
  def request_decoded(method, path, credentials, opts \\ []) do
    method
    |> request(path, credentials, opts)
    |> API.decode_response()
  end

  def get(path, credentials, opts \\ []), do: request(:get, path, credentials, opts)

  def post(path, credentials, body, opts \\ []),
    do: request(:post, path, credentials, Keyword.put(opts, :body, body))

  def put(path, credentials, body, opts \\ []),
    do: request(:put, path, credentials, Keyword.put(opts, :body, body))

  def patch(path, credentials, body, opts \\ []),
    do: request(:patch, path, credentials, Keyword.put(opts, :body, body))

  def delete(path, credentials, opts \\ []), do: request(:delete, path, credentials, opts)

  def get_decoded(path, credentials, opts \\ []),
    do: request_decoded(:get, path, credentials, opts)

  def post_decoded(path, credentials, body, opts \\ []),
    do: request_decoded(:post, path, credentials, Keyword.put(opts, :body, body))

  def put_decoded(path, credentials, body, opts \\ []),
    do: request_decoded(:put, path, credentials, Keyword.put(opts, :body, body))

  def patch_decoded(path, credentials, body, opts \\ []),
    do: request_decoded(:patch, path, credentials, Keyword.put(opts, :body, body))

  def delete_decoded(path, credentials, opts \\ []),
    do: request_decoded(:delete, path, credentials, opts)

  @spec users(credentials(), keyword()) :: GelotvBot.HTTPClient.response()
  def users(credentials, opts \\ []), do: get("/users", credentials, opts)

  @spec streams(credentials(), keyword()) :: GelotvBot.HTTPClient.response()
  def streams(credentials, opts \\ []), do: get("/streams", credentials, opts)

  @spec chat_settings(credentials(), String.t(), String.t(), keyword()) ::
          GelotvBot.HTTPClient.response()
  def chat_settings(credentials, broadcaster_id, moderator_id, opts \\ []) do
    get(
      "/chat/settings",
      credentials,
      Keyword.update(
        opts,
        :params,
        [broadcaster_id: broadcaster_id, moderator_id: moderator_id],
        fn params ->
          params
          |> Enum.to_list()
          |> Keyword.put(:broadcaster_id, broadcaster_id)
          |> Keyword.put(:moderator_id, moderator_id)
        end
      )
    )
  end

  @spec send_chat_message(credentials(), map(), keyword()) :: GelotvBot.HTTPClient.response()
  def send_chat_message(credentials, body, opts \\ []),
    do: post("/chat/messages", credentials, body, opts)

  @spec client_credentials_token(map() | keyword(), keyword()) :: {:ok, map()} | {:error, term()}
  def client_credentials_token(credentials, opts \\ []) do
    credentials = Map.new(credentials)

    OAuth.token_request(
      Keyword.get(opts, :token_url, @oauth_token_url),
      %{
        client_id: Map.get(credentials, :client_id),
        client_secret: Map.get(credentials, :client_secret),
        grant_type: "client_credentials",
        scope: Map.get(credentials, :scope)
      },
      opts
    )
  end

  @spec refresh_token(map() | keyword(), keyword()) :: {:ok, map()} | {:error, term()}
  def refresh_token(credentials, opts \\ []) do
    credentials = Map.new(credentials)

    OAuth.token_request(
      Keyword.get(opts, :token_url, @oauth_token_url),
      %{
        client_id: Map.get(credentials, :client_id),
        client_secret: Map.get(credentials, :client_secret),
        grant_type: "refresh_token",
        refresh_token: Map.get(credentials, :refresh_token)
      },
      opts
    )
  end

  @spec paginate(String.t(), credentials(), keyword()) :: {:ok, [map()]} | {:error, term()}
  def paginate(path, credentials, opts \\ []) do
    Pagination.collect(
      fn page_params ->
        params = Keyword.merge(Keyword.get(opts, :params, []), page_params)
        get(path, credentials, Keyword.put(opts, :params, params))
      end,
      next: Keyword.get(opts, :next, &Pagination.twitch_next/1),
      max_pages: Keyword.get(opts, :max_pages, 100)
    )
  end

  defp twitch_headers(credentials) do
    API.bearer(Map.get(credentials, :access_token)) ++
      client_id_header(Map.get(credentials, :client_id)) ++
      API.json_headers()
  end

  defp client_id_header(nil), do: []
  defp client_id_header(""), do: []
  defp client_id_header(client_id), do: [{"Client-Id", to_string(client_id)}]
end