mobilizon/lib/graphql/resolvers/member.ex
Thomas Citharel 6eba531c89
Allow group admins to moderate new members
Closes #881

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2021-11-12 17:17:05 +01:00

271 lines
9.1 KiB
Elixir
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule Mobilizon.GraphQL.Resolvers.Member do
@moduledoc """
Handles the member-related GraphQL calls
"""
import Mobilizon.Users.Guards
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Storage.Page
alias Mobilizon.Users.User
import Mobilizon.Web.Gettext
@doc """
Find members for group.
If actor requesting is not part of the group, we only return the number of members, not members
"""
@spec find_members_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Member.t())}
def find_members_for_group(
%Actor{id: group_id} = group,
%{page: page, limit: limit, roles: roles},
%{
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
roles =
case roles do
"" ->
[]
roles ->
roles
|> String.split(",")
|> Enum.map(&String.downcase/1)
|> Enum.map(&String.to_existing_atom/1)
end
%Page{} = page = Actors.list_members_for_group(group, roles, page, limit)
{:ok, page}
else
# Actor is not member of group, fallback to public
%Page{} = page = Actors.list_members_for_group(group)
{:ok, %Page{page | elements: []}}
end
end
def find_members_for_group(%Actor{} = group, _args, _resolution) do
%Page{} = page = Actors.list_members_for_group(group)
{:ok, %Page{page | elements: []}}
end
@spec invite_member(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def invite_member(
_parent,
%{group_id: group_id, target_actor_username: target_actor_username},
%{context: %{current_actor: %Actor{id: actor_id} = actor}}
) do
with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:has_rights_to_invite, {:ok, %Member{role: role}}}
when role in [:moderator, :administrator, :creator] <-
{:has_rights_to_invite, Actors.get_member(actor_id, group_id)},
target_actor_username <-
target_actor_username |> String.trim() |> String.trim_leading("@"),
{:target_actor_username, {:ok, %Actor{id: target_actor_id} = target_actor}} <-
{:target_actor_username,
ActivityPubActor.find_or_make_actor_from_nickname(target_actor_username)},
{:existant, true} <-
{:existant, check_member_not_existant_or_rejected(target_actor_id, group.id)},
{:ok, _activity, %Member{} = member} <-
Actions.Invite.invite(group, actor, target_actor) do
{:ok, member}
else
{:error, :group_not_found} ->
{:error, dgettext("errors", "Group not found")}
{:target_actor_username, _} ->
{:error, dgettext("errors", "Profile invited doesn't exist")}
{:has_rights_to_invite, {:error, :member_not_found}} ->
{:error, dgettext("errors", "You are not a member of this group")}
{:has_rights_to_invite, _} ->
{:error, dgettext("errors", "You cannot invite to this group")}
{:existant, _} ->
{:error, dgettext("errors", "Profile is already a member of this group")}
# Remove me ?
{:ok, %Member{}} ->
{:error, dgettext("errors", "Profile is already a member of this group")}
end
end
@spec accept_invitation(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def accept_invitation(_parent, %{id: member_id}, %{
context: %{current_actor: %Actor{id: actor_id}}
}) do
with %Member{actor: %Actor{id: member_actor_id}} = member <-
Actors.get_member(member_id),
{:is_same_actor, true} <- {:is_same_actor, member_actor_id == actor_id},
{:ok, _activity, %Member{} = member} <-
Actions.Accept.accept(
:invite,
member,
true
) do
{:ok, member}
else
{:is_same_actor, false} ->
{:error, dgettext("errors", "You can't accept this invitation with this profile.")}
end
end
@spec reject_invitation(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def reject_invitation(_parent, %{id: member_id}, %{
context: %{current_actor: %Actor{id: actor_id}}
}) do
with {:invitation_exists, %Member{actor: %Actor{id: member_actor_id}} = member} <-
{:invitation_exists, Actors.get_member(member_id)},
{:is_same_actor, true} <- {:is_same_actor, member_actor_id == actor_id},
{:ok, _activity, %Member{} = member} <-
Actions.Reject.reject(
:invite,
member,
true
) do
{:ok, member}
else
{:is_same_actor, false} ->
{:error, dgettext("errors", "You can't reject this invitation with this profile.")}
{:invitation_exists, _} ->
{:error, dgettext("errors", "This invitation doesn't exist.")}
end
end
def approve_member(_parent, %{member_id: member_id}, %{
context: %{current_actor: %Actor{} = moderator}
}) do
case Actors.get_member(member_id) do
%Member{} = member ->
with {:ok, _activity, %Member{} = member} <-
Actions.Accept.accept(:member, member, true, %{moderator: moderator}) do
{:ok, member}
end
{:error, :member_not_found} ->
{:error, dgettext("errors", "You are not a moderator or admin for this group")}
end
end
# TODO : Maybe remove me ? Remove member with exclude parameter does the same
def reject_member(_parent, %{member_id: member_id}, %{
context: %{current_actor: %Actor{} = moderator}
}) do
case Actors.get_member(member_id) do
%Member{} = member ->
with {:ok, _activity, %Member{} = member} <-
Actions.Reject.reject(:member, member, true, %{moderator: moderator}) do
{:ok, member}
end
{:error, :member_not_found} ->
{:error, dgettext("errors", "You are not a moderator or admin for this group")}
end
end
@spec update_member(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def update_member(_parent, %{member_id: member_id, role: role}, %{
context: %{current_actor: %Actor{} = moderator}
}) do
with %Member{} = member <- Actors.get_member(member_id),
{:ok, _activity, %Member{} = member} <-
Actions.Update.update(member, %{role: role}, true, %{moderator: moderator}) do
{:ok, member}
else
{:error, :member_not_found} ->
{:error, dgettext("errors", "You are not a moderator or admin for this group")}
{:error, :only_admin_left} ->
{:error,
dgettext(
"errors",
"You can't set yourself to a lower member role for this group because you are the only administrator"
)}
end
end
def update_member(_parent, _args, _resolution),
do: {:error, "You must be logged-in to update a member"}
@spec remove_member(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def remove_member(_parent, %{member_id: member_id, exclude: _exclude}, %{
context: %{current_actor: %Actor{id: moderator_id} = moderator}
}) do
case Actors.get_member(member_id) do
nil ->
{:error,
dgettext(
"errors",
"This member does not exist"
)}
%Member{role: :rejected} ->
{:error,
dgettext(
"errors",
"This member already has been rejected."
)}
%Member{parent_id: group_id} = member ->
case Actors.get_member(moderator_id, group_id) do
{:ok, %Member{role: role}} when role in [:moderator, :administrator, :creator] ->
%Actor{type: :Group} = group = Actors.get_actor(group_id)
with {:ok, _activity, %Member{}} <-
Actions.Remove.remove(member, group, moderator, true) do
{:ok, member}
end
{:ok, %Member{}} ->
{:error,
dgettext(
"errors",
"You don't have the role needed to remove this member."
)}
{:error, :member_not_found} ->
{:error,
dgettext(
"errors",
"You don't have the right to remove this member."
)}
end
end
end
def remove_member(_parent, _args, _resolution),
do:
{:error,
dgettext(
"errors",
"You must be logged-in to remove a member"
)}
# Rejected members can be invited again
@spec check_member_not_existant_or_rejected(String.t() | integer, String.t() | integer()) ::
boolean()
defp check_member_not_existant_or_rejected(target_actor_id, group_id) do
case Actors.get_member(target_actor_id, group_id) do
{:ok, %Member{role: :rejected}} ->
true
{:error, :member_not_found} ->
true
_err ->
false
end
end
end