defmodule Mobilizon.GraphQL.Resolvers.Conversation do @moduledoc """ Handles the group-related GraphQL calls. """ alias Mobilizon.{Actors, Conversations} alias Mobilizon.Actors.Actor alias Mobilizon.Conversations.{Conversation, ConversationParticipant, ConversationView} alias Mobilizon.Events.Event alias Mobilizon.GraphQL.API.Comments alias Mobilizon.Storage.Page alias Mobilizon.Users.User alias Mobilizon.Web.Endpoint import Mobilizon.Web.Gettext, only: [dgettext: 2] require Logger @spec find_conversations_for_event(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(ConversationView.t())} | {:error, :unauthenticated} def find_conversations_for_event( %Event{id: event_id, attributed_to_id: attributed_to_id}, %{page: page, limit: limit}, %{ context: %{ current_actor: %Actor{id: actor_id} } } ) when not is_nil(attributed_to_id) do if Actors.is_member?(actor_id, attributed_to_id) do {:ok, event_id |> Conversations.find_conversations_for_event(attributed_to_id, page, limit) |> conversation_participant_to_view()} else {:ok, %Page{total: 0, elements: []}} end end @spec find_conversations_for_event(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(ConversationView.t())} | {:error, :unauthenticated} def find_conversations_for_event( %Event{id: event_id, organizer_actor_id: organizer_actor_id}, %{page: page, limit: limit}, %{ context: %{ current_actor: %Actor{id: actor_id} } } ) do if organizer_actor_id == actor_id do {:ok, event_id |> Conversations.find_conversations_for_event(actor_id, page, limit) |> conversation_participant_to_view()} else {:ok, %Page{total: 0, elements: []}} end end def list_conversations(%Actor{id: actor_id}, %{page: page, limit: limit}, %{ context: %{ current_actor: %Actor{id: _current_actor_id} } }) do {:ok, actor_id |> Conversations.list_conversation_participants_for_actor(page, limit) |> conversation_participant_to_view()} end def list_conversations(%User{id: user_id}, %{page: page, limit: limit}, %{ context: %{ current_actor: %Actor{id: _current_actor_id} } }) do {:ok, user_id |> Conversations.list_conversation_participants_for_user(page, limit) |> conversation_participant_to_view()} end def unread_conversations_count(%Actor{id: actor_id}, _args, %{ context: %{ current_user: %User{} = user } }) do case User.owns_actor(user, actor_id) do {:is_owned, %Actor{}} -> {:ok, Conversations.count_unread_conversation_participants_for_person(actor_id)} _ -> {:error, :unauthorized} end end def get_conversation(_parent, %{id: conversation_participant_id}, %{ context: %{ current_actor: %Actor{id: performing_actor_id} } }) do case Conversations.get_conversation_participant(conversation_participant_id) do nil -> {:error, :not_found} %ConversationParticipant{actor_id: actor_id} = conversation_participant -> if actor_id == performing_actor_id or Actors.is_member?(performing_actor_id, actor_id) do {:ok, conversation_participant_to_view(conversation_participant)} else {:error, :not_found} end end end def get_comments_for_conversation( %ConversationView{origin_comment_id: origin_comment_id, actor_id: conversation_actor_id}, %{page: page, limit: limit}, %{ context: %{ current_actor: %Actor{id: performing_actor_id} } } ) do if conversation_actor_id == performing_actor_id or Actors.is_member?(performing_actor_id, conversation_actor_id) do {:ok, Mobilizon.Discussions.get_comments_in_reply_to_comment_id(origin_comment_id, page, limit)} else {:error, :unauthorized} end end def create_conversation( _parent, %{actor_id: actor_id} = args, %{ context: %{ current_actor: %Actor{} = current_actor } } ) do if authorized_to_reply?( Map.get(args, :conversation_id), Map.get(args, :attributed_to_id), current_actor.id ) do case Comments.create_conversation(args) do {:ok, _activity, %Conversation{} = conversation} -> Absinthe.Subscription.publish( Endpoint, Conversations.count_unread_conversation_participants_for_person(current_actor.id), person_unread_conversations_count: current_actor.id ) conversation_participant_actor = args |> Map.get(:attributed_to_id, actor_id) |> Actors.get_actor() {:ok, conversation_to_view(conversation, conversation_participant_actor)} {:error, :empty_participants} -> {:error, dgettext( "errors", "Conversation needs to mention at least one participant that's not yourself" )} end else Logger.debug( "Actor #{current_actor.id} is not authorized to reply to conversation #{inspect(Map.get(args, :conversation_id))}" ) {:error, :unauthorized} end end def update_conversation(_parent, %{conversation_id: conversation_participant_id, read: read}, %{ context: %{ current_actor: %Actor{id: current_actor_id} } }) do with {:no_participant, %ConversationParticipant{actor_id: actor_id} = conversation_participant} <- {:no_participant, Conversations.get_conversation_participant(conversation_participant_id)}, {:valid_actor, true} <- {:valid_actor, actor_id == current_actor_id or Actors.is_member?(current_actor_id, actor_id)}, {:ok, %ConversationParticipant{} = conversation_participant} <- Conversations.update_conversation_participant(conversation_participant, %{ unread: !read }) do Absinthe.Subscription.publish( Endpoint, Conversations.count_unread_conversation_participants_for_person(actor_id), person_unread_conversations_count: actor_id ) {:ok, conversation_participant_to_view(conversation_participant)} else {:no_participant, _} -> {:error, :not_found} {:valid_actor, _} -> {:error, :unauthorized} end end def delete_conversation(_, _, _), do: :ok defp conversation_participant_to_view(%Page{elements: elements} = page) do %Page{page | elements: Enum.map(elements, &conversation_participant_to_view/1)} end defp conversation_participant_to_view(%ConversationParticipant{} = conversation_participant) do value = conversation_participant |> Map.from_struct() |> Map.merge(Map.from_struct(conversation_participant.conversation)) |> Map.delete(:conversation) |> Map.put( :participants, Enum.map( conversation_participant.conversation.participants, &conversation_participant_to_actor/1 ) ) |> Map.put(:conversation_participant_id, conversation_participant.id) struct(ConversationView, value) end defp conversation_to_view( %Conversation{id: conversation_id} = conversation, %Actor{id: actor_id} = actor, unread \\ true ) do value = conversation |> Map.from_struct() |> Map.put(:actor, actor) |> Map.put(:unread, unread) |> Map.put( :conversation_participant_id, Conversations.get_participant_by_conversation_and_actor(conversation_id, actor_id).id ) struct(ConversationView, value) end defp conversation_participant_to_actor(%Actor{} = actor), do: actor defp conversation_participant_to_actor(%ConversationParticipant{} = conversation_participant), do: conversation_participant.actor @spec authorized_to_reply?(String.t() | nil, String.t() | nil, String.t()) :: boolean() # Not a reply defp authorized_to_reply?(conversation_id, _attributed_to_id, _current_actor_id) when is_nil(conversation_id), do: true # We are authorized to reply if we are one of the participants, or if we a a member of a participant group defp authorized_to_reply?(conversation_id, attributed_to_id, current_actor_id) do case Conversations.get_conversation(conversation_id) do nil -> false %Conversation{participants: participants} -> participant_ids = Enum.map(participants, fn participant -> to_string(participant.id) end) to_string(current_actor_id) in participant_ids or Enum.any?(participant_ids, fn participant_id -> Actors.is_member?(current_actor_id, participant_id) and attributed_to_id == participant_id end) end end end