Add group admin profiles
And other fixes Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -96,20 +96,26 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
{:existing, entity} ->
|
||||
Logger.debug("Entity is already existing")
|
||||
|
||||
entity =
|
||||
res =
|
||||
if force_fetch and not are_same_origin?(url, Endpoint.url()) do
|
||||
Logger.debug("Entity is external and we want a force fetch")
|
||||
|
||||
with {:ok, _activity, entity} <- Fetcher.fetch_and_update(url, options) do
|
||||
entity
|
||||
case Fetcher.fetch_and_update(url, options) do
|
||||
{:ok, _activity, entity} ->
|
||||
{:ok, entity}
|
||||
|
||||
{:error, "Gone"} ->
|
||||
{:error, "Gone", entity}
|
||||
end
|
||||
else
|
||||
entity
|
||||
{:ok, entity}
|
||||
end
|
||||
|
||||
Logger.debug("Going to preload an existing entity")
|
||||
with {:ok, entity} <- res do
|
||||
Logger.debug("Going to preload an existing entity")
|
||||
|
||||
Preloader.maybe_preload(entity)
|
||||
Preloader.maybe_preload(entity)
|
||||
end
|
||||
|
||||
e ->
|
||||
Logger.warn("Something failed while fetching url #{inspect(e)}")
|
||||
@@ -333,9 +339,9 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
def delete(object, actor, local \\ true) do
|
||||
def delete(object, actor, local \\ true, additional \\ %{}) do
|
||||
with {:ok, activity_data, actor, object} <-
|
||||
Managable.delete(object, actor, local),
|
||||
Managable.delete(object, actor, local, additional),
|
||||
group <- Ownable.group_actor(object),
|
||||
:ok <- check_for_actor_key_rotation(actor),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
@@ -417,12 +423,14 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
%Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url},
|
||||
%Actor{id: actor_id, url: actor_url},
|
||||
local,
|
||||
_additional
|
||||
additional
|
||||
) do
|
||||
with {:member, {:ok, %Member{id: member_id} = member}} <-
|
||||
{:member, Actors.get_member(actor_id, group_id)},
|
||||
{:is_only_admin, false} <-
|
||||
{:is_only_admin, Actors.is_only_administrator?(member_id, group_id)},
|
||||
{:is_not_only_admin, true} <-
|
||||
{:is_not_only_admin,
|
||||
Map.get(additional, :force_member_removal, false) ||
|
||||
!Actors.is_only_administrator?(member_id, group_id)},
|
||||
{:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)},
|
||||
leave_data <- %{
|
||||
"to" => [group_members_url],
|
||||
@@ -639,6 +647,19 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end)
|
||||
end
|
||||
|
||||
defp convert_followers_in_recipients(recipients) do
|
||||
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc ->
|
||||
case Actors.get_group_by_followers_url(recipient) do
|
||||
%Actor{} = group ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.followers_url end),
|
||||
follower_actors ++ Actors.list_external_followers_for_actor(group)}
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# @spec is_announce_activity?(Activity.t()) :: boolean
|
||||
# defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true
|
||||
# defp is_announce_activity?(_), do: false
|
||||
@@ -661,13 +682,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
Relay.publish(activity)
|
||||
end
|
||||
|
||||
{recipients, followers} =
|
||||
if actor.followers_url in activity.recipients do
|
||||
{Enum.filter(recipients, fn recipient -> recipient != actor.followers_url end),
|
||||
Actors.list_external_followers_for_actor(actor)}
|
||||
else
|
||||
{recipients, []}
|
||||
end
|
||||
{recipients, followers} = convert_followers_in_recipients(recipients)
|
||||
|
||||
{recipients, members} = convert_members_in_recipients(recipients)
|
||||
|
||||
@@ -858,7 +873,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
) do
|
||||
with %Actor{} = inviter <- Actors.get_actor(invited_by_id),
|
||||
%Actor{url: actor_url} <- Actors.get_actor(actor_id),
|
||||
{:ok, %Member{url: member_url, id: member_id} = member} <-
|
||||
{:ok, %Member{id: member_id} = member} <-
|
||||
Actors.update_member(member, %{role: :member}),
|
||||
accept_data <- %{
|
||||
"type" => "Accept",
|
||||
@@ -866,7 +881,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
"to" => [inviter.url, member.parent.members_url],
|
||||
"cc" => [member.parent.url],
|
||||
"actor" => actor_url,
|
||||
"object" => member_url,
|
||||
"object" => Convertible.model_to_as(member),
|
||||
"id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}"
|
||||
} do
|
||||
{:ok, member, accept_data}
|
||||
|
||||
@@ -27,6 +27,12 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <-
|
||||
ActivityPubClient.get(client, url) do
|
||||
{:ok, data}
|
||||
else
|
||||
{:ok, %Tesla.Env{status: 410}} ->
|
||||
{:error, "Gone"}
|
||||
|
||||
{:ok, %Tesla.Env{} = res} ->
|
||||
{:error, res}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,6 +53,9 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
{:origin_check, false} ->
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, "Object origin check failed"}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -67,6 +76,9 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
{:origin_check, false} ->
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, "Object origin check failed"}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,11 +3,33 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
Module that provides functions to explore and fetch collections on a group
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Fetcher, Transmogrifier}
|
||||
alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay, Transmogrifier}
|
||||
require Logger
|
||||
|
||||
def refresh_profile(%Actor{domain: nil}), do: {:error, "Can only refresh remote actors"}
|
||||
|
||||
def refresh_profile(%Actor{type: :Group, url: url, id: group_id} = group) do
|
||||
on_behalf_of =
|
||||
case Actors.get_single_group_member_actor(group_id) do
|
||||
%Actor{} = member_actor ->
|
||||
member_actor
|
||||
|
||||
_ ->
|
||||
Relay.get_actor()
|
||||
end
|
||||
|
||||
with :ok <- fetch_group(url, on_behalf_of) do
|
||||
{:ok, group}
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_profile(%Actor{type: :Person, url: url}) do
|
||||
ActivityPub.make_actor_from_url(url)
|
||||
end
|
||||
|
||||
@spec fetch_group(String.t(), Actor.t()) :: :ok
|
||||
def fetch_group(group_url, %Actor{} = on_behalf_of) do
|
||||
with {:ok,
|
||||
@@ -20,14 +42,15 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
discussions_url: discussions_url,
|
||||
events_url: events_url
|
||||
}} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(group_url) do
|
||||
fetch_collection(outbox_url, on_behalf_of)
|
||||
fetch_collection(members_url, on_behalf_of)
|
||||
fetch_collection(resources_url, on_behalf_of)
|
||||
fetch_collection(posts_url, on_behalf_of)
|
||||
fetch_collection(todos_url, on_behalf_of)
|
||||
fetch_collection(discussions_url, on_behalf_of)
|
||||
fetch_collection(events_url, on_behalf_of)
|
||||
ActivityPub.make_actor_from_url(group_url),
|
||||
:ok <- fetch_collection(outbox_url, on_behalf_of),
|
||||
:ok <- fetch_collection(members_url, on_behalf_of),
|
||||
:ok <- fetch_collection(resources_url, on_behalf_of),
|
||||
:ok <- fetch_collection(posts_url, on_behalf_of),
|
||||
:ok <- fetch_collection(todos_url, on_behalf_of),
|
||||
:ok <- fetch_collection(discussions_url, on_behalf_of),
|
||||
:ok <- fetch_collection(events_url, on_behalf_of) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,9 +60,10 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
Logger.debug("Fetching and preparing collection from url")
|
||||
Logger.debug(inspect(collection_url))
|
||||
|
||||
with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of) do
|
||||
Logger.debug("Fetch ok, passing to process_collection")
|
||||
process_collection(data, on_behalf_of)
|
||||
with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of),
|
||||
:ok <- Logger.debug("Fetch ok, passing to process_collection"),
|
||||
:ok <- process_collection(data, on_behalf_of) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,6 +92,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
Logger.debug(inspect(items))
|
||||
|
||||
Enum.each(items, &handling_element/1)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp process_collection(%{"type" => "OrderedCollection", "first" => first}, on_behalf_of)
|
||||
@@ -84,7 +109,15 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
end
|
||||
end
|
||||
|
||||
defp process_collection(_, _), do: :error
|
||||
|
||||
defp handling_element(data) when is_map(data) do
|
||||
id = Map.get(data, "id")
|
||||
|
||||
if id do
|
||||
Mobilizon.Tombstone.delete_uri_tombstone(id)
|
||||
end
|
||||
|
||||
activity = %{
|
||||
"type" => "Create",
|
||||
"to" => data["to"],
|
||||
|
||||
@@ -131,15 +131,18 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
Logger.info("Handle incoming to create a member")
|
||||
|
||||
with object_data when is_map(object_data) <-
|
||||
object |> Converter.Member.as_to_model_data(),
|
||||
{:existing_member, nil} <-
|
||||
{:existing_member, Actors.get_member_by_url(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Member{} = member} <-
|
||||
ActivityPub.join_group(object_data, false) do
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:existing_member, %Member{} = member} ->
|
||||
{:ok, nil, member}
|
||||
object |> Converter.Member.as_to_model_data() do
|
||||
with {:existing_member, nil} <-
|
||||
{:existing_member, Actors.get_member_by_url(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Member{} = member} <-
|
||||
ActivityPub.join_group(object_data, false) do
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:existing_member, %Member{} = member} ->
|
||||
{:ok, %Member{} = member} = Actors.update_member(member, object_data)
|
||||
|
||||
{:ok, nil, member}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -502,7 +505,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
with actor_url <- Utils.get_actor(data),
|
||||
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
|
||||
object_id <- Utils.get_url(object),
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object_id),
|
||||
{:error, "Gone", object} <- ActivityPub.fetch_object_from_url(object_id, force: true),
|
||||
{:origin_check, true} <-
|
||||
{:origin_check,
|
||||
Utils.origin_check_from_id?(actor_url, object_id) ||
|
||||
@@ -515,7 +518,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
:error
|
||||
|
||||
e ->
|
||||
Logger.debug(inspect(e))
|
||||
Logger.error(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,29 +38,55 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
end
|
||||
end
|
||||
|
||||
@public_ap "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
@impl Entity
|
||||
def delete(
|
||||
%Actor{followers_url: followers_url, url: target_actor_url} = target_actor,
|
||||
%Actor{url: actor_url} = actor,
|
||||
local
|
||||
%Actor{
|
||||
followers_url: followers_url,
|
||||
members_url: members_url,
|
||||
url: target_actor_url,
|
||||
type: type,
|
||||
domain: domain
|
||||
} = target_actor,
|
||||
%Actor{url: actor_url, id: author_id} = actor,
|
||||
_local,
|
||||
additionnal
|
||||
) do
|
||||
to = [@public_ap, followers_url]
|
||||
|
||||
{to, cc} =
|
||||
if type == :Group do
|
||||
{to ++ [members_url], [target_actor_url]}
|
||||
else
|
||||
{to, []}
|
||||
end
|
||||
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor_url,
|
||||
"object" => Convertible.model_to_as(target_actor),
|
||||
"id" => target_actor_url <> "/delete",
|
||||
"to" => [followers_url, "https://www.w3.org/ns/activitystreams#Public"]
|
||||
"to" => to,
|
||||
"cc" => cc
|
||||
}
|
||||
|
||||
# We completely delete the actor if activity is remote
|
||||
with {:ok, %Oban.Job{}} <- Actors.delete_actor(target_actor, reserve_username: local) do
|
||||
suspension = Map.get(additionnal, :suspension, false)
|
||||
|
||||
with {:ok, %Oban.Job{}} <-
|
||||
Actors.delete_actor(target_actor,
|
||||
# We completely delete the actor if the actor is remote
|
||||
reserve_username: is_nil(domain),
|
||||
suspension: suspension,
|
||||
author_id: author_id
|
||||
) do
|
||||
{:ok, activity_data, actor, target_actor}
|
||||
end
|
||||
end
|
||||
|
||||
def actor(%Actor{} = actor), do: actor
|
||||
|
||||
def group_actor(%Actor{} = _actor), do: nil
|
||||
def group_actor(%Actor{} = actor), do: actor
|
||||
|
||||
defp prepare_args_for_actor(args) do
|
||||
with preferred_username <-
|
||||
|
||||
@@ -53,8 +53,15 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Comment.t(), Actor.t(), boolean) :: {:ok, Comment.t()}
|
||||
def delete(%Comment{url: url} = comment, %Actor{} = actor, _local) do
|
||||
@spec delete(Comment.t(), Actor.t(), boolean, map()) :: {:ok, Comment.t()}
|
||||
def delete(
|
||||
%Comment{url: url, id: comment_id},
|
||||
%Actor{} = actor,
|
||||
_local,
|
||||
options \\ %{}
|
||||
) do
|
||||
comment = Discussions.get_comment_with_preload(comment_id)
|
||||
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
@@ -63,16 +70,17 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
force_deletion = Map.get(options, :force, false)
|
||||
|
||||
with audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(comment),
|
||||
{:ok, %Comment{} = comment} <- Discussions.delete_comment(comment),
|
||||
# Preload to be sure
|
||||
%Comment{} = comment <- Discussions.get_comment_with_preload(comment.id),
|
||||
{:ok, %Comment{} = updated_comment} <-
|
||||
Discussions.delete_comment(comment, force: force_deletion),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}) do
|
||||
Share.delete_all_by_uri(comment.url)
|
||||
{:ok, Map.merge(activity_data, audience), actor, comment}
|
||||
{:ok, Map.merge(activity_data, audience), actor, updated_comment}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Federation.ActivityPub.Audience
|
||||
alias Mobilizon.Federation.ActivityPub.Types.{Comments, Entity}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Storage.Repo
|
||||
alias Mobilizon.Web.Endpoint
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
|
||||
require Logger
|
||||
@@ -65,28 +64,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Discussion.t(), Actor.t(), boolean) :: {:ok, Discussion.t()}
|
||||
def delete(%Discussion{actor: group, url: url} = discussion, %Actor{} = actor, _local) do
|
||||
stream =
|
||||
discussion.comments
|
||||
|> Enum.map(
|
||||
&Repo.preload(&1, [
|
||||
:actor,
|
||||
:attributed_to,
|
||||
:in_reply_to_comment,
|
||||
:mentions,
|
||||
:origin_comment,
|
||||
:discussion,
|
||||
:tags,
|
||||
:replies
|
||||
])
|
||||
)
|
||||
|> Enum.map(&Map.put(&1, :event, nil))
|
||||
|> Task.async_stream(fn comment -> Comments.delete(comment, actor, nil) end)
|
||||
|
||||
Stream.run(stream)
|
||||
|
||||
with {:ok, %Discussion{}} <- Discussions.delete_discussion(discussion) do
|
||||
@spec delete(Discussion.t(), Actor.t(), boolean, map()) :: {:ok, Discussion.t()}
|
||||
def delete(
|
||||
%Discussion{actor: group, url: url} = discussion,
|
||||
%Actor{} = actor,
|
||||
_local,
|
||||
_additionnal
|
||||
) do
|
||||
with {:ok, _} <- Discussions.delete_discussion(discussion) do
|
||||
# This is just fake
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
|
||||
@@ -35,7 +35,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
|
||||
@callback update(struct :: t(), attrs :: map(), additionnal :: map()) ::
|
||||
{:ok, t(), ActivityStream.t()}
|
||||
|
||||
@callback delete(struct :: t(), Actor.t(), local :: boolean()) ::
|
||||
@callback delete(struct :: t(), Actor.t(), local :: boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), t()}
|
||||
end
|
||||
|
||||
@@ -50,10 +50,10 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
|
||||
"""
|
||||
def update(entity, attrs, additionnal)
|
||||
|
||||
@spec delete(Entity.t(), Actor.t(), boolean()) ::
|
||||
@spec delete(Entity.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Entity.t()}
|
||||
@doc "Deletes an entity and returns the activitystream representation for it"
|
||||
def delete(entity, actor, local)
|
||||
def delete(entity, actor, local, additionnal)
|
||||
end
|
||||
|
||||
defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
|
||||
@@ -68,7 +68,7 @@ end
|
||||
|
||||
defimpl Managable, for: Event do
|
||||
defdelegate update(entity, attrs, additionnal), to: Events
|
||||
defdelegate delete(entity, actor, local), to: Events
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Events
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Event do
|
||||
@@ -78,7 +78,7 @@ end
|
||||
|
||||
defimpl Managable, for: Comment do
|
||||
defdelegate update(entity, attrs, additionnal), to: Comments
|
||||
defdelegate delete(entity, actor, local), to: Comments
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Comments
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Comment do
|
||||
@@ -88,7 +88,7 @@ end
|
||||
|
||||
defimpl Managable, for: Post do
|
||||
defdelegate update(entity, attrs, additionnal), to: Posts
|
||||
defdelegate delete(entity, actor, local), to: Posts
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Posts
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Post do
|
||||
@@ -98,7 +98,7 @@ end
|
||||
|
||||
defimpl Managable, for: Actor do
|
||||
defdelegate update(entity, attrs, additionnal), to: Actors
|
||||
defdelegate delete(entity, actor, local), to: Actors
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Actors
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Actor do
|
||||
@@ -108,7 +108,7 @@ end
|
||||
|
||||
defimpl Managable, for: TodoList do
|
||||
defdelegate update(entity, attrs, additionnal), to: TodoLists
|
||||
defdelegate delete(entity, actor, local), to: TodoLists
|
||||
defdelegate delete(entity, actor, local, additionnal), to: TodoLists
|
||||
end
|
||||
|
||||
defimpl Ownable, for: TodoList do
|
||||
@@ -118,7 +118,7 @@ end
|
||||
|
||||
defimpl Managable, for: Todo do
|
||||
defdelegate update(entity, attrs, additionnal), to: Todos
|
||||
defdelegate delete(entity, actor, local), to: Todos
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Todos
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Todo do
|
||||
@@ -128,7 +128,7 @@ end
|
||||
|
||||
defimpl Managable, for: Resource do
|
||||
defdelegate update(entity, attrs, additionnal), to: Resources
|
||||
defdelegate delete(entity, actor, local), to: Resources
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Resources
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Resource do
|
||||
@@ -138,7 +138,7 @@ end
|
||||
|
||||
defimpl Managable, for: Discussion do
|
||||
defdelegate update(entity, attrs, additionnal), to: Discussions
|
||||
defdelegate delete(entity, actor, local), to: Discussions
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Discussions
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Discussion do
|
||||
@@ -153,5 +153,5 @@ end
|
||||
|
||||
defimpl Managable, for: Member do
|
||||
defdelegate update(entity, attrs, additionnal), to: Members
|
||||
defdelegate delete(entity, actor, local), to: Members
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Members
|
||||
end
|
||||
|
||||
@@ -53,8 +53,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Event.t(), Actor.t(), boolean) :: {:ok, Event.t()}
|
||||
def delete(%Event{url: url} = event, %Actor{} = actor, _local) do
|
||||
@spec delete(Event.t(), Actor.t(), boolean, map()) :: {:ok, Event.t()}
|
||||
def delete(%Event{url: url} = event, %Actor{} = actor, _local, _additionnal) do
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
|
||||
@@ -2,6 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
|
||||
@moduledoc false
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
require Logger
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2]
|
||||
@@ -38,8 +39,16 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
|
||||
end
|
||||
end
|
||||
|
||||
# Delete member is not used, see ActivityPub.leave/4 and ActivityPub.remove/5 instead
|
||||
def delete(_, _, _), do: :error
|
||||
# Used only when a group is suspended
|
||||
def delete(
|
||||
%Member{parent: %Actor{} = group, actor: %Actor{} = actor} = _member,
|
||||
%Actor{},
|
||||
local,
|
||||
_additionnal
|
||||
) do
|
||||
Logger.debug("Deleting a member")
|
||||
ActivityPub.leave(group, actor, local, %{force_member_removal: true})
|
||||
end
|
||||
|
||||
def actor(%Member{actor_id: actor_id}),
|
||||
do: Actors.get_actor(actor_id)
|
||||
|
||||
@@ -69,7 +69,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
|
||||
attributed_to: %Actor{url: group_url}
|
||||
} = post,
|
||||
%Actor{url: actor_url} = actor,
|
||||
_local
|
||||
_local,
|
||||
_additionnal
|
||||
) do
|
||||
activity_data = %{
|
||||
"actor" => actor_url,
|
||||
|
||||
@@ -131,7 +131,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
||||
def delete(
|
||||
%Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource,
|
||||
%Actor{url: actor_url} = actor,
|
||||
_local
|
||||
_local,
|
||||
_additionnal
|
||||
) do
|
||||
Logger.debug("Building Delete Resource activity")
|
||||
|
||||
|
||||
@@ -40,19 +40,20 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(TodoList.t(), Actor.t(), boolean()) ::
|
||||
@spec delete(TodoList.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), TodoList.t()}
|
||||
def delete(
|
||||
%TodoList{url: url, actor: %Actor{url: group_url}} = todo_list,
|
||||
%Actor{url: actor_url} = actor,
|
||||
_local
|
||||
_local,
|
||||
_additionnal
|
||||
) do
|
||||
Logger.debug("Building Delete TodoList activity")
|
||||
|
||||
activity_data = %{
|
||||
"actor" => actor_url,
|
||||
"type" => "Delete",
|
||||
"object" => Convertible.model_to_as(url),
|
||||
"object" => Convertible.model_to_as(todo_list),
|
||||
"id" => url <> "/delete",
|
||||
"to" => [group_url]
|
||||
}
|
||||
|
||||
@@ -44,11 +44,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Todo.t(), Actor.t(), boolean()) :: {:ok, ActivityStream.t(), Actor.t(), Todo.t()}
|
||||
@spec delete(Todo.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Todo.t()}
|
||||
def delete(
|
||||
%Todo{url: url, creator: %Actor{url: group_url}} = todo,
|
||||
%Actor{url: actor_url} = actor,
|
||||
_local
|
||||
_local,
|
||||
_additionnal
|
||||
) do
|
||||
Logger.debug("Building Delete Todo activity")
|
||||
|
||||
|
||||
@@ -143,7 +143,9 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
do_maybe_relay_if_group_activity(object, attributed_to_url)
|
||||
end
|
||||
|
||||
def maybe_relay_if_group_activity(_, _), do: :ok
|
||||
def maybe_relay_if_group_activity(_activity, _attributedTo) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp do_maybe_relay_if_group_activity(object, attributed_to) when is_list(attributed_to),
|
||||
do: do_maybe_relay_if_group_activity(object, hd(attributed_to))
|
||||
|
||||
@@ -135,6 +135,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
def model_to_as(%CommentModel{} = comment) do
|
||||
Convertible.model_to_as(%TombstoneModel{
|
||||
uri: comment.url,
|
||||
actor: comment.actor,
|
||||
inserted_at: comment.deleted_at
|
||||
})
|
||||
end
|
||||
|
||||
@@ -39,6 +39,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
|
||||
"actor" => discussion.creator.url,
|
||||
"attributedTo" => discussion.actor.url,
|
||||
"id" => discussion.url,
|
||||
"publishedAt" => discussion.inserted_at,
|
||||
"context" => discussion.url
|
||||
}
|
||||
end
|
||||
|
||||
@@ -28,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Tombstone do
|
||||
%{
|
||||
"type" => "Tombstone",
|
||||
"id" => tombstone.uri,
|
||||
"actor" => tombstone.actor.url,
|
||||
"actor" => if(tombstone.actor, do: tombstone.actor.url, else: nil),
|
||||
"deleted" => tombstone.inserted_at
|
||||
}
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ defmodule Mobilizon.GraphQL.API.Groups do
|
||||
with preferred_username <-
|
||||
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
|
||||
{:existing_group, nil} <-
|
||||
{:existing_group, Actors.get_local_group_by_title(preferred_username)},
|
||||
{:existing_group, Actors.get_local_actor_by_name(preferred_username)},
|
||||
args <- args |> Map.put(:type, :Group),
|
||||
{:ok, %Activity{} = activity, %Actor{} = group} <-
|
||||
ActivityPub.create(:actor, args, true, %{"actor" => args.creator_actor.url}) do
|
||||
|
||||
104
lib/graphql/resolvers/actor.ex
Normal file
104
lib/graphql/resolvers/actor.ex
Normal file
@@ -0,0 +1,104 @@
|
||||
defmodule Mobilizon.GraphQL.Resolvers.Actor do
|
||||
@moduledoc """
|
||||
Handles the group-related GraphQL calls.
|
||||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
alias Mobilizon.{Actors, Admin, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Refresher
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
require Logger
|
||||
|
||||
def refresh_profile(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
|
||||
when is_admin(role) do
|
||||
case Actors.get_actor(id) do
|
||||
%Actor{domain: domain} = actor when not is_nil(domain) ->
|
||||
Refresher.refresh_profile(actor)
|
||||
{:ok, actor}
|
||||
|
||||
%Actor{} ->
|
||||
{:error, "Only remote actors may be refreshed"}
|
||||
|
||||
_ ->
|
||||
{:error, "No actor found with this ID"}
|
||||
end
|
||||
end
|
||||
|
||||
def suspend_profile(_parent, %{id: id}, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||
{:moderator_actor, Users.get_actor_for_user(user)},
|
||||
%Actor{suspended: false} = actor <- Actors.get_actor_with_preload(id) do
|
||||
case actor do
|
||||
# Suspend a group on this instance
|
||||
%Actor{type: :Group, domain: nil} ->
|
||||
Logger.debug("We're suspending a group on this very instance")
|
||||
ActivityPub.delete(actor, moderator_actor, true, %{suspension: true})
|
||||
Admin.log_action(moderator_actor, "suspend", actor)
|
||||
{:ok, actor}
|
||||
|
||||
# Delete a remote actor
|
||||
%Actor{domain: domain} when not is_nil(domain) ->
|
||||
Logger.debug("We're just deleting a remote instance")
|
||||
Actors.delete_actor(actor, suspension: true)
|
||||
Admin.log_action(moderator_actor, "suspend", actor)
|
||||
{:ok, actor}
|
||||
|
||||
%Actor{domain: nil} ->
|
||||
{:error, "No remote profile found with this ID"}
|
||||
end
|
||||
else
|
||||
{:moderator_actor, nil} ->
|
||||
{:error, "No actor found for the moderator user"}
|
||||
|
||||
%Actor{suspended: true} ->
|
||||
{:error, "Actor already suspended"}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Error while performing background task"}
|
||||
end
|
||||
end
|
||||
|
||||
def suspend_profile(_parent, _args, _resolution) do
|
||||
{:error, "Only moderators and administrators can suspend a profile"}
|
||||
end
|
||||
|
||||
def unsuspend_profile(_parent, %{id: id}, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||
{:moderator_actor, Users.get_actor_for_user(user)},
|
||||
%Actor{suspended: true} = actor <-
|
||||
Actors.get_actor_with_preload(id, true),
|
||||
{:delete_tombstones, {_, nil}} <-
|
||||
{:delete_tombstones, Mobilizon.Tombstone.delete_actor_tombstones(id)},
|
||||
{:ok, %Actor{} = actor} <- Actors.update_actor(actor, %{suspended: false}),
|
||||
{:ok, %Actor{} = actor} <- refresh_if_remote(actor),
|
||||
{:ok, _} <- Admin.log_action(moderator_actor, "unsuspend", actor) do
|
||||
{:ok, actor}
|
||||
else
|
||||
{:moderator_actor, nil} ->
|
||||
{:error, "No actor found for the moderator user"}
|
||||
|
||||
nil ->
|
||||
{:error, "No remote profile found with this ID"}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Error while performing background task"}
|
||||
end
|
||||
end
|
||||
|
||||
def unsuspend_profile(_parent, _args, _resolution) do
|
||||
{:error, "Only moderators and administrators can unsuspend a profile"}
|
||||
end
|
||||
|
||||
@spec refresh_if_remote(Actor.t()) :: {:ok, Actor.t()}
|
||||
defp refresh_if_remote(%Actor{domain: nil} = actor), do: {:ok, actor}
|
||||
defp refresh_if_remote(%Actor{} = actor), do: Refresher.refresh_profile(actor)
|
||||
end
|
||||
@@ -20,8 +20,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
|
||||
{:ok, Discussions.find_discussions_for_actor(group_id)}
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, %Actor{type: :Group} = group} <- Actors.get_group_by_actor_id(group_id) do
|
||||
{:ok, Discussions.find_discussions_for_actor(group)}
|
||||
else
|
||||
{:member, false} ->
|
||||
{:ok, %Page{total: 0, elements: []}}
|
||||
@@ -174,6 +175,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
ActivityPub.delete(discussion, actor) do
|
||||
{:ok, discussion}
|
||||
else
|
||||
{:no_discussion, _} ->
|
||||
{:error, "No discussion with ID #{discussion_id}"}
|
||||
|
||||
{:member, _} ->
|
||||
{:error, "You are not a member of the group the discussion belongs to"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||
Handles the group-related GraphQL calls.
|
||||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
alias Mobilizon.{Actors, Events, Users}
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
@@ -54,13 +55,46 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a group
|
||||
"""
|
||||
def get_group(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
|
||||
with %Actor{type: :Group, suspended: suspended} = actor <-
|
||||
Actors.get_actor_with_preload(id, true),
|
||||
true <- suspended == false or is_moderator(role) do
|
||||
{:ok, actor}
|
||||
else
|
||||
_ ->
|
||||
{:error, "Group with ID #{id} not found"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists all groups
|
||||
"""
|
||||
def list_groups(_parent, %{page: page, limit: limit}, _resolution) do
|
||||
{:ok, Actors.list_groups(page, limit)}
|
||||
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, "You may not list groups unless moderator."}
|
||||
|
||||
@doc """
|
||||
Create a new group. The creator is automatically added as admin
|
||||
"""
|
||||
@@ -127,28 +161,23 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||
"""
|
||||
def delete_group(
|
||||
_parent,
|
||||
%{group_id: group_id, actor_id: actor_id},
|
||||
%{group_id: group_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {actor_id, ""} <- Integer.parse(actor_id),
|
||||
{group_id, ""} <- Integer.parse(group_id),
|
||||
with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
|
||||
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
|
||||
{:is_admin, true} <- {:is_admin, Member.is_administrator(member)},
|
||||
group <- Actors.delete_group!(group) do
|
||||
{:ok, _activity, group} <- ActivityPub.delete(group, actor, true) do
|
||||
{:ok, %{id: group.id}}
|
||||
else
|
||||
{:error, :group_not_found} ->
|
||||
{:error, "Group not found"}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:error, :member_not_found} ->
|
||||
{:error, "Actor id is not a member of this group"}
|
||||
|
||||
@@ -231,7 +260,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||
{:group, nil} ->
|
||||
{:error, "Group not found"}
|
||||
|
||||
{:is_only_admin, true} ->
|
||||
{:is_not_only_admin, false} ->
|
||||
{:error, "You can't leave this group because you are the only administrator"}
|
||||
end
|
||||
end
|
||||
@@ -245,12 +274,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||
_args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_user: %User{role: user_role} = user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
|
||||
{:member, true} <-
|
||||
{:member, 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)}
|
||||
else
|
||||
|
||||
@@ -3,6 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
||||
Handles the member-related GraphQL calls
|
||||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
alias Mobilizon.{Actors, Users}
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
@@ -19,11 +20,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
||||
%Actor{id: group_id} = group,
|
||||
%{page: page, limit: limit, roles: roles},
|
||||
%{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_user: %User{role: user_role} = user}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
|
||||
{:member, true} <-
|
||||
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)} do
|
||||
roles =
|
||||
case roles do
|
||||
"" ->
|
||||
@@ -167,9 +169,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
||||
{:error, :member_not_found} ->
|
||||
true
|
||||
|
||||
err ->
|
||||
require Logger
|
||||
Logger.error(inspect(err))
|
||||
_err ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Admin
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.Participant
|
||||
alias Mobilizon.Storage.Page
|
||||
@@ -321,64 +320,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
end
|
||||
end
|
||||
|
||||
def suspend_profile(_parent, %{id: id}, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||
{:moderator_actor, Users.get_actor_for_user(user)},
|
||||
%Actor{suspended: false} = actor <- Actors.get_remote_actor_with_preload(id),
|
||||
{:ok, _} <- Actors.delete_actor(actor),
|
||||
{:ok, _} <- Admin.log_action(moderator_actor, "suspend", actor) do
|
||||
{:ok, actor}
|
||||
else
|
||||
{:moderator_actor, nil} ->
|
||||
{:error, "No actor found for the moderator user"}
|
||||
|
||||
%Actor{suspended: true} ->
|
||||
{:error, "Actor already suspended"}
|
||||
|
||||
nil ->
|
||||
{:error, "No remote profile found with this ID"}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Error while performing background task"}
|
||||
end
|
||||
end
|
||||
|
||||
def suspend_profile(_parent, _args, _resolution) do
|
||||
{:error, "Only moderators and administrators can suspend a profile"}
|
||||
end
|
||||
|
||||
def unsuspend_profile(_parent, %{id: id}, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||
{:moderator_actor, Users.get_actor_for_user(user)},
|
||||
%Actor{preferred_username: preferred_username, domain: domain} = actor <-
|
||||
Actors.get_remote_actor_with_preload(id, true),
|
||||
{:ok, _} <- Actors.update_actor(actor, %{suspended: false}),
|
||||
{:ok, %Actor{} = actor} <-
|
||||
ActivityPub.make_actor_from_nickname("#{preferred_username}@#{domain}"),
|
||||
{:ok, _} <- Admin.log_action(moderator_actor, "unsuspend", actor) do
|
||||
{:ok, actor}
|
||||
else
|
||||
{:moderator_actor, nil} ->
|
||||
{:error, "No actor found for the moderator user"}
|
||||
|
||||
nil ->
|
||||
{:error, "No remote profile found with this ID"}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Error while performing background task"}
|
||||
end
|
||||
end
|
||||
|
||||
def unsuspend_profile(_parent, _args, _resolution) do
|
||||
{:error, "Only moderators and administrators can unsuspend a profile"}
|
||||
end
|
||||
|
||||
# We check that the actor is not the last administrator/creator of a group
|
||||
@spec last_admin_of_a_group?(integer()) :: boolean()
|
||||
defp last_admin_of_a_group?(actor_id) do
|
||||
|
||||
@@ -3,6 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
||||
Handles the posts-related GraphQL calls
|
||||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
alias Mobilizon.{Actors, Posts, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
@@ -24,12 +25,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
||||
%{page: page, limit: limit} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_user: %User{role: user_role} = user
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:member, true} <-
|
||||
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)},
|
||||
%Page{} = page <- Posts.get_posts_for_group(group, page, limit) do
|
||||
{:ok, page}
|
||||
else
|
||||
|
||||
@@ -175,6 +175,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
import_fields(:discussion_mutations)
|
||||
import_fields(:resource_mutations)
|
||||
import_fields(:post_mutations)
|
||||
import_fields(:actor_mutations)
|
||||
end
|
||||
|
||||
@desc """
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule Mobilizon.GraphQL.Schema.ActorInterface do
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.GraphQL.Resolvers.Actor, as: ActorResolver
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
import_types(Schema.Actors.FollowerType)
|
||||
@@ -59,4 +60,21 @@ defmodule Mobilizon.GraphQL.Schema.ActorInterface do
|
||||
value(:Organization, description: "An ActivityPub Organization")
|
||||
value(:Service, description: "An ActivityPub Service")
|
||||
end
|
||||
|
||||
object :actor_mutations do
|
||||
field :suspend_profile, :deleted_object do
|
||||
arg(:id, non_null(:id), description: "The profile ID to suspend")
|
||||
resolve(&ActorResolver.suspend_profile/3)
|
||||
end
|
||||
|
||||
field :unsuspend_profile, :actor do
|
||||
arg(:id, non_null(:id), description: "The profile ID to unsuspend")
|
||||
resolve(&ActorResolver.unsuspend_profile/3)
|
||||
end
|
||||
|
||||
field :refresh_profile, :actor do
|
||||
arg(:id, non_null(:id))
|
||||
resolve(&ActorResolver.refresh_profile/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -130,11 +130,22 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
object :group_queries do
|
||||
@desc "Get all groups"
|
||||
field :groups, :paginated_group_list do
|
||||
arg(:preferred_username, :string, default_value: "")
|
||||
arg(:name, :string, default_value: "")
|
||||
arg(:domain, :string, default_value: "")
|
||||
arg(:local, :boolean, default_value: true)
|
||||
arg(:suspended, :boolean, default_value: false)
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
resolve(&Group.list_groups/3)
|
||||
end
|
||||
|
||||
@desc "Get a group by its ID"
|
||||
field :get_group, :group do
|
||||
arg(:id, non_null(:id))
|
||||
resolve(&Group.get_group/3)
|
||||
end
|
||||
|
||||
@desc "Get a group by its preferred username"
|
||||
field :group, :group do
|
||||
arg(:preferred_username, non_null(:string))
|
||||
@@ -199,7 +210,6 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
@desc "Delete a group"
|
||||
field :delete_group, :deleted_object do
|
||||
arg(:group_id, non_null(:id))
|
||||
arg(:actor_id, non_null(:id))
|
||||
|
||||
resolve(&Group.delete_group/3)
|
||||
end
|
||||
|
||||
@@ -188,16 +188,6 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
|
||||
resolve(handle_errors(&Person.register_person/3))
|
||||
end
|
||||
|
||||
field :suspend_profile, :deleted_object do
|
||||
arg(:id, :id, description: "The profile ID to suspend")
|
||||
resolve(&Person.suspend_profile/3)
|
||||
end
|
||||
|
||||
field :unsuspend_profile, :person do
|
||||
arg(:id, :id, description: "The profile ID to unsuspend")
|
||||
resolve(&Person.unsuspend_profile/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :person_subscriptions do
|
||||
|
||||
@@ -33,6 +33,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
|
||||
field(:inserted_at, :datetime)
|
||||
field(:updated_at, :datetime)
|
||||
field(:deleted_at, :datetime)
|
||||
field(:published_at, :datetime)
|
||||
end
|
||||
|
||||
@desc "The list of visibility options for a comment"
|
||||
|
||||
@@ -13,13 +13,12 @@ defmodule Mobilizon.Actors do
|
||||
alias Mobilizon.Actors.{Actor, Bot, Follower, Member}
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.{Crypto, Events}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Media.File
|
||||
alias Mobilizon.Service.Workers
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
|
||||
alias Mobilizon.Web.Email.Group
|
||||
alias Mobilizon.Web.Upload
|
||||
|
||||
require Logger
|
||||
@@ -249,23 +248,19 @@ defmodule Mobilizon.Actors do
|
||||
"""
|
||||
@spec upsert_actor(map, boolean) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def upsert_actor(
|
||||
%{keys: keys, name: name, summary: summary, avatar: avatar, banner: banner} = data,
|
||||
data,
|
||||
preload \\ false
|
||||
) do
|
||||
# data =
|
||||
# data
|
||||
# |> Map.put(:avatar, transform_media_file(data.avatar))
|
||||
# |> Map.put(:banner, transform_media_file(data.banner))
|
||||
|
||||
insert =
|
||||
data
|
||||
|> Actor.remote_actor_creation_changeset()
|
||||
|> Repo.insert(
|
||||
on_conflict: [
|
||||
set: [
|
||||
keys: keys,
|
||||
name: name,
|
||||
summary: summary,
|
||||
avatar: transform_media_file(avatar),
|
||||
banner: transform_media_file(banner),
|
||||
last_refreshed_at: DateTime.utc_now()
|
||||
]
|
||||
],
|
||||
on_conflict: {:replace_all_except, [:id, :url, :preferred_username, :domain]},
|
||||
conflict_target: [:url]
|
||||
)
|
||||
|
||||
@@ -282,26 +277,28 @@ defmodule Mobilizon.Actors do
|
||||
end
|
||||
end
|
||||
|
||||
defp transform_media_file(nil), do: nil
|
||||
# defp transform_media_file(nil), do: nil
|
||||
|
||||
defp transform_media_file(file) do
|
||||
file = for({key, val} <- file, into: %{}, do: {String.to_atom(key), val})
|
||||
# defp transform_media_file(file) do
|
||||
# file = for({key, val} <- file, into: %{}, do: {String.to_atom(key), val})
|
||||
|
||||
if is_nil(file) do
|
||||
nil
|
||||
else
|
||||
struct(Mobilizon.Media.File, file)
|
||||
end
|
||||
end
|
||||
# if is_nil(file) do
|
||||
# nil
|
||||
# else
|
||||
# struct(Mobilizon.Media.File, file)
|
||||
# end
|
||||
# end
|
||||
|
||||
@delete_actor_default_options [reserve_username: true]
|
||||
@delete_actor_default_options [reserve_username: true, suspension: false]
|
||||
|
||||
def delete_actor(%Actor{} = actor, options \\ @delete_actor_default_options) do
|
||||
delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
|
||||
|
||||
Workers.Background.enqueue("delete_actor", %{
|
||||
"actor_id" => actor.id,
|
||||
"reserve_username" => Keyword.get(delete_actor_options, :reserve_username, true)
|
||||
"author_id" => Keyword.get(delete_actor_options, :author_id),
|
||||
"reserve_username" => Keyword.get(delete_actor_options, :reserve_username, true),
|
||||
"suspension" => Keyword.get(delete_actor_options, :suspension, false)
|
||||
})
|
||||
end
|
||||
|
||||
@@ -309,11 +306,16 @@ defmodule Mobilizon.Actors do
|
||||
Deletes an actor.
|
||||
"""
|
||||
@spec perform(atom(), Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def perform(:delete_actor, %Actor{} = actor, options \\ @delete_actor_default_options) do
|
||||
def perform(:delete_actor, %Actor{type: type} = actor, options \\ @delete_actor_default_options) do
|
||||
Logger.info("Going to delete actor #{actor.url}")
|
||||
actor = Repo.preload(actor, @actor_preloads)
|
||||
|
||||
delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
|
||||
Logger.debug(inspect(delete_actor_options))
|
||||
|
||||
if type == :Group do
|
||||
delete_eventual_local_members(actor, delete_actor_options)
|
||||
end
|
||||
|
||||
multi =
|
||||
Multi.new()
|
||||
@@ -322,6 +324,31 @@ defmodule Mobilizon.Actors do
|
||||
|> Multi.run(:remove_banner, fn _, _ -> remove_banner(actor) end)
|
||||
|> Multi.run(:remove_avatar, fn _, _ -> remove_avatar(actor) end)
|
||||
|
||||
multi =
|
||||
if type == :Group do
|
||||
multi
|
||||
|> Multi.run(:delete_remote_members, fn _, _ ->
|
||||
delete_group_elements(actor, :remote_members)
|
||||
end)
|
||||
|> Multi.run(:delete_group_organized_events, fn _, _ ->
|
||||
delete_group_elements(actor, :events)
|
||||
end)
|
||||
|> Multi.run(:delete_group_posts, fn _, _ ->
|
||||
delete_group_elements(actor, :posts)
|
||||
end)
|
||||
|> Multi.run(:delete_group_resources, fn _, _ ->
|
||||
delete_group_elements(actor, :resources)
|
||||
end)
|
||||
|> Multi.run(:delete_group_todo_lists, fn _, _ ->
|
||||
delete_group_elements(actor, :todo_lists)
|
||||
end)
|
||||
|> Multi.run(:delete_group_discussions, fn _, _ ->
|
||||
delete_group_elements(actor, :discussions)
|
||||
end)
|
||||
else
|
||||
multi
|
||||
end
|
||||
|
||||
multi =
|
||||
if Keyword.get(delete_actor_options, :reserve_username, true) do
|
||||
Multi.update(multi, :actor, Actor.delete_changeset(actor))
|
||||
@@ -329,6 +356,8 @@ defmodule Mobilizon.Actors do
|
||||
Multi.delete(multi, :actor, actor)
|
||||
end
|
||||
|
||||
Logger.debug("Going to run the transaction")
|
||||
|
||||
case Repo.transaction(multi) do
|
||||
{:ok, %{actor: %Actor{} = actor}} ->
|
||||
{:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}")
|
||||
@@ -357,7 +386,16 @@ defmodule Mobilizon.Actors do
|
||||
@doc """
|
||||
Returns the list of actors.
|
||||
"""
|
||||
@spec list_actors(String.t(), String.t(), boolean, boolean, integer, integer) :: Page.t()
|
||||
@spec list_actors(
|
||||
atom(),
|
||||
String.t(),
|
||||
String.t(),
|
||||
String.t(),
|
||||
boolean,
|
||||
boolean,
|
||||
integer,
|
||||
integer
|
||||
) :: Page.t()
|
||||
def list_actors(
|
||||
type \\ :Person,
|
||||
preferred_username \\ "",
|
||||
@@ -380,12 +418,41 @@ defmodule Mobilizon.Actors do
|
||||
limit
|
||||
) do
|
||||
person_query()
|
||||
|> filter_actors(preferred_username, name, domain, local, suspended)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
def list_actors(
|
||||
:Group,
|
||||
preferred_username,
|
||||
name,
|
||||
domain,
|
||||
local,
|
||||
suspended,
|
||||
page,
|
||||
limit
|
||||
) do
|
||||
group_query()
|
||||
|> filter_actors(preferred_username, name, domain, local, suspended)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@spec filter_actors(Ecto.Query.t(), String.t(), String.t(), String.t(), boolean(), boolean()) ::
|
||||
Ecto.Query.t()
|
||||
defp filter_actors(
|
||||
query,
|
||||
preferred_username,
|
||||
name,
|
||||
domain,
|
||||
local,
|
||||
suspended
|
||||
) do
|
||||
query
|
||||
|> filter_suspended(suspended)
|
||||
|> filter_preferred_username(preferred_username)
|
||||
|> filter_name(name)
|
||||
|> filter_domain(domain)
|
||||
|> filter_remote(local)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
defp filter_preferred_username(query, ""), do: query
|
||||
@@ -406,6 +473,7 @@ defmodule Mobilizon.Actors do
|
||||
defp filter_remote(query, true), do: filter_local(query)
|
||||
defp filter_remote(query, false), do: filter_external(query)
|
||||
|
||||
@spec filter_suspended(Ecto.Query.t(), boolean()) :: Ecto.Query.t()
|
||||
defp filter_suspended(query, true), do: where(query, [a], a.suspended)
|
||||
defp filter_suspended(query, false), do: where(query, [a], not a.suspended)
|
||||
|
||||
@@ -440,6 +508,7 @@ defmodule Mobilizon.Actors do
|
||||
|> actor_by_username_or_name_query(term)
|
||||
|> actors_for_location(args)
|
||||
|> filter_by_types(types)
|
||||
|> filter_suspended(false)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@@ -492,6 +561,13 @@ defmodule Mobilizon.Actors do
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@spec get_group_by_followers_url(String.t()) :: Actor.t()
|
||||
def get_group_by_followers_url(followers_url) do
|
||||
group_query()
|
||||
|> where([q], q.followers_url == ^followers_url)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a group.
|
||||
|
||||
@@ -702,6 +778,28 @@ defmodule Mobilizon.Actors do
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@spec list_local_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_local_members_for_group(
|
||||
%Actor{id: group_id, type: :Group} = _group,
|
||||
page \\ nil,
|
||||
limit \\ nil
|
||||
) do
|
||||
group_id
|
||||
|> group_internal_member_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@spec list_remote_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_remote_members_for_group(
|
||||
%Actor{id: group_id, type: :Group} = _group,
|
||||
page \\ nil,
|
||||
limit \\ nil
|
||||
) do
|
||||
group_id
|
||||
|> group_external_member_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of members for a group.
|
||||
"""
|
||||
@@ -1283,6 +1381,26 @@ defmodule Mobilizon.Actors do
|
||||
|> select([_m, a], a)
|
||||
end
|
||||
|
||||
@spec group_external_member_query(integer()) :: Ecto.Query.t()
|
||||
defp group_external_member_query(group_id) do
|
||||
Member
|
||||
|> where([m], m.parent_id == ^group_id)
|
||||
|> join(:inner, [m], a in Actor, on: m.actor_id == a.id)
|
||||
|> where([_m, a], not is_nil(a.domain))
|
||||
|> preload([m], [:parent, :actor])
|
||||
|> select([m, _a], m)
|
||||
end
|
||||
|
||||
@spec group_internal_member_query(integer()) :: Ecto.Query.t()
|
||||
defp group_internal_member_query(group_id) do
|
||||
Member
|
||||
|> where([m], m.parent_id == ^group_id)
|
||||
|> join(:inner, [m], a in Actor, on: m.actor_id == a.id)
|
||||
|> where([_m, a], is_nil(a.domain))
|
||||
|> preload([m], [:parent, :actor])
|
||||
|> select([m, _a], m)
|
||||
end
|
||||
|
||||
@spec filter_member_role(Ecto.Query.t(), list(atom()) | atom()) :: Ecto.Query.t()
|
||||
def filter_member_role(query, []), do: query
|
||||
|
||||
@@ -1497,4 +1615,72 @@ defmodule Mobilizon.Actors do
|
||||
{:error, res}
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_group_elements(%Actor{type: :Group} = actor, type) do
|
||||
Logger.debug("delete_group_elements #{inspect(type)}")
|
||||
|
||||
method =
|
||||
case type do
|
||||
:remote_members -> &list_remote_members_for_group/3
|
||||
:events -> &Events.list_organized_events_for_group/3
|
||||
:posts -> &Mobilizon.Posts.get_posts_for_group/3
|
||||
:resources -> &Mobilizon.Resources.get_resources_for_group/3
|
||||
:todo_lists -> &Mobilizon.Todos.get_todo_lists_for_group/3
|
||||
:discussions -> &Mobilizon.Discussions.find_discussions_for_actor/3
|
||||
end
|
||||
|
||||
res =
|
||||
actor
|
||||
|> accumulate_paginated_elements(method)
|
||||
|> Enum.map(fn element -> ActivityPub.delete(element, actor, false) end)
|
||||
|
||||
if Enum.all?(res, fn {status, _, _} -> status == :ok end) do
|
||||
Logger.debug("Return OK for all #{to_string(type)}")
|
||||
{:ok, res}
|
||||
else
|
||||
Logger.debug("Something failed #{inspect(res)}")
|
||||
{:error, res}
|
||||
end
|
||||
end
|
||||
|
||||
defp accumulate_paginated_elements(
|
||||
%Actor{} = actor,
|
||||
method,
|
||||
elements \\ [],
|
||||
page \\ 1,
|
||||
limit \\ 10
|
||||
) do
|
||||
Logger.debug("accumulate_paginated_elements")
|
||||
%Page{total: total, elements: new_elements} = page = method.(actor, page, limit)
|
||||
elements = elements ++ new_elements
|
||||
count = length(elements)
|
||||
|
||||
if count < total do
|
||||
accumulate_paginated_elements(actor, method, elements, page + 1, limit)
|
||||
else
|
||||
Logger.debug("Found #{count} group elements to delete")
|
||||
elements
|
||||
end
|
||||
end
|
||||
|
||||
# This one is not in the Multi transaction because it sends activities
|
||||
defp delete_eventual_local_members(%Actor{} = group, options) do
|
||||
suspended? = Keyword.get(options, :suspension, false)
|
||||
|
||||
group
|
||||
|> accumulate_paginated_elements(&list_local_members_for_group/3)
|
||||
|> Enum.map(fn member ->
|
||||
if suspended? do
|
||||
Group.send_group_suspension_notification(member)
|
||||
else
|
||||
with author_id when not is_nil(author_id) <- Keyword.get(options, :author_id),
|
||||
%Actor{} = author <- get_actor(author_id) do
|
||||
Group.send_group_deletion_notification(member, author)
|
||||
end
|
||||
end
|
||||
|
||||
member
|
||||
end)
|
||||
|> Enum.map(fn member -> ActivityPub.delete(member, group, false) end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -94,6 +94,7 @@ defmodule Mobilizon.Discussions do
|
||||
@spec get_comment!(integer | String.t()) :: Comment.t()
|
||||
def get_comment!(id), do: Repo.get!(Comment, id)
|
||||
|
||||
@spec get_comment_with_preload(String.t() | integer() | nil) :: Comment.t() | nil
|
||||
def get_comment_with_preload(nil), do: nil
|
||||
|
||||
def get_comment_with_preload(id) do
|
||||
@@ -214,11 +215,20 @@ defmodule Mobilizon.Discussions do
|
||||
|
||||
But actually just empty the fields so that threads are not broken.
|
||||
"""
|
||||
@spec delete_comment(Comment.t()) :: {:ok, Comment.t()} | {:error, Changeset.t()}
|
||||
def delete_comment(%Comment{} = comment) do
|
||||
comment
|
||||
|> Comment.delete_changeset()
|
||||
|> Repo.update()
|
||||
@spec delete_comment(Comment.t(), Keyword.t()) :: {:ok, Comment.t()} | {:error, Changeset.t()}
|
||||
def delete_comment(%Comment{} = comment, options \\ []) do
|
||||
if Keyword.get(options, :force, false) == false do
|
||||
with {:ok, %Comment{} = comment} <-
|
||||
comment
|
||||
|> Comment.delete_changeset()
|
||||
|> Repo.update(),
|
||||
%Comment{} = comment <- get_comment_with_preload(comment.id) do
|
||||
{:ok, comment}
|
||||
end
|
||||
else
|
||||
comment
|
||||
|> Repo.delete()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -289,8 +299,8 @@ defmodule Mobilizon.Discussions do
|
||||
|> Repo.preload(@discussion_preloads)
|
||||
end
|
||||
|
||||
@spec find_discussions_for_actor(integer, integer | nil, integer | nil) :: Page.t()
|
||||
def find_discussions_for_actor(actor_id, page \\ nil, limit \\ nil) do
|
||||
@spec find_discussions_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def find_discussions_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||
Discussion
|
||||
|> where([c], c.actor_id == ^actor_id)
|
||||
|> preload(^@discussion_preloads)
|
||||
@@ -372,9 +382,13 @@ defmodule Mobilizon.Discussions do
|
||||
Delete a discussion.
|
||||
"""
|
||||
@spec delete_discussion(Discussion.t()) :: {:ok, Discussion.t()} | {:error, Changeset.t()}
|
||||
def delete_discussion(%Discussion{} = discussion) do
|
||||
discussion
|
||||
|> Repo.delete()
|
||||
def delete_discussion(%Discussion{id: discussion_id}) do
|
||||
Multi.new()
|
||||
|> Multi.delete_all(:comments, fn _ ->
|
||||
where(Comment, [c], c.discussion_id == ^discussion_id)
|
||||
end)
|
||||
# |> Multi.delete(:discussion, discussion)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
defp public_comments_for_actor_query(actor_id) do
|
||||
@@ -402,7 +416,4 @@ defmodule Mobilizon.Discussions do
|
||||
|
||||
@spec preload_for_comment(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
defp preload_for_comment(query), do: preload(query, ^@comment_preloads)
|
||||
|
||||
# @spec preload_for_discussion(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
# defp preload_for_discussion(query), do: preload(query, ^@discussion_preloads)
|
||||
end
|
||||
|
||||
@@ -46,13 +46,6 @@ defmodule Mobilizon.Posts do
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
def do_get_posts_for_group(group_id) do
|
||||
Post
|
||||
|> where(attributed_to_id: ^group_id)
|
||||
|> order_by(desc: :inserted_at)
|
||||
|> preload([p], [:author, :attributed_to, :picture])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a post by it's ID
|
||||
"""
|
||||
@@ -144,4 +137,11 @@ defmodule Mobilizon.Posts do
|
||||
defp filter_public(query) do
|
||||
where(query, [p], p.visibility == ^:public and not p.draft)
|
||||
end
|
||||
|
||||
defp do_get_posts_for_group(group_id) do
|
||||
Post
|
||||
|> where(attributed_to_id: ^group_id)
|
||||
|> order_by(desc: :inserted_at)
|
||||
|> preload([p], [:author, :attributed_to, :picture])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule Mobilizon.Tombstone do
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Storage.Repo
|
||||
require Ecto.Query
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
uri: String.t(),
|
||||
@@ -42,4 +43,17 @@ defmodule Mobilizon.Tombstone do
|
||||
def find_tombstone(uri) do
|
||||
Repo.get_by(__MODULE__, uri: uri)
|
||||
end
|
||||
|
||||
@spec delete_actor_tombstones(String.t() | integer()) :: {integer(), nil}
|
||||
def delete_actor_tombstones(actorId) do
|
||||
__MODULE__
|
||||
|> Ecto.Query.where(actor_id: ^actorId)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def delete_uri_tombstone(uri) do
|
||||
__MODULE__
|
||||
|> Ecto.Query.where(uri: ^uri)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule Mobilizon.Web.Email.Group do
|
||||
import Bamboo.Phoenix
|
||||
import Mobilizon.Web.Gettext
|
||||
|
||||
alias Mobilizon.{Actors, Users}
|
||||
alias Mobilizon.{Actors, Config, Users}
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Web.{Email, Gettext}
|
||||
@@ -68,7 +68,7 @@ defmodule Mobilizon.Web.Email.Group do
|
||||
|> assign(:locale, locale)
|
||||
|> assign(:group, group)
|
||||
|> assign(:subject, subject)
|
||||
|> render(:group_removal)
|
||||
|> render(:group_member_removal)
|
||||
|> Email.Mailer.deliver_later()
|
||||
|
||||
:ok
|
||||
@@ -76,4 +76,83 @@ defmodule Mobilizon.Web.Email.Group do
|
||||
end
|
||||
|
||||
# TODO : def send_confirmation_to_inviter()
|
||||
|
||||
@member_roles [:administrator, :moderator, :member]
|
||||
def send_group_suspension_notification(%Member{actor: %Actor{user_id: nil}}), do: :ok
|
||||
|
||||
def send_group_suspension_notification(%Member{role: role}) when role not in @member_roles,
|
||||
do: :ok
|
||||
|
||||
def send_group_suspension_notification(%Member{
|
||||
actor: %Actor{user_id: user_id},
|
||||
parent: %Actor{domain: nil} = group,
|
||||
role: member_role
|
||||
}) do
|
||||
with %User{email: email, locale: locale} <- Users.get_user!(user_id) do
|
||||
Gettext.put_locale(locale)
|
||||
instance = Config.instance_name()
|
||||
|
||||
subject =
|
||||
gettext(
|
||||
"The group %{group} has been suspended on %{instance}",
|
||||
group: group.name,
|
||||
instance: instance
|
||||
)
|
||||
|
||||
Email.base_email(to: email, subject: subject)
|
||||
|> assign(:locale, locale)
|
||||
|> assign(:group, group)
|
||||
|> assign(:role, member_role)
|
||||
|> assign(:subject, subject)
|
||||
|> assign(:instance, instance)
|
||||
|> render(:group_suspension)
|
||||
|> Email.Mailer.deliver_later()
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def send_group_deletion_notification(%Member{actor: %Actor{user_id: nil}}, _author), do: :ok
|
||||
|
||||
def send_group_deletion_notification(%Member{role: role}, _author)
|
||||
when role not in @member_roles,
|
||||
do: :ok
|
||||
|
||||
def send_group_deletion_notification(
|
||||
%Member{
|
||||
actor: %Actor{user_id: user_id, id: actor_id},
|
||||
parent: %Actor{domain: nil} = group,
|
||||
role: member_role
|
||||
},
|
||||
%Actor{id: author_id} = author
|
||||
) do
|
||||
with %User{email: email, locale: locale} <- Users.get_user!(user_id),
|
||||
{:member_not_author, true} <- {:member_not_author, author_id !== actor_id} do
|
||||
Gettext.put_locale(locale)
|
||||
instance = Config.instance_name()
|
||||
|
||||
subject =
|
||||
gettext(
|
||||
"The group %{group} has been deleted on %{instance}",
|
||||
group: group.name,
|
||||
instance: instance
|
||||
)
|
||||
|
||||
Email.base_email(to: email, subject: subject)
|
||||
|> assign(:locale, locale)
|
||||
|> assign(:group, group)
|
||||
|> assign(:role, member_role)
|
||||
|> assign(:subject, subject)
|
||||
|> assign(:instance, instance)
|
||||
|> assign(:author, author)
|
||||
|> render(:group_deletion)
|
||||
|> Email.Mailer.deliver_later()
|
||||
|
||||
:ok
|
||||
else
|
||||
# Skip if it's the author itself
|
||||
{:member_not_author, _} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
51
lib/web/templates/email/group_deletion.html.eex
Normal file
51
lib/web/templates/email/group_deletion.html.eex
Normal file
@@ -0,0 +1,51 @@
|
||||
<!-- HERO -->
|
||||
<tr>
|
||||
<td bgcolor="#474467" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #3A384C; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "The group %{group} was deleted on %{instance}!", group: (@group.name || @group.preferred_username), instance: @instance %>
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
<!-- COPY BLOCK -->
|
||||
<tr>
|
||||
<td bgcolor="#E6E4F4" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<!-- COPY -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0;">
|
||||
<%= gettext "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted.",
|
||||
author: Mobilizon.Actors.Actor.display_name_and_username(@author),
|
||||
group: Mobilizon.Actors.Actor.display_name_and_username(@group) %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
5
lib/web/templates/email/group_deletion.text.eex
Normal file
5
lib/web/templates/email/group_deletion.text.eex
Normal file
@@ -0,0 +1,5 @@
|
||||
<%= gettext "The group %{group} was deleted on %{instance}!", group: (@group.name || @group.preferred_username), instance: @instance %>
|
||||
==
|
||||
<%= gettext "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted.",
|
||||
author: Mobilizon.Actors.Actor.display_name_and_username(@author),
|
||||
group: Mobilizon.Actors.Actor.display_name_and_username(@group) %>
|
||||
66
lib/web/templates/email/group_suspension.html.eex
Normal file
66
lib/web/templates/email/group_suspension.html.eex
Normal file
@@ -0,0 +1,66 @@
|
||||
<!-- HERO -->
|
||||
<tr>
|
||||
<td bgcolor="#474467" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #3A384C; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "The group %{group} has been suspended on %{instance}!", group: (@group.name || @group.preferred_username), instance: @instance %>
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
<!-- COPY BLOCK -->
|
||||
<tr>
|
||||
<td bgcolor="#E6E4F4" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<!-- COPY -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0;">
|
||||
<%= gettext "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group.", group_name: @group.name, group_address: if @group.domain, do: "@#{@group.preferred_username}@#{@group.domain}", else: "@#{@group.preferred_username}" %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<%= if is_nil(@group.domain) do %>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0">
|
||||
<%= gettext "As this group was located on this instance, all of it's data has been irretrievably deleted." %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<% else %>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0">
|
||||
<%= gettext "As this group was located on another instance, it will continue to work for other instances than this one." %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
8
lib/web/templates/email/group_suspension.text.eex
Normal file
8
lib/web/templates/email/group_suspension.text.eex
Normal file
@@ -0,0 +1,8 @@
|
||||
<%= gettext "The group %{group} has been suspended on %{instance}!", group: (@group.name || @group.preferred_username), instance: @instance %>
|
||||
==
|
||||
<%= gettext "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group.", group_name: @group.name, group_address: if @group.domain, do: "@#{@group.preferred_username}@#{@group.domain}", else: "@#{@group.preferred_username}" %>
|
||||
<%= if is_nil(@group.domain) do %>
|
||||
<%= gettext "As this group was located on this instance, all of it's data has been irretrievably deleted." %>
|
||||
<% else %>
|
||||
<%= gettext "As this group was located on another instance, it will continue to work for other instances than this one." %>
|
||||
<% end %>
|
||||
@@ -88,7 +88,7 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
|
||||
end
|
||||
|
||||
defp fetch_collection(:discussions, actor, page) do
|
||||
Discussions.find_discussions_for_actor(actor.id, page)
|
||||
Discussions.find_discussions_for_actor(actor, page)
|
||||
end
|
||||
|
||||
defp fetch_collection(:posts, actor, page) do
|
||||
|
||||
Reference in New Issue
Block a user