lib/botlead/client/server.ex

defmodule Botlead.Client.Server do
  @moduledoc """
  State manager for initialized client connections.
  """

  @doc """
  Custom bot factory
  """
  defmacro __using__(_opts) do
    quote do
      use GenServer
      require Logger

      @behaviour Botlead.Client.Behaviour

      @spec start_link(Keyword.t(), String.t(), Keyword.t()) :: {:ok, pid}
      def start_link(global_opts, chat_id, opts) when is_binary(chat_id) do
        opts = Keyword.merge(global_opts, opts)
        GenServer.start_link(__MODULE__, %{chat_id: chat_id, opts: opts}, name: instance(chat_id))
      end

      @impl true
      @spec init(map()) :: {:ok, map()}
      def init(%{chat_id: chat_id, opts: opts}) do
        server = instance(chat_id)
        state = get_initial_state(chat_id, opts)

        bot_server = Keyword.fetch!(opts, :bot_module)
        Process.send(bot_server, {:attach_client, chat_id, self()}, [])

        new_state = Map.put(state, :__opts__, opts)
        execute_callback(new_state, {:client_started, chat_id, opts})

        {:ok, new_state}
      end

      @impl true
      @spec terminate(any(), map()) :: :ok
      def terminate(reason, %{chat_id: chat_id, __opts__: opts} = state) do
        Logger.info(fn -> "Terminated clinet #{chat_id}, #{inspect(reason)}" end)
        bot_server = Keyword.fetch!(opts, :bot_module)
        Process.send(bot_server, {:detach_client, chat_id}, [])
        execute_callback(state, {:client_terminated, reason})
        :ok
      end

      @doc """
      Recieved message from bot.
      """
      @impl true
      def handle_cast({:parse_message, message, opts}, state) do
        conn = message_to_conn(message, state, opts)
        new_state = %{state | conn: conn, scope: conn.scope, path: conn.path}
        execute_callback(new_state, {:parsed_message, conn})
        {:noreply, new_state}
      end

      @doc """
      Message delivery callbacks from bot.
      """
      def handle_cast({:message_delivery_result, action, message}, state) do
        new_state = message_delivered(action, message, state)
        execute_callback(new_state, {:message_delivered, action, message})
        {:noreply, new_state}
      end

      @doc """
      Get call Conn object from session.
      """
      @impl true
      def handle_call({:get_last_conn}, _from, %{conn: conn} = state) do
        {:reply, conn, state}
      end

      @doc """
      Get pid for specific client id.
      """
      @spec get_client_pid(String.t()) :: pid | nil
      def get_client_pid(chat_id) when is_binary(chat_id) do
        server = __MODULE__.instance(chat_id)
        Process.whereis(server)
      end

      @doc """
      Make client connection recieve some new message.
      """
      @spec parse_message(String.t() | pid(), map(), Keyword.t()) :: :ok
      def parse_message(pid_or_chat_id, message, opts \\ [])

      def parse_message(pid, message, opts) when is_pid(pid) do
        GenServer.cast(pid, {:parse_message, message, opts})
      end

      def parse_message(chat_id, message, opts) when is_binary(chat_id) do
        pid = get_client_pid(chat_id)
        parse_message(pid, message, opts)
      end

      @doc """
      Check if client was started for specific client id.
      """
      @spec is_client_started?(String.t()) :: boolean()
      def is_client_started?(chat_id) when is_binary(chat_id) do
        server = get_client_pid(chat_id)
        server != nil and Process.alive?(server)
      end

      @doc """
      Start client instance for chat id.
      """
      @spec connect(pid(), String.t(), Keyword.t()) :: {:ok, pid} | :error
      def connect(bot_server, chat_id, opts \\ []) when is_binary(chat_id) do
        Botlead.Client.Supervisor.start_client(__MODULE__, bot_server, chat_id, opts)
      end

      @doc """
      Remove client instance for chat id.
      """
      @spec disconnect(pid(), String.t()) :: :ok | :error
      def disconnect(bot_server, chat_id) when is_binary(chat_id) do
        pid = get_client_pid(chat_id)

        if pid do
          Botlead.Client.Supervisor.remove_client(bot_server, pid, chat_id)
        else
          :ok
        end
      end

      @doc """
      Send message to a special listener pid if it's defined.
      """
      def execute_callback(state, msg) do
        if Keyword.has_key?(__MODULE__.__info__(:functions), :callback) do
          callback(state, msg)
        else
          :ok
        end
      end
    end
  end
end