defmodule Jido.Chat.EventNormalizer do @moduledoc false alias Jido.Chat.{ ActionEvent, Author, AssistantContextChangedEvent, AssistantThreadStartedEvent, EventEnvelope, Incoming, ModalCloseEvent, ModalSubmitEvent, ReactionEvent, SlashCommandEvent } @spec ensure_incoming(Incoming.t() | map() | term()) :: {:ok, Incoming.t()} | {:error, term()} def ensure_incoming(%Incoming{} = incoming), do: {:ok, incoming} def ensure_incoming(map) when is_map(map), do: {:ok, Incoming.new(map)} def ensure_incoming(other), do: {:error, {:invalid_incoming, other}} @spec ensure_reaction_event(ReactionEvent.t() | map() | term(), atom()) :: {:ok, ReactionEvent.t()} | {:error, term()} def ensure_reaction_event(%ReactionEvent{} = event, _adapter_name), do: {:ok, event} def ensure_reaction_event(map, adapter_name) when is_map(map), do: ensure_event_struct(map, adapter_name, ReactionEvent, :invalid_reaction_event) def ensure_reaction_event(other, _adapter_name), do: {:error, {:invalid_reaction_event, other}} @spec ensure_action_event(ActionEvent.t() | map() | term(), atom()) :: {:ok, ActionEvent.t()} | {:error, term()} def ensure_action_event(%ActionEvent{} = event, _adapter_name), do: {:ok, event} def ensure_action_event(map, adapter_name) when is_map(map), do: ensure_event_struct(map, adapter_name, ActionEvent, :invalid_action_event) def ensure_action_event(other, _adapter_name), do: {:error, {:invalid_action_event, other}} @spec ensure_modal_submit_event(ModalSubmitEvent.t() | map() | term(), atom()) :: {:ok, ModalSubmitEvent.t()} | {:error, term()} def ensure_modal_submit_event(%ModalSubmitEvent{} = event, _adapter_name), do: {:ok, event} def ensure_modal_submit_event(map, adapter_name) when is_map(map), do: ensure_event_struct(map, adapter_name, ModalSubmitEvent, :invalid_modal_submit_event) def ensure_modal_submit_event(other, _adapter_name), do: {:error, {:invalid_modal_submit_event, other}} @spec ensure_modal_close_event(ModalCloseEvent.t() | map() | term(), atom()) :: {:ok, ModalCloseEvent.t()} | {:error, term()} def ensure_modal_close_event(%ModalCloseEvent{} = event, _adapter_name), do: {:ok, event} def ensure_modal_close_event(map, adapter_name) when is_map(map), do: ensure_event_struct(map, adapter_name, ModalCloseEvent, :invalid_modal_close_event) def ensure_modal_close_event(other, _adapter_name), do: {:error, {:invalid_modal_close_event, other}} @spec ensure_slash_command_event(SlashCommandEvent.t() | map() | term(), atom()) :: {:ok, SlashCommandEvent.t()} | {:error, term()} def ensure_slash_command_event(%SlashCommandEvent{} = event, _adapter_name), do: {:ok, event} def ensure_slash_command_event(map, adapter_name) when is_map(map), do: ensure_event_struct(map, adapter_name, SlashCommandEvent, :invalid_slash_command_event) def ensure_slash_command_event(other, _adapter_name), do: {:error, {:invalid_slash_command_event, other}} @spec ensure_assistant_thread_started_event( AssistantThreadStartedEvent.t() | map() | term(), atom() ) :: {:ok, AssistantThreadStartedEvent.t()} | {:error, term()} def ensure_assistant_thread_started_event( %AssistantThreadStartedEvent{} = event, _adapter_name ), do: {:ok, event} def ensure_assistant_thread_started_event(map, adapter_name) when is_map(map) do {:ok, AssistantThreadStartedEvent.new( map |> Map.put_new(:adapter_name, adapter_name) |> Map.put_new(:thread_id, "unknown") )} end def ensure_assistant_thread_started_event(other, _adapter_name), do: {:error, {:invalid_assistant_thread_started_event, other}} @spec ensure_assistant_context_changed_event( AssistantContextChangedEvent.t() | map() | term(), atom() ) :: {:ok, AssistantContextChangedEvent.t()} | {:error, term()} def ensure_assistant_context_changed_event( %AssistantContextChangedEvent{} = event, _adapter_name ), do: {:ok, event} def ensure_assistant_context_changed_event(map, adapter_name) when is_map(map) do {:ok, AssistantContextChangedEvent.new( map |> Map.put_new(:adapter_name, adapter_name) |> Map.put_new(:thread_id, "unknown") )} end def ensure_assistant_context_changed_event(other, _adapter_name), do: {:error, {:invalid_assistant_context_changed_event, other}} @spec ensure_event_envelope(EventEnvelope.t() | map() | term(), atom()) :: {:ok, EventEnvelope.t()} | {:error, term()} def ensure_event_envelope(%EventEnvelope{} = envelope, _adapter_name), do: {:ok, envelope} def ensure_event_envelope(map, adapter_name) when is_map(map) do event_type = map[:event_type] || map["event_type"] || infer_event_type(map[:payload] || map["payload"]) {:ok, EventEnvelope.new( map |> Map.put_new(:adapter_name, adapter_name) |> Map.put_new(:event_type, event_type || :message) |> Map.put_new(:raw, map[:raw] || map["raw"] || %{}) )} end def ensure_event_envelope(other, _adapter_name), do: {:error, {:invalid_event_envelope, other}} @spec thread_id_from(atom(), Incoming.t()) :: String.t() def thread_id_from(adapter_name, %Incoming{} = incoming) do if incoming.external_thread_id do "#{adapter_name}:#{incoming.external_room_id}:#{incoming.external_thread_id}" else "#{adapter_name}:#{incoming.external_room_id}" end end @spec with_envelope_payload(EventEnvelope.t(), term()) :: EventEnvelope.t() def with_envelope_payload(%EventEnvelope{} = envelope, payload) do %{ envelope | payload: payload, thread_id: envelope.thread_id || payload_thread_id(envelope.adapter_name, payload), channel_id: envelope.channel_id || payload_channel_id(payload), message_id: envelope.message_id || payload_message_id(payload) } end defp ensure_event_struct(map, adapter_name, mod, error_tag) when is_map(map) do {:ok, map |> normalize_event_user() |> Map.put_new(:adapter_name, adapter_name) |> mod.new()} rescue _ -> {:error, {error_tag, map}} end defp normalize_event_user(map) when is_map(map) do case map[:user] || map["user"] do %Author{} -> map %{} = user -> normalized_user = %{ user_id: to_string(user[:user_id] || user["user_id"] || user[:id] || user["id"] || "unknown"), user_name: user[:user_name] || user["user_name"] || user[:username] || user["username"] || to_string(user[:id] || user["id"] || "unknown"), full_name: user[:full_name] || user["full_name"] || user[:name] || user["name"] || user[:global_name] || user["global_name"], is_bot: user[:is_bot] || user["is_bot"] || false, is_me: user[:is_me] || user["is_me"] || false } Map.put(map, :user, Author.new(normalized_user)) _ -> map end end defp infer_event_type(nil), do: nil defp infer_event_type(payload) when is_map(payload) do cond do Map.has_key?(payload, :emoji) or Map.has_key?(payload, "emoji") -> :reaction Map.has_key?(payload, :action_id) or Map.has_key?(payload, "action_id") -> :action Map.get(payload, :event_type) == :modal_close or Map.get(payload, "event_type") == "modal_close" -> :modal_close Map.has_key?(payload, :callback_id) or Map.has_key?(payload, "callback_id") -> :modal_submit Map.has_key?(payload, :command) or Map.has_key?(payload, "command") -> :slash_command true -> :message end end defp infer_event_type(_), do: :message defp payload_thread_id(adapter_name, %Incoming{} = incoming) do if incoming.external_thread_id do "#{adapter_name}:#{incoming.external_room_id}:#{incoming.external_thread_id}" else "#{adapter_name}:#{incoming.external_room_id}" end end defp payload_thread_id(_adapter_name, %ReactionEvent{} = payload), do: payload.thread_id defp payload_thread_id(_adapter_name, %ActionEvent{} = payload), do: payload.thread_id defp payload_thread_id(_adapter_name, %ModalSubmitEvent{} = payload), do: payload.thread_id defp payload_thread_id(_adapter_name, %ModalCloseEvent{} = payload), do: payload.thread_id defp payload_thread_id(_adapter_name, %SlashCommandEvent{} = payload), do: payload.thread_id defp payload_thread_id(_adapter_name, %AssistantThreadStartedEvent{} = payload), do: payload.thread_id defp payload_thread_id(_adapter_name, %AssistantContextChangedEvent{} = payload), do: payload.thread_id defp payload_thread_id(_adapter_name, _), do: nil defp payload_channel_id(%Incoming{} = incoming), do: stringify(incoming.external_room_id) defp payload_channel_id(%ReactionEvent{} = payload), do: payload.channel_id defp payload_channel_id(%ActionEvent{} = payload), do: payload.channel_id defp payload_channel_id(%ModalSubmitEvent{} = payload), do: payload.channel_id defp payload_channel_id(%ModalCloseEvent{} = payload), do: payload.channel_id defp payload_channel_id(%SlashCommandEvent{} = payload), do: payload.channel_id defp payload_channel_id(%AssistantThreadStartedEvent{} = payload), do: payload.channel_id defp payload_channel_id(%AssistantContextChangedEvent{} = payload), do: payload.channel_id defp payload_channel_id(_), do: nil defp payload_message_id(%Incoming{} = incoming), do: stringify(incoming.external_message_id) defp payload_message_id(%ReactionEvent{} = payload), do: payload.message_id defp payload_message_id(%ActionEvent{} = payload), do: payload.message_id defp payload_message_id(%ModalSubmitEvent{} = payload), do: payload.message_id defp payload_message_id(%ModalCloseEvent{} = payload), do: payload.message_id defp payload_message_id(%SlashCommandEvent{} = payload), do: payload.message_id defp payload_message_id(%AssistantThreadStartedEvent{} = payload), do: payload.message_id defp payload_message_id(%AssistantContextChangedEvent{} = payload), do: payload.message_id defp payload_message_id(_), do: nil defp stringify(nil), do: nil defp stringify(value) when is_binary(value), do: value defp stringify(value), do: to_string(value) end