Current section

Files

Jump to
whatsapp_elixir lib whatsapp_elixir http.ex
Raw

lib/whatsapp_elixir/http.ex

defmodule WhatsappElixir.HTTP do
@moduledoc """
Module to handle HTTP requests for the WhatsApp Elixir library.
"""
require Logger
alias Req.Response
@default_config [
token: "",
phone_number_id: "",
verify_token: "",
base_url: "https://graph.facebook.com",
api_version: "v18.0"
]
@spec config(keyword()) :: keyword()
def config(custom_config \\ []) do
app_config = Application.get_env(:whatsapp_elixir, __MODULE__, @default_config)
Keyword.merge(@default_config, Keyword.merge(app_config, custom_config))
end
def base_url(custom_config \\ []) do
config(custom_config) |> Keyword.get(:base_url)
end
def api_version(custom_config \\ []) do
config(custom_config) |> Keyword.get(:api_version)
end
@doc """
Sends a POST request to the specified endpoint with the given body.
"""
def post(
endpoint,
body,
opts \\ [],
include_phone_number_id \\ true,
content_type \\ "application/json"
) do
config_opts = config(opts)
# Get phone_number_id if needed
phone_number_id =
if include_phone_number_id do
config(config_opts) |> Keyword.get(:phone_number_id)
end
if include_phone_number_id and (phone_number_id == "" or phone_number_id == nil) do
raise ArgumentError, "phone_number_id must be provided"
end
token = config(config_opts) |> Keyword.get(:token)
if token == "" do
raise ArgumentError, "Missing App Token"
end
# Construct URL based on whether phone_number_id is included
url =
if include_phone_number_id do
"#{base_url(config_opts)}/#{api_version(config_opts)}/#{phone_number_id}/#{endpoint}"
else
"#{base_url(config_opts)}/#{api_version(config_opts)}/#{endpoint}"
end
if content_type == "multipart/form-data" do
content_type = Multipart.content_type(body, content_type)
body = Multipart.body_stream(body)
case Req.post(url, body: body, decode_body: false, headers: headers(token, content_type)) do
{:ok, %Response{status: status, body: body}} when status in 200..299 ->
{:ok, Jason.decode!(body)}
{:ok, %Response{status: status, body: body}} ->
Logger.error(
"[WHATSAPP_ELIXIR] HTTP request failed with status #{status}: #{inspect(body)}"
)
{:error, Jason.decode!(body)}
{:error, reason} ->
Logger.error("HTTP request failed: #{inspect(reason)}")
{:error, reason}
end
else
case Req.post(url,
body: Jason.encode!(body),
decode_body: false,
headers: headers(token, content_type)
) do
{:ok, %Response{status: status, body: body}} when status in 200..299 ->
{:ok, Jason.decode!(body)}
{:ok, %Response{status: status, body: body}} ->
Logger.error(
"[WHATSAPP_ELIXIR] HTTP request failed with status #{status}: #{inspect(body)}"
)
{:error, Jason.decode!(body)}
{:error, reason} ->
Logger.error("HTTP request failed: #{inspect(reason)}")
{:error, reason}
end
end
end
def get(endpoint, params \\ %{}, opts \\ [], url_overide \\ false) do
config_opts = config(opts)
phone_number_id = config(config_opts) |> Keyword.get(:phone_number_id)
if phone_number_id == "" do
raise ArgumentError, "phone_number_id must be provided"
end
token = config(config_opts) |> Keyword.get(:token)
if token == "" do
raise ArgumentError, "Missing App Token"
end
# Construct the base URL
base_url_path =
if url_overide do
# When `url_overide` is true, directly use the `endpoint` as part of the path
"#{base_url(config_opts)}/#{api_version(config_opts)}/#{endpoint}"
else
phone_number_id = config_opts |> Keyword.get(:phone_number_id)
if phone_number_id == "" do
raise ArgumentError, "phone_number_id must be provided"
end
"#{base_url(config_opts)}/#{api_version(config_opts)}/#{phone_number_id}/#{endpoint}"
end
# Add query params only if `url_overide` is false
query_string = URI.encode_query(params)
full_url =
if query_string != "" do
"#{base_url_path}?#{query_string}"
else
base_url_path
end
Logger.info("Constructed URL: #{full_url}")
case Req.get(full_url, decode_body: false, headers: headers(token)) do
{:ok, %Response{status: status, body: body}} when status in 200..299 ->
{:ok, Jason.decode!(body)}
{:ok, %Response{status: status, body: body}} ->
Logger.error(
"[WHATSAPP_ELIXIR] HTTP request failed with status #{status}: #{inspect(body)}"
)
{:error, body}
{:error, reason} ->
Logger.error("[WHATSAPP_ELIXIR] HTTP request failed: #{inspect(reason)}")
{:error, reason}
end
end
def get_url(url, opts \\ [], headers \\ []) do
config_opts = config(opts)
token = config(config_opts) |> Keyword.get(:token)
if token == "" do
raise ArgumentError, "Missing App Token"
end
full_headers = [{"Authorization", "Bearer #{token}"} | headers]
Logger.info("Making GET request to URL: #{url}")
case Req.get(url, decode_body: false, headers: full_headers) do
{:ok, %Response{status: status, body: body, headers: response_headers}}
when status in 200..299 ->
content_type = get_content_type(response_headers)
if should_json_decode?(content_type) do
{:ok, Jason.decode!(body)}
else
# Return raw binary for media files
{:ok, body}
end
{:ok, %Response{status: status, body: body}} ->
Logger.error("HTTP GET request failed with status #{status}: #{inspect(body)}")
{:error, %{status: status, body: body}}
{:error, reason} ->
Logger.error("HTTP GET request failed: #{inspect(reason)}")
{:error, reason}
end
end
# Helper functions
defp get_content_type(headers) do
headers
|> Enum.find(fn {key, _value} -> String.downcase(key) == "content-type" end)
|> case do
{_key, content_type} when is_list(content_type) -> List.first(content_type)
{_key, content_type} -> content_type
nil -> "application/octet-stream"
end
end
defp should_json_decode?(content_type) do
content_type
|> String.downcase()
|> String.contains?(["application/json", "text/json"])
end
@doc """
Sends a DELETE request to the specified endpoint with the given parameters.
"""
def delete(endpoint, params \\ %{}, opts \\ [], url_override \\ false) do
config_opts = config(opts)
token = config(config_opts) |> Keyword.get(:token)
if token == "" do
raise ArgumentError, "Missing App Token"
end
url =
if url_override do
"#{base_url(config_opts)}/#{api_version(config_opts)}/#{endpoint}"
else
phone_number_id = config(config_opts) |> Keyword.get(:phone_number_id)
if phone_number_id == "" do
raise ArgumentError, "phone_number_id must be provided"
end
"#{base_url(config_opts)}/#{api_version(config_opts)}/#{phone_number_id}/#{endpoint}"
end
query_string = URI.encode_query(params)
full_url = if query_string != "", do: "#{url}?#{query_string}", else: url
Logger.info("Constructed DELETE URL: #{full_url}")
case Req.delete(full_url, headers: headers(token)) do
{:ok, %Response{status: status, body: body}} when status in 200..299 ->
{:ok, Jason.decode!(body)}
{:ok, %Response{status: status, body: body}} ->
Logger.error(
"[WHATSAPP_ELIXIR] HTTP DELETE failed with status #{status}: #{inspect(body)}"
)
{:error, body}
{:error, reason} ->
Logger.error("[WHATSAPP_ELIXIR] HTTP DELETE request failed: #{inspect(reason)}")
{:error, reason}
end
end
defp headers(token, content_type \\ "application/json") do
[
{"Content-Type", content_type},
{"Authorization", "Bearer #{token}"}
]
end
end