lib/slack/socket.ex

defmodule Slack.Socket do
  @moduledoc """
  Slack websocket connection for "Socket Mode."
  """
  use WebSockex

  require Logger

  def start_link(config) do
    state = %{
      app_token: Keyword.fetch!(config, :app_token),
      bot_token: Keyword.fetch!(config, :bot_token),
      bot: Keyword.fetch!(config, :bot)
    }

    {:ok, %{"url" => url}} = Slack.API.post("apps.connections.open", state.app_token)

    Logger.info("[Socket] connecting...")

    WebSockex.start_link(url, __MODULE__, state)
  end

  def handle_frame({:text, msg}, state) do
    case Jason.decode(msg) do
      {:ok, %{"type" => "hello"} = hello} ->
        Logger.info("[Socket] hello: #{inspect(hello)}")
        {:ok, state}

      {:ok, %{"payload" => %{"event" => event}} = msg} ->
        Logger.debug("[Socket] message: #{inspect(msg)}")

        case state.bot.handle_event(event["type"], event) do
          {:reply, response} ->
            Slack.API.post("chat.postMessage", state.bot_token, response)

          :ok ->
            :noop
        end

        {:reply, ack_frame(msg), state}

      _ ->
        Logger.debug("[Socket] Unhandled payload: #{msg}")
        {:ok, state}
    end
  end

  def handle_frame({type, msg}, state) do
    Logger.debug("[Socket] unhandled message type: #{inspect(type)}, msg: #{inspect(msg)}")
    {:ok, state}
  end

  def handle_cast({:send, {type, msg} = frame}, state) do
    IO.puts("Sending #{type} frame with payload: #{msg}")
    {:reply, frame, state}
  end

  defp ack_frame(payload) do
    ack =
      payload
      |> Map.take(["envelope_id"])
      |> Jason.encode!()

    {:text, ack}
  end
end