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