defmodule Jido.Chat.HandlerDispatch do @moduledoc false alias Jido.Chat.{ ActionEvent, EventNormalizer, Incoming, ModalCloseEvent, ModalSubmitEvent, ReactionEvent, SlashCommandEvent, Thread } @spec process_message(map(), atom(), String.t(), Incoming.t() | map(), (map(), Incoming.t(), String.t() -> Thread.t())) :: {:ok, map(), Incoming.t()} | {:error, term()} def process_message(chat, adapter_name, thread_id, incoming, build_thread) when is_map(chat) and is_atom(adapter_name) and is_binary(thread_id) and is_function(build_thread, 3) do with {:ok, incoming} <- EventNormalizer.ensure_incoming(incoming) do dedupe_key = dedupe_key(adapter_name, incoming) if duplicate?(chat, dedupe_key) do {:ok, chat, incoming} else chat = mark_dedupe(chat, dedupe_key) thread = build_thread.(chat, incoming, thread_id) routed_chat = route_handlers(chat, thread, incoming) {:ok, routed_chat, incoming} end end end @spec run_event_handlers(map(), list(), term()) :: map() def run_event_handlers(chat, handlers, event) when is_map(chat) and is_list(handlers) do handlers |> ordered_handlers() |> Enum.reduce(chat, fn handler, acc -> run_event_handler(acc, handler, event) end) end defp dedupe_key(_adapter_name, %Incoming{external_message_id: nil}), do: nil defp dedupe_key(adapter_name, %Incoming{external_message_id: external_message_id}) do {adapter_name, to_string(external_message_id)} end defp duplicate?(_chat, nil), do: false defp duplicate?(chat, key) do Jido.Chat.duplicate?(chat, key) end defp mark_dedupe(chat, nil), do: chat defp mark_dedupe(chat, key), do: Jido.Chat.mark_dedupe(chat, key) defp route_handlers(chat, %Thread{} = thread, %Incoming{} = incoming) do cond do subscribed?(chat, thread.id) -> run_handlers(chat, chat.handlers.subscribed, thread, incoming) mentioned?(chat, incoming) -> run_handlers(chat, chat.handlers.mention, thread, incoming) true -> run_message_handlers(chat, thread, incoming) end end defp run_message_handlers(chat, %Thread{} = thread, %Incoming{} = incoming) do text = incoming.text || "" Enum.reduce(chat.handlers.message, chat, fn {pattern, handler}, acc -> if Regex.match?(pattern, text) do run_handler(acc, handler, thread, incoming) else acc end end) end defp run_handlers(chat, handlers, %Thread{} = thread, %Incoming{} = incoming) do Enum.reduce(handlers, chat, fn handler, acc -> run_handler(acc, handler, thread, incoming) end) end defp run_handler(chat, handler, %Thread{} = thread, %Incoming{} = incoming) do case :erlang.fun_info(handler, :arity) do {:arity, 3} -> coerce_handler_result(chat, handler.(chat, thread, incoming)) _ -> coerce_handler_result(chat, handler.(thread, incoming)) end end defp run_event_handler(chat, {selector, handler}, event) do if selector_matches?(selector, event) do run_event_handler(chat, handler, event) else chat end end defp run_event_handler(chat, handler, event) do case :erlang.fun_info(handler, :arity) do {:arity, 2} -> coerce_handler_result(chat, handler.(chat, event)) _ -> coerce_handler_result(chat, handler.(event)) end end defp ordered_handlers(handlers) do {specific, catch_all} = Enum.split_with(handlers, &match?({_selector, _handler}, &1)) specific ++ catch_all end defp selector_matches?(:all, _event), do: true defp selector_matches?(selectors, event) when is_list(selectors) do Enum.any?(selectors, &selector_matches?(&1, event)) end defp selector_matches?(%Regex{} = pattern, event) do case selector_value(event) do value when is_binary(value) -> Regex.match?(pattern, value) _ -> false end end defp selector_matches?(selector, event) when is_function(selector, 1), do: selector.(event) defp selector_matches?(selector, event) when is_atom(selector) do selector == selector_value(event) || Atom.to_string(selector) == selector_value(event) end defp selector_matches?(selector, event) when is_binary(selector), do: selector == selector_value(event) defp selector_matches?(_selector, _event), do: false defp selector_value(%ReactionEvent{emoji: emoji}), do: emoji defp selector_value(%ActionEvent{action_id: action_id}), do: action_id defp selector_value(%ModalSubmitEvent{callback_id: callback_id}), do: callback_id defp selector_value(%ModalCloseEvent{callback_id: callback_id}), do: callback_id defp selector_value(%SlashCommandEvent{command: command}), do: command defp selector_value(_event), do: nil defp coerce_handler_result(current_chat, next_chat) when is_map(next_chat) do if Map.get(next_chat, :__struct__) == Jido.Chat do next_chat else current_chat end end defp coerce_handler_result(current_chat, {:ok, next_chat}) when is_map(next_chat) do if Map.get(next_chat, :__struct__) == Jido.Chat do next_chat else current_chat end end defp coerce_handler_result(current_chat, _other), do: current_chat defp mentioned?(_chat, %Incoming{was_mentioned: true}), do: true defp mentioned?(chat, %Incoming{text: text}) when is_binary(text) do mention_regex = ~r/(^|\s)@#{Regex.escape(Map.get(chat, :user_name, "bot"))}\b/i Regex.match?(mention_regex, text) end defp mentioned?(_chat, _incoming), do: false defp subscribed?(chat, thread_id) do Jido.Chat.subscribed?(chat, thread_id) end end