lib/glific/conversations_group.ex

defmodule Glific.ConversationsGroup do
  @moduledoc """
  The main Glific abstraction that exposes the group conversation data in a stuctured manner as a set
  of conversations. For now each group is associated with a set of outgoing messages
  We will keep the API simple for now, but it is likely to become similar to the contact conversations
  API
  """

  use Ecto.Schema

  import Ecto.Query, warn: false

  alias Glific.{
    Conversations,
    Conversations.Conversation,
    Groups,
    Groups.Group,
    Messages.Message,
    Repo
  }

  @doc """
  Returns the last M conversations, each conversation not more than N messages
  """
  @spec list_conversations(list() | nil, map()) :: list() | integer
  def list_conversations(group_ids, args) do
    group_ids
    |> get_groups(args.contact_opts)
    |> get_conversations(args.message_opts)
  end

  @spec get_groups_query(map()) :: Ecto.Queryable.t()
  defp get_groups_query(%{limit: limit, offset: offset}) do
    Group
    |> Ecto.Queryable.to_query()
    |> Repo.add_permission(&Groups.add_permission/2)
    |> order_by([g], desc: g.last_communication_at)
    |> limit(^limit)
    |> offset(^offset)
  end

  @spec get_groups(list() | nil, map()) :: [Group.t()]
  defp get_groups(nil, opts) do
    get_groups_query(opts)
    |> Repo.all()
  end

  defp get_groups(gids, opts) when is_list(gids) do
    get_groups_query(opts)
    |> where([g], g.id in ^gids)
    |> Repo.all()
  end

  @spec get_conversations([Group.t()], map()) :: [Conversation.t()]
  defp get_conversations(groups, message_opts) do
    groups
    |> Enum.map(fn g -> g.id end)
    |> get_messages(message_opts)
    |> make_conversations(groups)
  end

  @spec get_messages(list(), map()) :: [Message.t()]
  defp get_messages(ids, %{limit: limit, offset: offset}) do
    query = from m in Message, as: :m

    query
    |> join(:inner, [m: m], c in Group, as: :c, on: c.id == m.group_id)
    |> where([m: m], m.group_id in ^ids and m.receiver_id == m.sender_id)
    |> Conversations.add_special_offset(length(ids), limit, offset)
    |> order_by([m], desc: m.inserted_at)
    |> Repo.all()
  end

  @spec make_conversations([Message.t()], [Group.t()]) :: [Conversation.t()]
  defp make_conversations(messages, groups) do
    conversations =
      groups
      |> Enum.reduce(
        %{},
        fn g, acc -> Map.put(acc, g.id, %{group: g, messages: []}) end
      )

    conversations =
      Enum.reduce(
        messages,
        conversations,
        fn m, acc ->
          Map.update!(acc, m.group_id, fn l -> %{group: l.group, messages: [m | l.messages]} end)
        end
      )
      |> Enum.map(fn {group_id, c} -> {group_id, Map.update!(c, :messages, &Enum.reverse/1)} end)
      |> Enum.into(%{})

    Enum.map(
      groups,
      fn group ->
        c = Map.get(conversations, group.id)
        Conversation.new(nil, c.group, c.messages)
      end
    )
  end
end