mobilizon/lib/graphql/resolvers/group.ex

461 lines
14 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.Group do
@moduledoc """
Handles the group-related GraphQL calls.
"""
import Mobilizon.Users.Guards
alias Mobilizon.Config
alias Mobilizon.{Actors, Events}
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.GraphQL.API
alias Mobilizon.Users.{User, UserRole}
alias Mobilizon.Web.Upload
import Mobilizon.Web.Gettext
require Logger
@spec find_group(
any,
%{:preferred_username => binary, optional(any) => any},
Absinthe.Resolution.t()
) ::
{:error, :group_not_found} | {:ok, Actor.t()}
@doc """
Find a group
"""
def find_group(
parent,
%{preferred_username: name} = args,
%{
context: %{
current_actor: %Actor{id: actor_id}
}
}
) do
case ActivityPubActor.find_or_make_group_from_nickname(name) do
{:ok, %Actor{id: group_id, suspended: false} = group} ->
if Actors.is_member?(actor_id, group_id) do
{:ok, group}
else
find_group(parent, args, nil)
end
{:ok, %Actor{}} ->
{:error, :group_not_found}
{:error, _err} ->
{:error, :group_not_found}
end
end
def find_group(_parent, %{preferred_username: name}, _resolution) do
case ActivityPubActor.find_or_make_group_from_nickname(name) do
{:ok, %Actor{suspended: false} = actor} ->
%Actor{} = actor = restrict_fields_for_non_member_request(actor)
{:ok, actor}
{:ok, %Actor{}} ->
{:error, :group_not_found}
{:error, _err} ->
{:error, :group_not_found}
end
end
@doc """
Get a group
"""
@spec get_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def get_group(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
case Actors.get_actor_with_preload(id, true) do
%Actor{type: :Group, suspended: suspended} = actor ->
if suspended == false or is_moderator(role) do
{:ok, actor}
else
{:error, dgettext("errors", "Group with ID %{id} not found", id: id)}
end
nil ->
{:error, dgettext("errors", "Group with ID %{id} not found", id: id)}
end
end
@doc """
Lists all groups
"""
@spec list_groups(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Actor.t())} | {:error, String.t()}
def list_groups(
_parent,
%{
preferred_username: preferred_username,
name: name,
domain: domain,
local: local,
suspended: suspended,
page: page,
limit: limit
},
%{
context: %{current_user: %User{role: role}}
}
)
when is_moderator(role) do
{:ok,
Actors.list_actors(:Group, preferred_username, name, domain, local, suspended, page, limit)}
end
def list_groups(_parent, _args, _resolution),
do: {:error, dgettext("errors", "You may not list groups unless moderator.")}
# TODO Move me to somewhere cleaner
@spec save_attached_pictures(map()) :: map()
defp save_attached_pictures(args) do
Enum.reduce([:avatar, :banner], args, fn key, args ->
if is_map(args) && Map.has_key?(args, key) && !is_nil(args[key][:media]) do
pic = args[key][:media]
with {:ok, %{name: name, url: url, content_type: content_type, size: _size}} <-
Upload.store(pic.file, type: key, description: pic.alt) do
Map.put(args, key, %{"name" => name, "url" => url, "mediaType" => content_type})
end
else
args
end
end)
end
@doc """
Create a new group. The creator is automatically added as admin
"""
@spec create_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def create_group(
_parent,
args,
%{
context: %{
current_actor: %Actor{id: creator_actor_id} = creator_actor,
current_user: %User{role: role} = _resolution
}
}
) do
if can_create_group?(role) do
args =
args
|> Map.update(:preferred_username, "", &String.downcase/1)
|> Map.put(:creator_actor, creator_actor)
|> Map.put(:creator_actor_id, creator_actor_id)
with {:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
{:ok, _activity, %Actor{type: :Group} = group} <-
API.Groups.create_group(args) do
{:ok, group}
else
{:picture, {:error, :file_too_large}} ->
{:error, dgettext("errors", "The provided picture is too heavy")}
{:error, err} when is_binary(err) ->
{:error, err}
end
else
{:error, dgettext("errors", "Only admins can create groups")}
end
end
def create_group(_parent, _args, _resolution) do
{:error, "You need to be logged-in to create a group"}
end
@spec can_create_group?(UserRole.t()) :: boolean()
defp can_create_group?(role) do
if Config.only_admin_can_create_groups?() do
is_admin(role)
else
true
end
end
@doc """
Update a group. The creator is automatically added as admin
"""
@spec update_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def update_group(
_parent,
%{id: group_id} = args,
%{
context: %{
current_actor: %Actor{} = updater_actor
}
}
) do
if Actors.is_administrator?(updater_actor.id, group_id) do
args = Map.put(args, :updater_actor, updater_actor)
case save_attached_pictures(args) do
{:error, :file_too_large} ->
{:error, dgettext("errors", "The provided picture is too heavy")}
map when is_map(map) ->
case API.Groups.update_group(args) do
{:ok, _activity, %Actor{type: :Group} = group} ->
{:ok, group}
{:error, _err} ->
{:error, dgettext("errors", "Failed to update the group")}
end
end
else
{:error, dgettext("errors", "Profile is not administrator for the group")}
end
end
def update_group(_parent, _args, _resolution) do
{:error, dgettext("errors", "You need to be logged-in to update a group")}
end
@doc """
Delete an existing group
"""
@spec delete_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, %{id: integer()}} | {:error, String.t()}
def delete_group(
_parent,
%{group_id: group_id},
%{
context: %{
current_actor: %Actor{id: actor_id} = actor
}
}
) do
with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
{:is_admin, true} <- {:is_admin, Member.is_administrator(member)},
{:ok, _activity, group} <- Actions.Delete.delete(group, actor, true) do
{:ok, %{id: group.id}}
else
{:error, :group_not_found} ->
{:error, dgettext("errors", "Group not found")}
{:error, :member_not_found} ->
{:error, dgettext("errors", "Current profile is not a member of this group")}
{:is_admin, false} ->
{:error,
dgettext("errors", "Current profile is not an administrator of the selected group")}
end
end
def delete_group(_parent, _args, _resolution) do
{:error, dgettext("errors", "You need to be logged-in to delete a group")}
end
@doc """
Join an existing group
"""
@spec join_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def join_group(_parent, %{group_id: group_id} = args, %{
context: %{current_actor: %Actor{} = actor}
}) do
with {:ok, %Actor{type: :Group} = group} <-
Actors.get_group_by_actor_id(group_id),
{:error, :member_not_found} <- Actors.get_member(actor.id, group.id),
{:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
{:ok, _activity, %Member{} = member} <-
Actions.Join.join(group, actor, true, args) do
{:ok, member}
else
{:error, :group_not_found} ->
{:error, dgettext("errors", "Group not found")}
{:is_able_to_join, false} ->
{:error, dgettext("errors", "You cannot join this group")}
{:ok, %Member{}} ->
{:error, dgettext("errors", "You are already a member of this group")}
end
end
def join_group(_parent, _args, _resolution) do
{:error, dgettext("errors", "You need to be logged-in to join a group")}
end
@doc """
Leave a existing group
"""
@spec leave_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def leave_group(
_parent,
%{group_id: group_id},
%{
context: %{
current_actor: %Actor{} = actor
}
}
) do
with {:group, %Actor{type: :Group} = group} <- {:group, Actors.get_actor(group_id)},
{:ok, _activity, %Member{} = member} <-
Actions.Leave.leave(group, actor, true) do
{:ok, member}
else
{:error, :member_not_found} ->
{:error, dgettext("errors", "Member not found")}
{:group, nil} ->
{:error, dgettext("errors", "Group not found")}
{:error, :is_not_only_admin} ->
{:error,
dgettext("errors", "You can't leave this group because you are the only administrator")}
end
end
def leave_group(_parent, _args, _resolution) do
{:error, dgettext("errors", "You need to be logged-in to leave a group")}
end
@doc """
Follow a group
"""
@spec follow_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Follower.t()} | {:error, String.t()}
def follow_group(_parent, %{group_id: group_id, notify: _notify}, %{
context: %{current_actor: %Actor{} = actor}
}) do
case Actors.get_actor(group_id) do
%Actor{type: :Group} = group ->
case Actions.Follow.follow(actor, group) do
{:ok, _activity, %Follower{} = follower} ->
{:ok, follower}
{:error, :already_following} ->
{:error, dgettext("errors", "You are already following this group")}
end
nil ->
{:error, dgettext("errors", "Group not found")}
end
end
def follow_group(_parent, _args, _resolution) do
{:error, dgettext("errors", "You need to be logged-in to follow a group")}
end
@doc """
Update a group follow
"""
@spec update_group_follow(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def update_group_follow(_parent, %{follow_id: follow_id, notify: notify}, %{
context: %{current_actor: %Actor{} = actor}
}) do
case Actors.get_follower(follow_id) do
%Follower{} = follower ->
if follower.actor_id == actor.id do
# Update notify
Actors.update_follower(follower, %{notify: notify})
else
{:error, dgettext("errors", "Follow does not match your account")}
end
nil ->
{:error, dgettext("errors", "Follow not found")}
end
end
def update_group_follow(_parent, _args, _resolution) do
{:error, dgettext("errors", "You need to be logged-in to update a group follow")}
end
@doc """
Unfollow a group
"""
@spec unfollow_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Follower.t()} | {:error, String.t()}
def unfollow_group(_parent, %{group_id: group_id}, %{
context: %{current_actor: %Actor{} = actor}
}) do
case Actors.get_actor(group_id) do
%Actor{type: :Group} = group ->
with {:ok, _activity, %Follower{} = follower} <- Actions.Follow.unfollow(actor, group) do
{:ok, follower}
end
nil ->
{:error, dgettext("errors", "Group not found")}
end
end
def unfollow_group(_parent, _args, _resolution) do
{:error, dgettext("errors", "You need to be logged-in to unfollow a group")}
end
@spec find_events_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Event.t())}
def find_events_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}
}
}
) do
if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do
# TODO : Handle public / restricted to group members events
{:ok,
Events.list_organized_events_for_group(
group,
:all,
Map.get(args, :after_datetime),
Map.get(args, :before_datetime),
page,
limit
)}
else
find_events_for_group(group, args, nil)
end
end
def find_events_for_group(
%Actor{} = group,
%{
page: page,
limit: limit
} = args,
_resolution
) do
{:ok,
Events.list_organized_events_for_group(
group,
:public,
Map.get(args, :after_datetime),
Map.get(args, :before_datetime),
page,
limit
)}
end
@spec restrict_fields_for_non_member_request(Actor.t()) :: Actor.t()
defp restrict_fields_for_non_member_request(%Actor{} = group) do
%Actor{
group
| followers: [],
followings: [],
organized_events: [],
comments: [],
feed_tokens: []
}
end
end