Add group admin profiles

And other fixes

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-08-27 11:53:24 +02:00
parent 8afda73214
commit 1984f71cbf
107 changed files with 3514 additions and 1146 deletions

View File

@@ -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}

View File

@@ -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

View File

@@ -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"],

View File

@@ -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

View File

@@ -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 <-

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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")

View File

@@ -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]
}

View File

@@ -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")

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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