defmodule Mobilizon.GraphQL.Resolvers.Post do @moduledoc """ Handles the posts-related GraphQL calls """ import Mobilizon.Users.Guards alias Mobilizon.{Actors, Posts} alias Mobilizon.Actors.Actor alias Mobilizon.Federation.ActivityPub.{Actions, Permission, Utils} alias Mobilizon.Posts.Post alias Mobilizon.Storage.Page alias Mobilizon.Users.User import Mobilizon.Web.Gettext require Logger @public_accessible_visibilities [:public, :unlisted] @doc """ Find posts for group. Returns only if actor requesting is a member of the group """ @spec find_posts_for_group(Actor.t(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(Post.t())} def find_posts_for_group( %Actor{id: group_id} = group, %{page: page, limit: limit} = args, %{ context: %{ current_user: %User{role: user_role}, current_actor: %Actor{id: actor_id} } } = _resolution ) do if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do %Page{} = page = Posts.get_posts_for_group(group, page, limit) {:ok, page} else find_posts_for_group(group, args, nil) end end def find_posts_for_group( %Actor{} = group, %{page: page, limit: limit}, _resolution ) do %Page{} = page = Posts.get_public_posts_for_group(group, page, limit) {:ok, page} end def find_posts_for_group( _group, _args, _resolution ) do {:ok, %Page{total: 0, elements: []}} end @spec get_post(any(), map(), Absinthe.Resolution.t()) :: {:ok, Post.t()} | {:error, :post_not_found} def get_post( parent, %{slug: slug}, %{ context: %{ current_user: %User{role: user_role}, current_actor: %Actor{} = current_profile } } = _resolution ) do with {:post, %Post{attributed_to: %Actor{}} = post} <- {:post, Posts.get_post_by_slug_with_preloads(slug)}, {:member, true} <- {:member, Permission.can_access_group_object?(current_profile, post) or is_moderator(user_role)} do {:ok, post} else {:member, false} -> get_post(parent, %{slug: slug}, nil) {:post, _} -> {:error, :post_not_found} end end def get_post( _parent, %{slug: slug}, _resolution ) do case {:post, Posts.get_post_by_slug_with_preloads(slug)} do {:post, %Post{visibility: visibility, draft: false} = post} when visibility in @public_accessible_visibilities -> {:ok, post} {:post, _} -> {:error, :post_not_found} end end def get_post(_parent, _args, _resolution) do {:error, :post_not_found} end @spec create_post(any(), map(), Absinthe.Resolution.t()) :: {:ok, Post.t()} | {:error, String.t()} def create_post( _parent, %{attributed_to_id: group_id} = args, %{ context: %{ current_actor: %Actor{id: actor_id} } } = _resolution ) do with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, %Actor{} = group <- Actors.get_actor(group_id), args <- Map.update(args, :picture, nil, fn picture -> process_picture(picture, group) end), args <- extract_pictures_from_post_body(args, actor_id), {:ok, _, %Post{} = post} <- Actions.Create.create( :post, args |> Map.put(:author_id, actor_id) |> Map.put(:attributed_to_id, group_id), true, %{} ) do {:ok, post} else {:member, _} -> {:error, dgettext("errors", "Profile is not member of group")} {:error, error} -> {:error, error} end end def create_post(_parent, _args, _resolution) do {:error, dgettext("errors", "You need to be logged-in to create posts")} end @spec update_post(any(), map(), Absinthe.Resolution.t()) :: {:ok, Post.t()} | {:error, String.t()} def update_post( _parent, %{id: id} = args, %{ context: %{ current_actor: %Actor{id: actor_id, url: actor_url} } } = _resolution ) do with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(id)}, {:post, %Post{attributed_to: %Actor{id: group_id} = group} = post} <- {:post, Posts.get_post_with_preloads(id)}, args <- Map.update(args, :picture, nil, fn picture -> process_picture(picture, group) end), args <- extract_pictures_from_post_body(args, actor_id), {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %Post{} = post} <- Actions.Update.update(post, args, true, %{"actor" => actor_url}) do {:ok, post} else {:uuid, :error} -> {:error, dgettext("errors", "Post ID is not a valid ID")} {:post, _} -> {:error, dgettext("errors", "Post doesn't exist")} {:member, _} -> {:error, dgettext("errors", "Profile is not member of group")} end end def update_post(_parent, _args, _resolution) do {:error, dgettext("errors", "You need to be logged-in to update posts")} end @spec delete_post(any(), map(), Absinthe.Resolution.t()) :: {:ok, Post.t()} | {:error, String.t()} def delete_post( _parent, %{id: post_id}, %{ context: %{ current_actor: %Actor{id: actor_id} = actor } } = _resolution ) do with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(post_id)}, {:post, %Post{attributed_to: %Actor{id: group_id}} = post} <- {:post, Posts.get_post_with_preloads(post_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %Post{} = post} <- Actions.Delete.delete(post, actor) do {:ok, post} else {:uuid, :error} -> {:error, dgettext("errors", "Post ID is not a valid ID")} {:post, _} -> {:error, dgettext("errors", "Post doesn't exist")} {:member, _} -> {:error, dgettext("errors", "Profile is not member of group")} end end def delete_post(_parent, _args, _resolution) do {:error, dgettext("errors", "You need to be logged-in to delete posts")} end @spec process_picture(map() | nil, Actor.t()) :: nil | map() defp process_picture(nil, _), do: nil defp process_picture(%{media_id: _picture_id} = args, _), do: args defp process_picture(%{media: media}, %Actor{id: actor_id}) do with uploaded when is_map(uploaded) <- media |> Map.get(:file) |> Utils.make_media_data(description: Map.get(media, :name)) do %{ file: Map.take(uploaded, [:url, :name, :content_type, :size]), metadata: Map.take(uploaded, [:width, :height, :blurhash]), actor_id: actor_id } end end @spec extract_pictures_from_post_body(map(), String.t()) :: map() defp extract_pictures_from_post_body(%{body: body} = args, actor_id) do pictures = Mobilizon.GraphQL.API.Utils.extract_pictures_from_body(body, actor_id) Map.put(args, :media, pictures) end defp extract_pictures_from_post_body(args, _actor_id), do: args end