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}} = object,
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 # Retricted to user updates for now, always public
282 @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
283 def update(actor, object) do
284 to = [Pleroma.Constants.as_public(), actor.follower_address]
288 "id" => Utils.generate_activity_id(),
290 "actor" => actor.ap_id,
296 @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
297 def block(blocker, blocked) do
300 "id" => Utils.generate_activity_id(),
302 "actor" => blocker.ap_id,
303 "object" => blocked.ap_id,
304 "to" => [blocked.ap_id]
308 @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
309 def announce(actor, object, options \\ []) do
310 public? = Keyword.get(options, :public, false)
314 actor.ap_id == Relay.ap_id() ->
315 [actor.follower_address]
317 public? and Visibility.is_local_public?(object) ->
318 [actor.follower_address, object.data["actor"], Utils.as_local_public()]
321 [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
324 [actor.follower_address, object.data["actor"]]
329 "id" => Utils.generate_activity_id(),
330 "actor" => actor.ap_id,
331 "object" => object.data["id"],
333 "context" => object.data["context"],
334 "type" => "Announce",
335 "published" => Utils.make_date()
339 @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
340 defp object_action(actor, object) do
341 object_actor = User.get_cached_by_ap_id(object.data["actor"])
343 # Address the actor of the object, and our actor's follower collection if the post is public.
345 if Visibility.is_public?(object) do
346 [actor.follower_address, object.data["actor"]]
348 [object.data["actor"]]
351 # CC everyone who's been addressed in the object, except ourself and the object actor's
352 # follower collection
354 (object.data["to"] ++ (object.data["cc"] || []))
355 |> List.delete(actor.ap_id)
356 |> List.delete(object_actor.follower_address)
360 "id" => Utils.generate_activity_id(),
361 "actor" => actor.ap_id,
362 "object" => object.data["id"],
365 "context" => object.data["context"]
369 @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
370 def pin(%User{} = user, object) do
373 "id" => Utils.generate_activity_id(),
374 "target" => pinned_url(user.nickname),
375 "object" => object.data["id"],
376 "actor" => user.ap_id,
378 "to" => [Pleroma.Constants.as_public()],
379 "cc" => [user.follower_address]
383 @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
384 def unpin(%User{} = user, object) do
387 "id" => Utils.generate_activity_id(),
388 "target" => pinned_url(user.nickname),
389 "object" => object.data["id"],
390 "actor" => user.ap_id,
392 "to" => [Pleroma.Constants.as_public()],
393 "cc" => [user.follower_address]
397 defp pinned_url(nickname) when is_binary(nickname) do
398 Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)