defmodule Intercom.API do @moduledoc """ Provides direct access to the Intercom API if other modules in this package don't provide the functionality you need. See https://developers.intercom.com/intercom-api-reference/reference """ @type opts :: {:per_page, integer()} | {:starting_after, binary()} @type metadata_pagination :: %{ required(:page) => integer(), optional(:next_page) => integer(), required(:total_pages) => integer(), required(:per_page) => integer(), optional(:starting_after) => binary() } @type metadata :: %{ required(:response) => map(), required(:rate_limit) => %{ limit: integer() | nil, reset: %DateTime{} | nil, remaining: integer() | nil }, optional(:pagination) => metadata_pagination, optional(:errors) => [map()] } @type single_success :: {:ok, map(), metadata} @type multiple_success :: {:ok, [map()], metadata} @type error :: {:error, atom(), metadata | nil} @type response :: single_success | multiple_success | error @doc """ Call an Intercom API endpoint. Arguments: - `method`: The HTTP request method. - `path`: The request path, e.g `"users/1234"`. - `body`: The body of the request. Optional. - `opts`: Will be added either to the query part of the url (get) or the body of the request (post). Optional. - `per_page`: Results per page if result is a list, has to be between 1 and 150. - `starting_after`: Hash returned by the intercom API to get next page of results. Returns `{:ok, data, metadata}` or `{:error, error_code, metadata}`. """ def call_endpoint(method, path, body \\ nil, opts \\ []) @spec call_endpoint(:get, String.t(), nil) :: response() @spec call_endpoint(:get, String.t(), nil, [opts]) :: response() def call_endpoint(:get, path, nil, opts) do with url <- Intercom.API.Rest.url(path, opts) do call_endpoint_with_full_url_and_body(:get, url, nil) end end @spec call_endpoint(:post, String.t(), map() | nil) :: response() def call_endpoint(:post, path, body, []) do with url <- Intercom.API.Rest.url(path) do call_endpoint_with_full_url_and_body(:post, url, body) end end @spec call_endpoint(:post, String.t(), map() | nil, [opts]) :: response() def call_endpoint(:post, path, body, opts) do body = Map.merge(body, %{ pagination: Enum.reduce(opts, %{}, fn {k, v}, acc -> Map.merge(acc, %{"#{k}": v}) end) }) call_endpoint(:post, path, body) end @spec call_endpoint(:put, String.t(), map() | nil) :: response() def call_endpoint(:put, path, body, []) do with url <- Intercom.API.Rest.url(path) do call_endpoint_with_full_url_and_body(:put, url, body) end end defp call_endpoint_with_full_url_and_body(method, url, body) do with {:ok, authorized_headers} <- Intercom.API.Rest.authorized_headers(), {:ok, response, body} <- Intercom.API.Request.make_request(method, url, authorized_headers, body) do metadata = Map.merge(extract_metadata_from_headers(response), extract_metadata_from_body(body)) {:ok, extract_body(body), metadata} else {:error, response, body} -> metadata = Map.merge(extract_metadata_from_headers(response), extract_metadata_from_body(body)) {:error, extract_error_code(response), metadata} {:error, response} -> {:error, extract_error_code(response), extract_metadata_from_headers(response)} end end defp extract_metadata_from_body(%{"pages" => %{"next" => next} = pages, "type" => "list"}) do %{ pagination: %{ page: Map.get(pages, "page"), next_page: Map.get(next, "page"), total_pages: Map.get(pages, "total_pages"), per_page: Map.get(pages, "per_page"), starting_after: Map.get(next, "starting_after") } } end defp extract_metadata_from_body(%{"pages" => pages, "type" => "list"}) do %{ pagination: %{ page: Map.get(pages, "page"), total_pages: Map.get(pages, "total_pages"), per_page: Map.get(pages, "per_page") } } end defp extract_metadata_from_body(%{"type" => "error.list", "errors" => errors}) do %{ errors: errors } end defp extract_metadata_from_body(_body), do: %{} defp extract_metadata_from_headers(%HTTPoison.Response{} = response) do headers = Enum.into(response.headers, %{}) %{ response: response, rate_limit: %{ limit: extract_x_ratelimit_limit_from_headers(headers), reset: extract_x_ratelimit_reset_from_headers(headers), remaining: extract_x_ratelimit_remaining_from_headers(headers) } } end defp extract_metadata_from_headers(%HTTPoison.Error{} = error), do: %{error: error} defp extract_metadata_from_headers(_response), do: nil defp extract_x_ratelimit_limit_from_headers(%{"X-RateLimit-Limit" => x_ratelimit_limit}), do: String.to_integer(x_ratelimit_limit) defp extract_x_ratelimit_limit_from_headers(_headers), do: nil defp extract_x_ratelimit_reset_from_headers(%{"X-RateLimit-Reset" => x_ratelimit_reset}), do: x_ratelimit_reset |> String.to_integer() |> DateTime.from_unix!() defp extract_x_ratelimit_reset_from_headers(_headers), do: nil defp extract_x_ratelimit_remaining_from_headers(%{ "X-RateLimit-Remaining" => x_ratelimit_remaining }), do: String.to_integer(x_ratelimit_remaining) defp extract_x_ratelimit_remaining_from_headers(_headers), do: nil defp extract_body(%{"data" => data, "type" => "list"}), do: data defp extract_body(body), do: body defp extract_error_code(%HTTPoison.Response{status_code: 400}), do: :bad_request defp extract_error_code(%HTTPoison.Response{status_code: 404}), do: :resource_not_found defp extract_error_code(%HTTPoison.Response{status_code: 429}), do: :rate_limit_exceeded defp extract_error_code(:no_access_token), do: :no_access_token defp extract_error_code(:invalid_access_token), do: :invalid_access_token defp extract_error_code(_response), do: :undefined end