defmodule ExMicrosoftBot.Client do
@moduledoc """
This module contains low-level functions for interacting with the Bot
Framework API.
For most use-cases and a friendlier API, each of the individual endpoints'
client modules should be used instead:
* `ExMicrosoftBot.Client.Attachments`
* `ExMicrosoftBot.Client.Conversations`
* `ExMicrosoftBot.Client.Teams`
Still, this module exports functions that can help perform arbitrary,
pre-authorized API calls, for example for fetching assets or calling new
endpoints that this library hasn't implemented yet.
"""
alias ExMicrosoftBot.TokenManager
require Logger
@type error_type :: {:error, integer, String.t()}
@http_client_opts Application.get_env(:ex_microsoftbot, :http_client_opts, [])
@doc """
GETs the given URI with an authorized request & standard options, logging the
result.
"""
@spec get(url :: String.t(), extra_opts :: keyword()) :: HTTPoison.Response.t()
def get(url, extra_opts \\ []) do
response = HTTPoison.get(url, authed_headers(url), opts(extra_opts))
Logger.debug("GET #{inspect(url)}: #{inspect(response)}")
response
end
@doc """
DELETEs the given URI with an authorized request & standard options, logging
the result.
"""
@spec delete(url :: String.t(), extra_opts :: keyword()) :: HTTPoison.Response.t()
def delete(url, extra_opts \\ []) do
response = HTTPoison.delete(url, authed_headers(url), opts(extra_opts))
Logger.debug("DELETE #{inspect(url)}: #{inspect(response)}")
response
end
@doc """
POSTs the given body to the given URI with an authorized request & standard
options, logging the result.
If a map, the body is JSON encoded before POSTing. Will raise if encoding
fails.
"""
@spec post(url :: String.t(), body :: String.t() | map(), extra_opts :: keyword()) ::
HTTPoison.Response.t()
def post(url, body, extra_opts \\ [])
def post(url, body, extra_opts) when is_binary(body) do
response = HTTPoison.post(url, body, authed_headers(url), opts(extra_opts))
Logger.debug("POST #{inspect(url)}: #{inspect(response)}", body: body)
response
end
def post(url, body, extra_opts) when is_map(body),
do: post(url, Poison.encode!(body), extra_opts)
@doc """
PUTs the given body to the given URI with an authorized request & standard
options, logging the result.
If a map, the body is JSON encoded before PUTing. Will raise if encoding
fails.
"""
@spec put(url :: String.t(), body :: String.t() | map(), extra_opts :: keyword()) ::
HTTPoison.Response.t()
def put(url, body, extra_opts \\ [])
def put(url, body, extra_opts) when is_binary(body) do
response = HTTPoison.put(url, body, authed_headers(url), opts(extra_opts))
Logger.debug("PUT #{inspect(url)}: #{inspect(response)}", body: body)
response
end
def put(url, body, extra_opts) when is_map(body),
do: put(url, Poison.encode!(body), extra_opts)
@doc """
Helper that deserializes a request response using the given deserializing
function if the given `response` has a successful status. Otherwise, returns
an error tuple instead.
The deserializing function is skipped when there is no body; an empty string
is returned instead.
"""
@spec deserialize_response(response :: HTTPoison.Response.t(), (String.t() -> deserialized)) ::
{:ok, deserialized | String.t()} | {:error, integer(), String.t()}
when deserialized: any()
def deserialize_response({:ok, %HTTPoison.Response{status_code: sc, body: ""}}, _deserialize_fn)
when sc >= 200 and sc < 300 do
{:ok, ""}
end
def deserialize_response(
{:ok, %HTTPoison.Response{status_code: sc, body: body}},
deserialize_fn
)
when sc >= 200 and sc < 300 do
{:ok, deserialize_fn.(body)}
end
def deserialize_response(
{:ok, %HTTPoison.Response{status_code: status_code, body: body} = response},
_deserialize_fn
) do
Logger.error("Error response: #{status_code}: #{body} \n Raw Response: #{inspect(response)}")
{:error, status_code, body}
end
def deserialize_response({:error, %HTTPoison.Error{} = resp}, _deserialize_fn) do
message = Exception.message(resp)
Logger.error("deserialize_response/2: Error response: #{message}")
Logger.error("deserialize_response/2: Error response: #{inspect(resp)}")
{:error, 0, message}
end
@doc """
Returns the headers required for an authorized JSON request to the
BotFramework API.
"""
@spec authed_headers(uri :: String.t()) :: [{String.t(), String.t()}]
def authed_headers(uri) do
Keyword.merge(
[
"Content-Type": "application/json",
Accept: "application/json"
],
create_auth_headers(uri)
)
end
@doc """
Returns an options Keyword for request functions. Merges the default options
(from config) with the given `extra`.
"""
@spec opts(keyword :: keyword()) :: keyword()
def opts(extra \\ []),
do: Keyword.merge(@http_client_opts, extra)
# Private
defp create_auth_headers(uri) do
if !https?(uri) && using_emulator?() do
[Authorization: "Bearer"]
else
[Authorization: "Bearer #{TokenManager.get_token()}"]
end
end
defp https?(uri) do
%URI{scheme: scheme} = URI.parse(uri)
scheme == "https"
end
defp using_emulator?(),
do: Application.get_env(:ex_microsoftbot, :using_bot_emulator)
end