mobilizon/lib/graphql/resolvers/conversation.ex

278 lines
8.9 KiB
Elixir

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