line_bot/lib/line_bot/api_client.ex

132 lines
4.4 KiB
Elixir

defmodule LineBot.APIClient do
use GenServer
use HTTPoison.Base
require Logger
@token_server Application.get_env(:line_bot, :token_server, LineBot.TokenServer)
@moduledoc """
An implementation of `HTTPoison.Base` for making API calls to the Messaging API.
## API Client
This module is for creating and sending HTTP requests manually. To use the documented messaging
API endpoints, use the `LineBot` module, which calls the functions in this module.
## Implementation
This module extends `HTTPoison.Base` with the following behaviour:
* The `Authorization` header containing the bearer token is automatically added. The access
token is retreived from `LineBot.TokenServer`.
* URLs are automatically prefixed with `https://api.line.me/v2/bot/`. When making a request,
only the path after this prefix is required.
* JSON responses are automatically decoded.
* POST requests are automatically encoded into JSON, and the appropriate `Content-Type` header
is added.
* 401 Unauthorized errors are caught. When an unauthorized request is received, an attempt
to retrieve a new access token is made by calling `LineBot.TokenServer.purge/0`. However,
after three successive unauthorized errors, further unauthorized responses are returned
directly.
"""
# GenServer
############
@impl GenServer
def init(_), do: {:ok, 0}
@impl GenServer
def handle_call(:get_and_inc_auth_fail, _from, count), do: {:reply, count, count + 1}
@impl GenServer
def handle_call(:reset, _from, _count), do: {:reply, :ok, 0}
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
# HTTPoison
############
@impl HTTPoison.Base
@doc "Prepends the request url with `https://api.line.me/v2/bot/`."
def process_request_url(url), do: super("https://api.line.me/v2/bot/" <> url)
@impl HTTPoison.Base
@doc """
Adds the OAuth Bearer token to the `Authorization` header.
The token is retrieved by calling `LineBot.TokenServer.get_token/0`.
Also adds the `User-Agent` header with a value of `line-botsdk-elixir/vX.X.X`.
This follows the pattern of Line Bot libraries in other languages.
"""
def process_request_headers(headers) do
[
{"Authorization", "Bearer #{@token_server.get_token()}"},
{"User-Agent", "line-botsdk-elixir/v#{Application.spec(:line_bot, :vsn)}"}
| super(headers)
]
end
@impl HTTPoison.Base
@doc """
If the reponse headers indidate that the response is JSON, the response body is
automatically decoded.
"""
def process_response(%{headers: headers, body: body} = response) do
# TODO make this configurable
Logger.debug("API Response: " <> inspect(response))
case Enum.find(headers, &(String.downcase(elem(&1, 0)) == "content-type")) do
{_, "application/json" <> _} -> put_in(response.body, Jason.decode!(body))
_ -> super(response)
end
end
@impl HTTPoison.Base
@doc """
Issues a POST request to the given url.
The body is automatically encoded into JSON, and the `Content-Type` header is added.
"""
def post(url, body, headers \\ []) do
super(url, Jason.encode!(body), [{"Content-Type", "application/json"} | headers])
end
@impl HTTPoison.Base
@doc """
Issues a POST request to the given url.
The body is automatically encoded into JSON, and the `Content-Type` header is added.
"""
def post!(url, body, headers \\ []) do
super(url, Jason.encode!(body), [{"Content-Type", "application/json"} | headers])
end
@impl HTTPoison.Base
@doc """
In addition to the `HTTPoison.request/5` behaviour, will automatically refresh the access
token after an unauthorized request. Gives up after three subsequent unauthorized errors.
"""
def request(method, url, body, headers, opts \\ []) do
case super(method, url, body, headers, opts) do
{:ok, %{status_code: 200} = response} ->
{:via, Registry, {LineBot.Registry, "client"}}
GenServer.call(__MODULE__, :reset)
{:ok, response}
{:ok, %{status_code: 401} = response} ->
if GenServer.call(__MODULE__, :get_and_inc_auth_fail) < 3 do
Logger.warn("Unauthorized. Purge token and try again.")
@token_server.purge()
request(method, url, body, headers, opts)
else
Logger.warn("Unauthorized after multiple attempts with different tokens. Giving up.")
{:ok, response}
end
other ->
other
end
end
end