1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ActivityPub.Builder do
7 This module builds the objects. Meant to be used for creating local objects.
9 This module encodes our addressing policies and general shape of our objects.
15 alias Pleroma.Web.ActivityPub.Relay
16 alias Pleroma.Web.ActivityPub.Utils
17 alias Pleroma.Web.ActivityPub.Visibility
18 alias Pleroma.Web.CommonAPI.ActivityDraft
19 alias Pleroma.Web.Endpoint
21 require Pleroma.Constants
23 def accept_or_reject(actor, activity, type) do
25 "id" => Utils.generate_activity_id(),
26 "actor" => actor.ap_id,
28 "object" => activity.data["id"],
29 "to" => [activity.actor]
35 @spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()}
36 def reject(actor, rejected_activity) do
37 accept_or_reject(actor, rejected_activity, "Reject")
40 @spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()}
41 def accept(actor, accepted_activity) do
42 accept_or_reject(actor, accepted_activity, "Accept")
45 @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
46 def follow(follower, followed) do
48 "id" => Utils.generate_activity_id(),
49 "actor" => follower.ap_id,
51 "object" => followed.ap_id,
52 "to" => [followed.ap_id]
58 defp unicode_emoji_react(_object, data, emoji) do
60 |> Map.put("content", emoji)
61 |> Map.put("type", "EmojiReact")
64 defp add_emoji_content(data, emoji, url) do
66 |> Map.put("content", Emoji.maybe_quote(emoji))
67 |> Map.put("type", "EmojiReact")
71 |> Map.put("type", "Emoji")
72 |> Map.put("name", Emoji.maybe_quote(emoji))
76 |> Map.put("type", "Image")
77 |> Map.put("url", url)
82 defp remote_custom_emoji_react(
83 %{data: %{"reactions" => existing_reactions}},
87 [emoji_code, instance] = String.split(Emoji.stripped_name(emoji), "@")
94 url.host == instance && name == emoji_code
98 if matching_reaction do
99 [name, _, url] = matching_reaction
100 add_emoji_content(data, name, url)
102 {:error, "Could not react"}
106 defp remote_custom_emoji_react(_object, _data, _emoji) do
107 {:error, "Could not react"}
110 defp local_custom_emoji_react(data, emoji) do
111 with %{} = emojo <- Emoji.get(emoji) do
112 path = emojo |> Map.get(:file)
113 url = "#{Endpoint.url()}#{path}"
114 add_emoji_content(data, emojo.code, url)
116 _ -> {:error, "Emoji does not exist"}
120 defp custom_emoji_react(object, data, emoji) do
121 if String.contains?(emoji, "@") do
122 remote_custom_emoji_react(object, data, emoji)
124 local_custom_emoji_react(data, emoji)
128 @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
129 def emoji_react(actor, object, emoji) do
130 with {:ok, data, meta} <- object_action(actor, object) do
132 if Emoji.is_unicode_emoji?(emoji) do
133 unicode_emoji_react(object, data, emoji)
135 custom_emoji_react(object, data, emoji)
142 @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
143 def undo(actor, object) do
146 "id" => Utils.generate_activity_id(),
147 "actor" => actor.ap_id,
149 "object" => object.data["id"],
150 "to" => object.data["to"] || [],
151 "cc" => object.data["cc"] || []
155 @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
156 def delete(actor, object_id) do
157 object = Object.normalize(object_id, fetch: false)
159 user = !object && User.get_cached_by_ap_id(object_id)
162 case {object, user} do
164 # We are deleting an object, address everyone who was originally mentioned
165 (object.data["to"] || []) ++ (object.data["cc"] || [])
167 {_, %User{follower_address: follower_address}} ->
168 # We are deleting a user, address the followers of that user
174 "id" => Utils.generate_activity_id(),
175 "actor" => actor.ap_id,
176 "object" => object_id,
182 def create(actor, object, recipients) do
192 "id" => Utils.generate_activity_id(),
193 "actor" => actor.ap_id,
197 "published" => DateTime.utc_now() |> DateTime.to_iso8601()
199 |> Pleroma.Maps.put_if_present("context", context), []}
202 @spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
203 def note(%ActivityDraft{} = draft) do
209 "content" => draft.content_html,
210 "summary" => draft.summary,
211 "sensitive" => draft.sensitive,
212 "context" => draft.context,
213 "attachment" => draft.attachments,
214 "actor" => draft.user.ap_id,
215 "tag" => Keyword.values(draft.tags) |> Enum.uniq()
217 |> add_in_reply_to(draft.in_reply_to)
218 |> add_quote(draft.quote)
219 |> Map.merge(draft.extra)
224 defp add_in_reply_to(object, nil), do: object
226 defp add_in_reply_to(object, in_reply_to) do
227 with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
228 Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
234 defp add_quote(object, nil), do: object
236 defp add_quote(object, quote) do
237 with %Object{} = quote_object <- Object.normalize(quote, fetch: false) do
238 Map.put(object, "quoteUri", quote_object.data["id"])
244 def answer(user, object, name) do
248 "actor" => user.ap_id,
249 "attributedTo" => user.ap_id,
250 "cc" => [object.data["actor"]],
253 "inReplyTo" => object.data["id"],
254 "context" => object.data["context"],
255 "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
256 "id" => Utils.generate_object_id()
260 @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
261 def tombstone(actor, id) do
266 "type" => "Tombstone"
270 @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
271 def like(actor, object) do
272 with {:ok, data, meta} <- object_action(actor, object) do
275 |> Map.put("type", "Like")
281 @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
282 def update(actor, object) do
284 if object["type"] in Pleroma.Constants.actor_types() do
285 # User updates, always public
286 {[Pleroma.Constants.as_public(), actor.follower_address], []}
288 # Status updates, follow the recipients in the object
289 {object["to"] || [], object["cc"] || []}
294 "id" => Utils.generate_activity_id(),
296 "actor" => actor.ap_id,
303 @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
304 def block(blocker, blocked) do
307 "id" => Utils.generate_activity_id(),
309 "actor" => blocker.ap_id,
310 "object" => blocked.ap_id,
311 "to" => [blocked.ap_id]
315 @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
316 def announce(actor, object, options \\ []) do
317 public? = Keyword.get(options, :public, false)
321 actor.ap_id == Relay.ap_id() ->
322 [actor.follower_address]
324 public? and Visibility.is_local_public?(object) ->
325 [actor.follower_address, object.data["actor"], Utils.as_local_public()]
328 [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
331 [actor.follower_address, object.data["actor"]]
336 "id" => Utils.generate_activity_id(),
337 "actor" => actor.ap_id,
338 "object" => object.data["id"],
340 "context" => object.data["context"],
341 "type" => "Announce",
342 "published" => Utils.make_date()
346 @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
347 defp object_action(actor, object) do
348 object_actor = User.get_cached_by_ap_id(object.data["actor"])
350 # Address the actor of the object, and our actor's follower collection if the post is public.
352 if Visibility.is_public?(object) do
353 [actor.follower_address, object.data["actor"]]
355 [object.data["actor"]]
358 # CC everyone who's been addressed in the object, except ourself and the object actor's
359 # follower collection
361 (object.data["to"] ++ (object.data["cc"] || []))
362 |> List.delete(actor.ap_id)
363 |> List.delete(object_actor.follower_address)
367 "id" => Utils.generate_activity_id(),
368 "actor" => actor.ap_id,
369 "object" => object.data["id"],
372 "context" => object.data["context"]
376 @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
377 def pin(%User{} = user, object) do
380 "id" => Utils.generate_activity_id(),
381 "target" => pinned_url(user.nickname),
382 "object" => object.data["id"],
383 "actor" => user.ap_id,
385 "to" => [Pleroma.Constants.as_public()],
386 "cc" => [user.follower_address]
390 @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
391 def unpin(%User{} = user, object) do
394 "id" => Utils.generate_activity_id(),
395 "target" => pinned_url(user.nickname),
396 "object" => object.data["id"],
397 "actor" => user.ap_id,
399 "to" => [Pleroma.Constants.as_public()],
400 "cc" => [user.follower_address]
404 defp pinned_url(nickname) when is_binary(nickname) do
405 Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)