2020-10-12 11:00:50 -06:00
|
|
|
# Pleroma: A lightweight social networking server
|
2023-01-02 13:38:50 -07:00
|
|
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
2020-10-12 11:00:50 -06:00
|
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2019-10-16 08:16:39 -06:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2020-04-20 06:08:54 -06:00
|
|
|
alias Pleroma.Emoji
|
2019-10-23 04:18:05 -06:00
|
|
|
alias Pleroma.Object
|
|
|
|
alias Pleroma.User
|
2020-05-26 03:47:03 -06:00
|
|
|
alias Pleroma.Web.ActivityPub.Relay
|
2019-10-16 08:16:39 -06:00
|
|
|
alias Pleroma.Web.ActivityPub.Utils
|
|
|
|
alias Pleroma.Web.ActivityPub.Visibility
|
2021-08-14 10:01:06 -06:00
|
|
|
alias Pleroma.Web.CommonAPI.ActivityDraft
|
2022-09-04 17:31:41 -06:00
|
|
|
alias Pleroma.Web.Endpoint
|
2019-10-16 08:16:39 -06:00
|
|
|
|
2020-05-20 07:44:37 -06:00
|
|
|
require Pleroma.Constants
|
|
|
|
|
2020-08-12 06:48:51 -06:00
|
|
|
def accept_or_reject(actor, activity, type) do
|
2020-08-11 07:13:07 -06:00
|
|
|
data = %{
|
|
|
|
"id" => Utils.generate_activity_id(),
|
|
|
|
"actor" => actor.ap_id,
|
2020-08-12 06:48:51 -06:00
|
|
|
"type" => type,
|
|
|
|
"object" => activity.data["id"],
|
|
|
|
"to" => [activity.actor]
|
2020-08-11 07:13:07 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
{:ok, data, []}
|
|
|
|
end
|
|
|
|
|
2020-08-12 06:48:51 -06:00
|
|
|
@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
|
|
|
|
|
2020-07-06 07:57:19 -06:00
|
|
|
@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
|
|
|
|
|
2022-09-04 17:31:41 -06:00
|
|
|
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
|
2023-03-02 00:09:13 -07:00
|
|
|
tag = [
|
|
|
|
%{
|
|
|
|
"id" => url,
|
|
|
|
"type" => "Emoji",
|
|
|
|
"name" => Emoji.maybe_quote(emoji),
|
|
|
|
"icon" => %{
|
|
|
|
"type" => "Image",
|
|
|
|
"url" => url
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
2022-09-04 17:31:41 -06:00
|
|
|
data
|
|
|
|
|> Map.put("content", Emoji.maybe_quote(emoji))
|
|
|
|
|> Map.put("type", "EmojiReact")
|
2023-03-02 00:09:13 -07:00
|
|
|
|> Map.put("tag", tag)
|
2022-09-04 17:31:41 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
defp remote_custom_emoji_react(
|
|
|
|
%{data: %{"reactions" => existing_reactions}},
|
|
|
|
data,
|
|
|
|
emoji
|
|
|
|
) do
|
2023-03-02 00:09:13 -07:00
|
|
|
[emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@")
|
2022-09-04 17:31:41 -06:00
|
|
|
|
|
|
|
matching_reaction =
|
|
|
|
Enum.find(
|
|
|
|
existing_reactions,
|
|
|
|
fn [name, _, url] ->
|
2022-12-18 11:52:19 -07:00
|
|
|
if url != nil do
|
|
|
|
url = URI.parse(url)
|
|
|
|
url.host == instance && name == emoji_code
|
|
|
|
end
|
2022-09-04 17:31:41 -06:00
|
|
|
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
|
2023-03-02 00:09:13 -07:00
|
|
|
with %{file: path} = emojo <- Emoji.get(emoji) do
|
2022-09-04 17:31:41 -06:00
|
|
|
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
|
|
|
|
|
2020-05-05 04:11:46 -06:00
|
|
|
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
|
|
|
def emoji_react(actor, object, emoji) do
|
2020-05-08 03:30:31 -06:00
|
|
|
with {:ok, data, meta} <- object_action(actor, object) do
|
2020-05-05 04:11:46 -06:00
|
|
|
data =
|
2022-09-04 17:31:41 -06:00
|
|
|
if Emoji.is_unicode_emoji?(emoji) do
|
|
|
|
unicode_emoji_react(object, data, emoji)
|
|
|
|
else
|
|
|
|
custom_emoji_react(object, data, emoji)
|
|
|
|
end
|
2020-05-05 04:11:46 -06:00
|
|
|
|
|
|
|
{:ok, data, meta}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-05 06:17:47 -06:00
|
|
|
@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
|
|
|
|
|
2020-04-29 11:09:51 -06:00
|
|
|
@spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
|
|
|
|
def delete(actor, object_id) do
|
2021-01-04 05:38:31 -07:00
|
|
|
object = Object.normalize(object_id, fetch: false)
|
2020-04-29 11:09:51 -06:00
|
|
|
|
2020-04-30 07:42:30 -06:00
|
|
|
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
|
2020-04-29 11:09:51 -06:00
|
|
|
|
|
|
|
{:ok,
|
|
|
|
%{
|
|
|
|
"id" => Utils.generate_activity_id(),
|
|
|
|
"actor" => actor.ap_id,
|
|
|
|
"object" => object_id,
|
|
|
|
"to" => to,
|
|
|
|
"type" => "Delete"
|
|
|
|
}, []}
|
|
|
|
end
|
|
|
|
|
2020-04-28 09:29:54 -06:00
|
|
|
def create(actor, object, recipients) do
|
2020-07-01 21:45:19 -06:00
|
|
|
context =
|
|
|
|
if is_map(object) do
|
|
|
|
object["context"]
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
2020-04-09 04:44:20 -06:00
|
|
|
{:ok,
|
|
|
|
%{
|
|
|
|
"id" => Utils.generate_activity_id(),
|
|
|
|
"actor" => actor.ap_id,
|
|
|
|
"to" => recipients,
|
2020-04-28 09:29:54 -06:00
|
|
|
"object" => object,
|
2020-04-20 05:14:59 -06:00
|
|
|
"type" => "Create",
|
|
|
|
"published" => DateTime.utc_now() |> DateTime.to_iso8601()
|
2020-07-01 21:45:19 -06:00
|
|
|
}
|
|
|
|
|> Pleroma.Maps.put_if_present("context", context), []}
|
2020-04-09 04:44:20 -06:00
|
|
|
end
|
|
|
|
|
2021-08-14 10:24:55 -06:00
|
|
|
@spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
|
2021-08-14 10:01:06 -06:00
|
|
|
def note(%ActivityDraft{} = draft) do
|
2021-08-14 10:24:55 -06:00
|
|
|
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)
|
|
|
|
|> Map.merge(draft.extra)
|
|
|
|
|
|
|
|
{:ok, data, []}
|
2021-08-14 10:01:06 -06:00
|
|
|
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
|
|
|
|
|
2020-05-06 08:12:36 -06:00
|
|
|
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
|
2020-04-09 04:44:20 -06:00
|
|
|
end
|
|
|
|
|
2020-06-17 20:05:42 -06:00
|
|
|
def answer(user, object, name) do
|
|
|
|
{:ok,
|
|
|
|
%{
|
|
|
|
"type" => "Answer",
|
|
|
|
"actor" => user.ap_id,
|
2020-06-25 16:07:43 -06:00
|
|
|
"attributedTo" => user.ap_id,
|
2020-06-17 20:05:42 -06:00
|
|
|
"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
|
|
|
|
|
2020-05-11 07:06:23 -06:00
|
|
|
@spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
|
|
|
|
def tombstone(actor, id) do
|
|
|
|
{:ok,
|
|
|
|
%{
|
|
|
|
"id" => id,
|
|
|
|
"actor" => actor,
|
|
|
|
"type" => "Tombstone"
|
|
|
|
}, []}
|
|
|
|
end
|
|
|
|
|
2019-10-16 08:16:39 -06:00
|
|
|
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
|
|
|
def like(actor, object) do
|
2020-05-08 03:30:31 -06:00
|
|
|
with {:ok, data, meta} <- object_action(actor, object) do
|
|
|
|
data =
|
|
|
|
data
|
|
|
|
|> Map.put("type", "Like")
|
|
|
|
|
|
|
|
{:ok, data, meta}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-19 07:30:30 -06:00
|
|
|
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
|
|
|
def update(actor, object) do
|
2022-05-31 12:29:12 -06:00
|
|
|
{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
|
2020-06-19 07:30:30 -06:00
|
|
|
|
|
|
|
{:ok,
|
|
|
|
%{
|
|
|
|
"id" => Utils.generate_activity_id(),
|
|
|
|
"type" => "Update",
|
|
|
|
"actor" => actor.ap_id,
|
|
|
|
"object" => object,
|
2022-05-31 12:29:12 -06:00
|
|
|
"to" => to,
|
|
|
|
"cc" => cc
|
2020-06-19 07:30:30 -06:00
|
|
|
}, []}
|
|
|
|
end
|
|
|
|
|
2020-06-25 03:13:35 -06:00
|
|
|
@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
|
|
|
|
|
2020-05-26 03:47:03 -06:00
|
|
|
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
|
2020-05-20 07:44:37 -06:00
|
|
|
def announce(actor, object, options \\ []) do
|
|
|
|
public? = Keyword.get(options, :public, false)
|
2020-05-18 08:45:11 -06:00
|
|
|
|
2020-05-20 07:44:37 -06:00
|
|
|
to =
|
2020-05-26 03:47:03 -06:00
|
|
|
cond do
|
2020-08-18 09:21:34 -06:00
|
|
|
actor.ap_id == Relay.ap_id() ->
|
2020-05-26 03:47:03 -06:00
|
|
|
[actor.follower_address]
|
|
|
|
|
2020-11-11 07:47:57 -07:00
|
|
|
public? and Visibility.is_local_public?(object) ->
|
2021-05-31 12:39:15 -06:00
|
|
|
[actor.follower_address, object.data["actor"], Utils.as_local_public()]
|
2020-10-02 11:00:50 -06:00
|
|
|
|
2020-05-26 03:47:03 -06:00
|
|
|
public? ->
|
|
|
|
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
|
|
|
|
|
|
|
|
true ->
|
|
|
|
[actor.follower_address, object.data["actor"]]
|
2020-05-20 07:44:37 -06:00
|
|
|
end
|
|
|
|
|
2020-05-18 08:45:11 -06:00
|
|
|
{:ok,
|
|
|
|
%{
|
|
|
|
"id" => Utils.generate_activity_id(),
|
|
|
|
"actor" => actor.ap_id,
|
|
|
|
"object" => object.data["id"],
|
|
|
|
"to" => to,
|
|
|
|
"context" => object.data["context"],
|
2020-05-20 07:44:37 -06:00
|
|
|
"type" => "Announce",
|
|
|
|
"published" => Utils.make_date()
|
2020-05-18 08:45:11 -06:00
|
|
|
}, []}
|
|
|
|
end
|
|
|
|
|
2020-05-08 03:30:31 -06:00
|
|
|
@spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
|
|
|
defp object_action(actor, object) do
|
2019-10-16 08:16:39 -06:00
|
|
|
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
|
2021-02-03 06:09:28 -07:00
|
|
|
|
|
|
|
@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
|
2019-10-16 08:16:39 -06:00
|
|
|
end
|