First
[anni] / lib / pleroma / web / activity_pub / builder.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.Builder do
6   @moduledoc """
7   This module builds the objects. Meant to be used for creating local objects.
8
9   This module encodes our addressing policies and general shape of our objects.
10   """
11
12   alias Pleroma.Emoji
13   alias Pleroma.Object
14   alias Pleroma.User
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
20   require Pleroma.Constants
21
22   def accept_or_reject(actor, activity, type) do
23     data = %{
24       "id" => Utils.generate_activity_id(),
25       "actor" => actor.ap_id,
26       "type" => type,
27       "object" => activity.data["id"],
28       "to" => [activity.actor]
29     }
30
31     {:ok, data, []}
32   end
33
34   @spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()}
35   def reject(actor, rejected_activity) do
36     accept_or_reject(actor, rejected_activity, "Reject")
37   end
38
39   @spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()}
40   def accept(actor, accepted_activity) do
41     accept_or_reject(actor, accepted_activity, "Accept")
42   end
43
44   @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
45   def follow(follower, followed) do
46     data = %{
47       "id" => Utils.generate_activity_id(),
48       "actor" => follower.ap_id,
49       "type" => "Follow",
50       "object" => followed.ap_id,
51       "to" => [followed.ap_id]
52     }
53
54     {:ok, data, []}
55   end
56
57   @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
58   def emoji_react(actor, object, emoji) do
59     with {:ok, data, meta} <- object_action(actor, object) do
60       data =
61         data
62         |> Map.put("content", emoji)
63         |> Map.put("type", "EmojiReact")
64
65       {:ok, data, meta}
66     end
67   end
68
69   @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
70   def undo(actor, object) do
71     {:ok,
72      %{
73        "id" => Utils.generate_activity_id(),
74        "actor" => actor.ap_id,
75        "type" => "Undo",
76        "object" => object.data["id"],
77        "to" => object.data["to"] || [],
78        "cc" => object.data["cc"] || []
79      }, []}
80   end
81
82   @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
83   def delete(actor, object_id) do
84     object = Object.normalize(object_id, fetch: false)
85
86     user = !object && User.get_cached_by_ap_id(object_id)
87
88     to =
89       case {object, user} do
90         {%Object{}, _} ->
91           # We are deleting an object, address everyone who was originally mentioned
92           (object.data["to"] || []) ++ (object.data["cc"] || [])
93
94         {_, %User{follower_address: follower_address}} ->
95           # We are deleting a user, address the followers of that user
96           [follower_address]
97       end
98
99     {:ok,
100      %{
101        "id" => Utils.generate_activity_id(),
102        "actor" => actor.ap_id,
103        "object" => object_id,
104        "to" => to,
105        "type" => "Delete"
106      }, []}
107   end
108
109   def create(actor, object, recipients) do
110     context =
111       if is_map(object) do
112         object["context"]
113       else
114         nil
115       end
116
117     {:ok,
118      %{
119        "id" => Utils.generate_activity_id(),
120        "actor" => actor.ap_id,
121        "to" => recipients,
122        "object" => object,
123        "type" => "Create",
124        "published" => DateTime.utc_now() |> DateTime.to_iso8601()
125      }
126      |> Pleroma.Maps.put_if_present("context", context), []}
127   end
128
129   @spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
130   def note(%ActivityDraft{} = draft) do
131     data =
132       %{
133         "type" => "Note",
134         "to" => draft.to,
135         "cc" => draft.cc,
136         "content" => draft.content_html,
137         "summary" => draft.summary,
138         "sensitive" => draft.sensitive,
139         "context" => draft.context,
140         "attachment" => draft.attachments,
141         "actor" => draft.user.ap_id,
142         "tag" => Keyword.values(draft.tags) |> Enum.uniq()
143       }
144       |> add_in_reply_to(draft.in_reply_to)
145       |> Map.merge(draft.extra)
146
147     {:ok, data, []}
148   end
149
150   defp add_in_reply_to(object, nil), do: object
151
152   defp add_in_reply_to(object, in_reply_to) do
153     with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
154       Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
155     else
156       _ -> object
157     end
158   end
159
160   def chat_message(actor, recipient, content, opts \\ []) do
161     basic = %{
162       "id" => Utils.generate_object_id(),
163       "actor" => actor.ap_id,
164       "type" => "ChatMessage",
165       "to" => [recipient],
166       "content" => content,
167       "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
168       "emoji" => Emoji.Formatter.get_emoji_map(content)
169     }
170
171     case opts[:attachment] do
172       %Object{data: attachment_data} ->
173         {
174           :ok,
175           Map.put(basic, "attachment", attachment_data),
176           []
177         }
178
179       _ ->
180         {:ok, basic, []}
181     end
182   end
183
184   def answer(user, object, name) do
185     {:ok,
186      %{
187        "type" => "Answer",
188        "actor" => user.ap_id,
189        "attributedTo" => user.ap_id,
190        "cc" => [object.data["actor"]],
191        "to" => [],
192        "name" => name,
193        "inReplyTo" => object.data["id"],
194        "context" => object.data["context"],
195        "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
196        "id" => Utils.generate_object_id()
197      }, []}
198   end
199
200   @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
201   def tombstone(actor, id) do
202     {:ok,
203      %{
204        "id" => id,
205        "actor" => actor,
206        "type" => "Tombstone"
207      }, []}
208   end
209
210   @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
211   def like(actor, object) do
212     with {:ok, data, meta} <- object_action(actor, object) do
213       data =
214         data
215         |> Map.put("type", "Like")
216
217       {:ok, data, meta}
218     end
219   end
220
221   @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
222   def update(actor, object) do
223     {to, cc} =
224       if object["type"] in Pleroma.Constants.actor_types() do
225         # User updates, always public
226         {[Pleroma.Constants.as_public(), actor.follower_address], []}
227       else
228         # Status updates, follow the recipients in the object
229         {object["to"] || [], object["cc"] || []}
230       end
231
232     {:ok,
233      %{
234        "id" => Utils.generate_activity_id(),
235        "type" => "Update",
236        "actor" => actor.ap_id,
237        "object" => object,
238        "to" => to,
239        "cc" => cc
240      }, []}
241   end
242
243   @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
244   def block(blocker, blocked) do
245     {:ok,
246      %{
247        "id" => Utils.generate_activity_id(),
248        "type" => "Block",
249        "actor" => blocker.ap_id,
250        "object" => blocked.ap_id,
251        "to" => [blocked.ap_id]
252      }, []}
253   end
254
255   @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
256   def announce(actor, object, options \\ []) do
257     public? = Keyword.get(options, :public, false)
258
259     to =
260       cond do
261         actor.ap_id == Relay.ap_id() ->
262           [actor.follower_address]
263
264         public? and Visibility.is_local_public?(object) ->
265           [actor.follower_address, object.data["actor"], Utils.as_local_public()]
266
267         public? ->
268           [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
269
270         true ->
271           [actor.follower_address, object.data["actor"]]
272       end
273
274     {:ok,
275      %{
276        "id" => Utils.generate_activity_id(),
277        "actor" => actor.ap_id,
278        "object" => object.data["id"],
279        "to" => to,
280        "context" => object.data["context"],
281        "type" => "Announce",
282        "published" => Utils.make_date()
283      }, []}
284   end
285
286   @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
287   defp object_action(actor, object) do
288     object_actor = User.get_cached_by_ap_id(object.data["actor"])
289
290     # Address the actor of the object, and our actor's follower collection if the post is public.
291     to =
292       if Visibility.is_public?(object) do
293         [actor.follower_address, object.data["actor"]]
294       else
295         [object.data["actor"]]
296       end
297
298     # CC everyone who's been addressed in the object, except ourself and the object actor's
299     # follower collection
300     cc =
301       (object.data["to"] ++ (object.data["cc"] || []))
302       |> List.delete(actor.ap_id)
303       |> List.delete(object_actor.follower_address)
304
305     {:ok,
306      %{
307        "id" => Utils.generate_activity_id(),
308        "actor" => actor.ap_id,
309        "object" => object.data["id"],
310        "to" => to,
311        "cc" => cc,
312        "context" => object.data["context"]
313      }, []}
314   end
315
316   @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
317   def pin(%User{} = user, object) do
318     {:ok,
319      %{
320        "id" => Utils.generate_activity_id(),
321        "target" => pinned_url(user.nickname),
322        "object" => object.data["id"],
323        "actor" => user.ap_id,
324        "type" => "Add",
325        "to" => [Pleroma.Constants.as_public()],
326        "cc" => [user.follower_address]
327      }, []}
328   end
329
330   @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
331   def unpin(%User{} = user, object) do
332     {:ok,
333      %{
334        "id" => Utils.generate_activity_id(),
335        "target" => pinned_url(user.nickname),
336        "object" => object.data["id"],
337        "actor" => user.ap_id,
338        "type" => "Remove",
339        "to" => [Pleroma.Constants.as_public()],
340        "cc" => [user.follower_address]
341      }, []}
342   end
343
344   defp pinned_url(nickname) when is_binary(nickname) do
345     Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
346   end
347 end