434 lines
12 KiB
Elixir
434 lines
12 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Pleroma.Web.ActivityPub.Builder do
|
|
@moduledoc """
|
|
This module builds the objects. Meant to be used for creating local objects.
|
|
|
|
This module encodes our addressing policies and general shape of our objects.
|
|
"""
|
|
|
|
alias Pleroma.Emoji
|
|
alias Pleroma.Object
|
|
alias Pleroma.User
|
|
alias Pleroma.Web.ActivityPub.Relay
|
|
alias Pleroma.Web.ActivityPub.Utils
|
|
alias Pleroma.Web.ActivityPub.Visibility
|
|
alias Pleroma.Web.CommonAPI.ActivityDraft
|
|
alias Pleroma.Web.Endpoint
|
|
|
|
require Pleroma.Constants
|
|
|
|
def accept_or_reject(actor, activity, type) do
|
|
data = %{
|
|
"id" => Utils.generate_activity_id(),
|
|
"actor" => actor.ap_id,
|
|
"type" => type,
|
|
"object" => activity.data["id"],
|
|
"to" => [activity.actor]
|
|
}
|
|
|
|
{:ok, data, []}
|
|
end
|
|
|
|
@spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()}
|
|
def reject(actor, rejected_activity) do
|
|
accept_or_reject(actor, rejected_activity, "Reject")
|
|
end
|
|
|
|
@spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()}
|
|
def accept(actor, accepted_activity) do
|
|
accept_or_reject(actor, accepted_activity, "Accept")
|
|
end
|
|
|
|
@spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
|
|
def follow(follower, followed) do
|
|
data = %{
|
|
"id" => Utils.generate_activity_id(),
|
|
"actor" => follower.ap_id,
|
|
"type" => "Follow",
|
|
"object" => followed.ap_id,
|
|
"to" => [followed.ap_id]
|
|
}
|
|
|
|
{:ok, data, []}
|
|
end
|
|
|
|
defp unicode_emoji_react(_object, data, emoji) do
|
|
data
|
|
|> Map.put("content", emoji)
|
|
|> Map.put("type", "EmojiReact")
|
|
end
|
|
|
|
defp add_emoji_content(data, emoji, url) do
|
|
tag = [
|
|
%{
|
|
"id" => url,
|
|
"type" => "Emoji",
|
|
"name" => Emoji.maybe_quote(emoji),
|
|
"icon" => %{
|
|
"type" => "Image",
|
|
"url" => url
|
|
}
|
|
}
|
|
]
|
|
|
|
data
|
|
|> Map.put("content", Emoji.maybe_quote(emoji))
|
|
|> Map.put("type", "EmojiReact")
|
|
|> Map.put("tag", tag)
|
|
end
|
|
|
|
defp remote_custom_emoji_react(
|
|
%{data: %{"reactions" => existing_reactions}},
|
|
data,
|
|
emoji
|
|
) do
|
|
[emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@")
|
|
|
|
matching_reaction =
|
|
Enum.find(
|
|
existing_reactions,
|
|
fn [name, _, url] ->
|
|
if url != nil do
|
|
url = URI.parse(url)
|
|
url.host == instance && name == emoji_code
|
|
end
|
|
end
|
|
)
|
|
|
|
if matching_reaction do
|
|
[name, _, url] = matching_reaction
|
|
add_emoji_content(data, name, url)
|
|
else
|
|
{:error, "Could not react"}
|
|
end
|
|
end
|
|
|
|
defp remote_custom_emoji_react(_object, _data, _emoji) do
|
|
{:error, "Could not react"}
|
|
end
|
|
|
|
defp local_custom_emoji_react(data, emoji) do
|
|
with %{file: path} = emojo <- Emoji.get(emoji) do
|
|
url = "#{Endpoint.url()}#{path}"
|
|
add_emoji_content(data, emojo.code, url)
|
|
else
|
|
_ -> {:error, "Emoji does not exist"}
|
|
end
|
|
end
|
|
|
|
defp custom_emoji_react(object, data, emoji) do
|
|
if String.contains?(emoji, "@") do
|
|
remote_custom_emoji_react(object, data, emoji)
|
|
else
|
|
local_custom_emoji_react(data, emoji)
|
|
end
|
|
end
|
|
|
|
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
|
def emoji_react(actor, object, emoji) do
|
|
with {:ok, data, meta} <- object_action(actor, object) do
|
|
data =
|
|
if Emoji.is_unicode_emoji?(emoji) do
|
|
unicode_emoji_react(object, data, emoji)
|
|
else
|
|
custom_emoji_react(object, data, emoji)
|
|
end
|
|
|
|
{:ok, data, meta}
|
|
end
|
|
end
|
|
|
|
@spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
|
|
def undo(actor, object) do
|
|
{:ok,
|
|
%{
|
|
"id" => Utils.generate_activity_id(),
|
|
"actor" => actor.ap_id,
|
|
"type" => "Undo",
|
|
"object" => object.data["id"],
|
|
"to" => object.data["to"] || [],
|
|
"cc" => object.data["cc"] || []
|
|
}, []}
|
|
end
|
|
|
|
@spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
|
|
def delete(actor, object_id) do
|
|
object = Object.normalize(object_id, fetch: false)
|
|
|
|
user = !object && User.get_cached_by_ap_id(object_id)
|
|
|
|
to =
|
|
case {object, user} do
|
|
{%Object{}, _} ->
|
|
# We are deleting an object, address everyone who was originally mentioned
|
|
(object.data["to"] || []) ++ (object.data["cc"] || [])
|
|
|
|
{_, %User{follower_address: follower_address}} ->
|
|
# We are deleting a user, address the followers of that user
|
|
[follower_address]
|
|
end
|
|
|
|
{:ok,
|
|
%{
|
|
"id" => Utils.generate_activity_id(),
|
|
"actor" => actor.ap_id,
|
|
"object" => object_id,
|
|
"to" => to,
|
|
"type" => "Delete"
|
|
}, []}
|
|
end
|
|
|
|
def create(actor, object, recipients) do
|
|
context =
|
|
if is_map(object) do
|
|
object["context"]
|
|
else
|
|
nil
|
|
end
|
|
|
|
{:ok,
|
|
%{
|
|
"id" => Utils.generate_activity_id(),
|
|
"actor" => actor.ap_id,
|
|
"to" => recipients,
|
|
"object" => object,
|
|
"type" => "Create",
|
|
"published" => DateTime.utc_now() |> DateTime.to_iso8601()
|
|
}
|
|
|> Pleroma.Maps.put_if_present("context", context), []}
|
|
end
|
|
|
|
@spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
|
|
def note(%ActivityDraft{} = draft) do
|
|
data =
|
|
%{
|
|
"type" => "Note",
|
|
"to" => draft.to,
|
|
"cc" => draft.cc,
|
|
"content" => draft.content_html,
|
|
"summary" => draft.summary,
|
|
"sensitive" => draft.sensitive,
|
|
"context" => draft.context,
|
|
"attachment" => draft.attachments,
|
|
"actor" => draft.user.ap_id,
|
|
"tag" => Keyword.values(draft.tags) |> Enum.uniq()
|
|
}
|
|
|> add_in_reply_to(draft.in_reply_to)
|
|
|> add_quote(draft.quote_post)
|
|
|> Map.merge(draft.extra)
|
|
|
|
{:ok, data, []}
|
|
end
|
|
|
|
defp add_in_reply_to(object, nil), do: object
|
|
|
|
defp add_in_reply_to(object, in_reply_to) do
|
|
with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
|
|
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
|
|
else
|
|
_ -> object
|
|
end
|
|
end
|
|
|
|
defp add_quote(object, nil), do: object
|
|
|
|
defp add_quote(object, quote_post) do
|
|
with %Object{} = quote_object <- Object.normalize(quote_post, fetch: false) do
|
|
Map.put(object, "quoteUrl", quote_object.data["id"])
|
|
else
|
|
_ -> object
|
|
end
|
|
end
|
|
|
|
def chat_message(actor, recipient, content, opts \\ []) do
|
|
basic = %{
|
|
"id" => Utils.generate_object_id(),
|
|
"actor" => actor.ap_id,
|
|
"type" => "ChatMessage",
|
|
"to" => [recipient],
|
|
"content" => content,
|
|
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
|
|
"emoji" => Emoji.Formatter.get_emoji_map(content)
|
|
}
|
|
|
|
case opts[:attachment] do
|
|
%Object{data: attachment_data} ->
|
|
{
|
|
:ok,
|
|
Map.put(basic, "attachment", attachment_data),
|
|
[]
|
|
}
|
|
|
|
_ ->
|
|
{:ok, basic, []}
|
|
end
|
|
end
|
|
|
|
def answer(user, object, name) do
|
|
{:ok,
|
|
%{
|
|
"type" => "Answer",
|
|
"actor" => user.ap_id,
|
|
"attributedTo" => user.ap_id,
|
|
"cc" => [object.data["actor"]],
|
|
"to" => [],
|
|
"name" => name,
|
|
"inReplyTo" => object.data["id"],
|
|
"context" => object.data["context"],
|
|
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
|
|
"id" => Utils.generate_object_id()
|
|
}, []}
|
|
end
|
|
|
|
@spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
|
|
def tombstone(actor, id) do
|
|
{:ok,
|
|
%{
|
|
"id" => id,
|
|
"actor" => actor,
|
|
"type" => "Tombstone"
|
|
}, []}
|
|
end
|
|
|
|
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
|
def like(actor, object) do
|
|
with {:ok, data, meta} <- object_action(actor, object) do
|
|
data =
|
|
data
|
|
|> Map.put("type", "Like")
|
|
|
|
{:ok, data, meta}
|
|
end
|
|
end
|
|
|
|
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
|
def update(actor, object) do
|
|
{to, cc} =
|
|
if object["type"] in Pleroma.Constants.actor_types() do
|
|
# User updates, always public
|
|
{[Pleroma.Constants.as_public(), actor.follower_address], []}
|
|
else
|
|
# Status updates, follow the recipients in the object
|
|
{object["to"] || [], object["cc"] || []}
|
|
end
|
|
|
|
{:ok,
|
|
%{
|
|
"id" => Utils.generate_activity_id(),
|
|
"type" => "Update",
|
|
"actor" => actor.ap_id,
|
|
"object" => object,
|
|
"to" => to,
|
|
"cc" => cc
|
|
}, []}
|
|
end
|
|
|
|
@spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
|
|
def block(blocker, blocked) do
|
|
{:ok,
|
|
%{
|
|
"id" => Utils.generate_activity_id(),
|
|
"type" => "Block",
|
|
"actor" => blocker.ap_id,
|
|
"object" => blocked.ap_id,
|
|
"to" => [blocked.ap_id]
|
|
}, []}
|
|
end
|
|
|
|
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
|
|
def announce(actor, object, options \\ []) do
|
|
public? = Keyword.get(options, :public, false)
|
|
|
|
to =
|
|
cond do
|
|
actor.ap_id == Relay.ap_id() ->
|
|
[actor.follower_address]
|
|
|
|
public? and Visibility.is_local_public?(object) ->
|
|
[actor.follower_address, object.data["actor"], Utils.as_local_public()]
|
|
|
|
public? ->
|
|
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
|
|
|
|
true ->
|
|
[actor.follower_address, object.data["actor"]]
|
|
end
|
|
|
|
{:ok,
|
|
%{
|
|
"id" => Utils.generate_activity_id(),
|
|
"actor" => actor.ap_id,
|
|
"object" => object.data["id"],
|
|
"to" => to,
|
|
"context" => object.data["context"],
|
|
"type" => "Announce",
|
|
"published" => Utils.make_date()
|
|
}, []}
|
|
end
|
|
|
|
@spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
|
defp object_action(actor, object) do
|
|
object_actor = User.get_cached_by_ap_id(object.data["actor"])
|
|
|
|
# Address the actor of the object, and our actor's follower collection if the post is public.
|
|
to =
|
|
if Visibility.is_public?(object) do
|
|
[actor.follower_address, object.data["actor"]]
|
|
else
|
|
[object.data["actor"]]
|
|
end
|
|
|
|
# CC everyone who's been addressed in the object, except ourself and the object actor's
|
|
# follower collection
|
|
cc =
|
|
(object.data["to"] ++ (object.data["cc"] || []))
|
|
|> List.delete(actor.ap_id)
|
|
|> List.delete(object_actor.follower_address)
|
|
|
|
{:ok,
|
|
%{
|
|
"id" => Utils.generate_activity_id(),
|
|
"actor" => actor.ap_id,
|
|
"object" => object.data["id"],
|
|
"to" => to,
|
|
"cc" => cc,
|
|
"context" => object.data["context"]
|
|
}, []}
|
|
end
|
|
|
|
@spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
|
def pin(%User{} = user, object) do
|
|
{:ok,
|
|
%{
|
|
"id" => Utils.generate_activity_id(),
|
|
"target" => pinned_url(user.nickname),
|
|
"object" => object.data["id"],
|
|
"actor" => user.ap_id,
|
|
"type" => "Add",
|
|
"to" => [Pleroma.Constants.as_public()],
|
|
"cc" => [user.follower_address]
|
|
}, []}
|
|
end
|
|
|
|
@spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
|
|
def unpin(%User{} = user, object) do
|
|
{:ok,
|
|
%{
|
|
"id" => Utils.generate_activity_id(),
|
|
"target" => pinned_url(user.nickname),
|
|
"object" => object.data["id"],
|
|
"actor" => user.ap_id,
|
|
"type" => "Remove",
|
|
"to" => [Pleroma.Constants.as_public()],
|
|
"cc" => [user.follower_address]
|
|
}, []}
|
|
end
|
|
|
|
defp pinned_url(nickname) when is_binary(nickname) do
|
|
Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
|
|
end
|
|
end
|