1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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.
12 alias Pleroma.Activity
16 alias Pleroma.Web.ActivityPub.Relay
17 alias Pleroma.Web.ActivityPub.Utils
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.CommonAPI.ActivityDraft
20 alias Pleroma.Web.Endpoint
22 require Pleroma.Constants
24 def accept_or_reject(actor, activity, type) do
26 "id" => Utils.generate_activity_id(),
27 "actor" => actor.ap_id,
29 "object" => activity.data["id"],
30 "to" => [activity.actor]
36 @spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()}
37 def reject(actor, rejected_activity) do
38 accept_or_reject(actor, rejected_activity, "Reject")
41 @spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()}
42 def accept(actor, accepted_activity) do
43 accept_or_reject(actor, accepted_activity, "Accept")
46 @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
47 def follow(follower, followed) do
49 "id" => Utils.generate_activity_id(),
50 "actor" => follower.ap_id,
52 "object" => followed.ap_id,
53 "to" => [followed.ap_id]
59 defp unicode_emoji_react(_object, data, emoji) do
61 |> Map.put("content", emoji)
62 |> Map.put("type", "EmojiReact")
65 defp add_emoji_content(data, emoji, url) do
70 "name" => Emoji.maybe_quote(emoji),
79 |> Map.put("content", Emoji.maybe_quote(emoji))
80 |> Map.put("type", "EmojiReact")
81 |> Map.put("tag", tag)
84 defp remote_custom_emoji_react(
85 %{data: %{"reactions" => existing_reactions}},
89 [emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@")
97 url.host == instance && name == emoji_code
102 if matching_reaction do
103 [name, _, url] = matching_reaction
104 add_emoji_content(data, name, url)
106 {:error, "Could not react"}
110 defp remote_custom_emoji_react(_object, _data, _emoji) do
111 {:error, "Could not react"}
114 defp local_custom_emoji_react(data, emoji) do
115 with %{file: path} = emojo <- Emoji.get(emoji) do
116 url = "#{Endpoint.url()}#{path}"
117 add_emoji_content(data, emojo.code, url)
119 _ -> {:error, "Emoji does not exist"}
123 defp custom_emoji_react(object, data, emoji) do
124 if String.contains?(emoji, "@") do
125 remote_custom_emoji_react(object, data, emoji)
127 local_custom_emoji_react(data, emoji)
131 @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
132 def emoji_react(actor, object, emoji) do
133 with {:ok, data, meta} <- object_action(actor, object) do
135 if Emoji.unicode?(emoji) do
136 unicode_emoji_react(object, data, emoji)
138 custom_emoji_react(object, data, emoji)
145 @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
146 def undo(actor, object) do
149 "id" => Utils.generate_activity_id(),
150 "actor" => actor.ap_id,
152 "object" => object.data["id"],
153 "to" => object.data["to"] || [],
154 "cc" => object.data["cc"] || []
158 @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
159 def delete(actor, object_id) do
160 object = Object.normalize(object_id, fetch: false)
162 user = !object && User.get_cached_by_ap_id(object_id)
165 case {object, user} do
167 # We are deleting an object, address everyone who was originally mentioned
168 (object.data["to"] || []) ++ (object.data["cc"] || [])
170 {_, %User{follower_address: follower_address}} ->
171 # We are deleting a user, address the followers of that user
177 "id" => Utils.generate_activity_id(),
178 "actor" => actor.ap_id,
179 "object" => object_id,
185 def create(actor, object, recipients) do
195 "id" => Utils.generate_activity_id(),
196 "actor" => actor.ap_id,
200 "published" => DateTime.utc_now() |> DateTime.to_iso8601()
202 |> Pleroma.Maps.put_if_present("context", context), []}
205 @spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
206 def note(%ActivityDraft{} = draft) do
212 "content" => draft.content_html,
213 "summary" => draft.summary,
214 "sensitive" => draft.sensitive,
215 "context" => draft.context,
216 "attachment" => draft.attachments,
217 "actor" => draft.user.ap_id,
218 "tag" => Keyword.values(draft.tags) |> Enum.uniq()
220 |> add_in_reply_to(draft.in_reply_to)
221 |> add_quote(draft.quote_post)
222 |> Map.merge(draft.extra)
227 defp add_in_reply_to(object, nil), do: object
229 defp add_in_reply_to(object, in_reply_to) do
230 with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
231 Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
237 defp add_quote(object, nil), do: object
239 defp add_quote(object, quote_post) do
240 with %Object{} = quote_object <- Object.normalize(quote_post, fetch: false) do
241 Map.put(object, "quoteUrl", quote_object.data["id"])
247 def chat_message(actor, recipient, content, opts \\ []) do
249 "id" => Utils.generate_object_id(),
250 "actor" => actor.ap_id,
251 "type" => "ChatMessage",
253 "content" => content,
254 "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
255 "emoji" => Emoji.Formatter.get_emoji_map(content)
258 case opts[:attachment] do
259 %Object{data: attachment_data} ->
262 Map.put(basic, "attachment", attachment_data),
271 def answer(user, object, name) do
275 "actor" => user.ap_id,
276 "attributedTo" => user.ap_id,
277 "cc" => [object.data["actor"]],
280 "inReplyTo" => object.data["id"],
281 "context" => object.data["context"],
282 "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
283 "id" => Utils.generate_object_id()
287 @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
288 def tombstone(actor, id) do
293 "type" => "Tombstone"
297 @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
298 def like(actor, object) do
299 with {:ok, data, meta} <- object_action(actor, object) do
302 |> Map.put("type", "Like")
308 @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
309 def update(actor, object) do
311 if object["type"] in Pleroma.Constants.actor_types() do
312 # User updates, always public
313 {[Pleroma.Constants.as_public(), actor.follower_address], []}
315 # Status updates, follow the recipients in the object
316 {object["to"] || [], object["cc"] || []}
321 "id" => Utils.generate_activity_id(),
323 "actor" => actor.ap_id,
330 @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
331 def block(blocker, blocked) do
334 "id" => Utils.generate_activity_id(),
336 "actor" => blocker.ap_id,
337 "object" => blocked.ap_id,
338 "to" => [blocked.ap_id]
342 @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
343 def announce(actor, object, options \\ []) do
344 public? = Keyword.get(options, :public, false)
348 actor.ap_id == Relay.ap_id() ->
349 [actor.follower_address]
351 public? and Visibility.local_public?(object) ->
352 [actor.follower_address, object.data["actor"], Utils.as_local_public()]
355 [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
358 [actor.follower_address, object.data["actor"]]
363 "id" => Utils.generate_activity_id(),
364 "actor" => actor.ap_id,
365 "object" => object.data["id"],
367 "context" => object.data["context"],
368 "type" => "Announce",
369 "published" => Utils.make_date()
373 @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
374 defp object_action(actor, object) do
375 object_actor = User.get_cached_by_ap_id(object.data["actor"])
377 # Address the actor of the object, and our actor's follower collection if the post is public.
379 if Visibility.public?(object) do
380 [actor.follower_address, object.data["actor"]]
382 [object.data["actor"]]
385 # CC everyone who's been addressed in the object, except ourself and the object actor's
386 # follower collection
388 (object.data["to"] ++ (object.data["cc"] || []))
389 |> List.delete(actor.ap_id)
390 |> List.delete(object_actor.follower_address)
394 "id" => Utils.generate_activity_id(),
395 "actor" => actor.ap_id,
396 "object" => object.data["id"],
399 "context" => object.data["context"]
403 @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
404 def pin(%User{} = user, object) do
407 "id" => Utils.generate_activity_id(),
408 "target" => pinned_url(user.nickname),
409 "object" => object.data["id"],
410 "actor" => user.ap_id,
412 "to" => [Pleroma.Constants.as_public()],
413 "cc" => [user.follower_address]
417 @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
418 def unpin(%User{} = user, object) do
421 "id" => Utils.generate_activity_id(),
422 "target" => pinned_url(user.nickname),
423 "object" => object.data["id"],
424 "actor" => user.ap_id,
426 "to" => [Pleroma.Constants.as_public()],
427 "cc" => [user.follower_address]
431 defp pinned_url(nickname) when is_binary(nickname) do
432 Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)