lib/slack/api.ex

defmodule Slack.API do
  @moduledoc """
  Slack Web API.
  """

  require Logger

  @base_url "https://slack.com/api"

  @doc """
  Req client for Slack API.
  """
  @spec client(String.t()) :: Req.Request.t()
  def client(token) do
    Req.new(base_url: @base_url, auth: {:bearer, token})
  end

  @doc """
  GET from Slack API.
  """
  @spec get(String.t(), String.t(), map() | keyword()) :: {:ok, map()} | {:error, term()}
  def get(endpoint, token, args \\ %{}) do
    result =
      Req.get(client(token),
        url: endpoint,
        params: args
      )

    case result do
      {:ok, %{body: %{"ok" => true} = body}} ->
        {:ok, body}

      {_, error} ->
        Logger.error(inspect(error))
        {:error, error}
    end
  end

  @doc """
  POST to Slack API.
  """
  @spec post(String.t(), String.t(), map() | keyword()) :: {:ok, map()} | {:error, term()}
  def post(endpoint, token, args \\ %{}) do
    result =
      Req.post(client(token),
        url: endpoint,
        form: args
      )

    case result do
      {:ok, %{body: %{"ok" => true} = body}} ->
        {:ok, body}

      {_, error} ->
        Logger.error(inspect(error))
        {:error, error}
    end
  end

  @doc """
  GET pages from Slack API as a `Stream`.

  You can start at a cursor if you pass in `:next_cursor` as one of the `args`.
  Note that it is assumed to be an atom key. If you use a string key you'll
  end up with `next_cursor` parameter twice.
  """
  @spec stream(String.t(), String.t(), String.t(), map() | keyword()) :: Enumerable.t()
  def stream(endpoint, token, resource, args \\ %{}) do
    {starting_cursor, args} =
      args
      |> Map.new()
      |> Map.pop(:next_cursor, nil)

    Stream.resource(
      # start_fun
      fn -> starting_cursor end,

      # next_fun
      fn
        "" ->
          {:halt, nil}

        cursor ->
          case get(endpoint, token, Map.merge(args, %{next_cursor: cursor})) do
            {:ok, %{"ok" => true, ^resource => data} = body} ->
              cursor = get_in(body, ["response_metadata", "next_cursor"]) || ""
              {data, cursor}

            {_, error} ->
              raise "Error fetching #{resource}: #{inspect(error)}"
          end
      end,

      # end_fun
      fn acc ->
        acc
      end
    )
  end
end