total rebase
[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.Activity
13   alias Pleroma.Emoji
14   alias Pleroma.Object
15   alias Pleroma.User
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
21
22   require Pleroma.Constants
23
24   def accept_or_reject(actor, activity, type) do
25     data = %{
26       "id" => Utils.generate_activity_id(),
27       "actor" => actor.ap_id,
28       "type" => type,
29       "object" => activity.data["id"],
30       "to" => [activity.actor]
31     }
32
33     {:ok, data, []}
34   end
35
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")
39   end
40
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")
44   end
45
46   @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
47   def follow(follower, followed) do
48     data = %{
49       "id" => Utils.generate_activity_id(),
50       "actor" => follower.ap_id,
51       "type" => "Follow",
52       "object" => followed.ap_id,
53       "to" => [followed.ap_id]
54     }
55
56     {:ok, data, []}
57   end
58
59   defp unicode_emoji_react(_object, data, emoji) do
60     data
61     |> Map.put("content", emoji)
62     |> Map.put("type", "EmojiReact")
63   end
64
65   defp add_emoji_content(data, emoji, url) do
66     tag = [
67       %{
68         "id" => url,
69         "type" => "Emoji",
70         "name" => Emoji.maybe_quote(emoji),
71         "icon" => %{
72           "type" => "Image",
73           "url" => url
74         }
75       }
76     ]
77
78     data
79     |> Map.put("content", Emoji.maybe_quote(emoji))
80     |> Map.put("type", "EmojiReact")
81     |> Map.put("tag", tag)
82   end
83
84   defp remote_custom_emoji_react(
85          %{data: %{"reactions" => existing_reactions}},
86          data,
87          emoji
88        ) do
89     [emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@")
90
91     matching_reaction =
92       Enum.find(
93         existing_reactions,
94         fn [name, _, url] ->
95           if url != nil do
96             url = URI.parse(url)
97             url.host == instance && name == emoji_code
98           end
99         end
100       )
101
102     if matching_reaction do
103       [name, _, url] = matching_reaction
104       add_emoji_content(data, name, url)
105     else
106       {:error, "Could not react"}
107     end
108   end
109
110   defp remote_custom_emoji_react(_object, _data, _emoji) do
111     {:error, "Could not react"}
112   end
113
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)
118     else
119       _ -> {:error, "Emoji does not exist"}
120     end
121   end
122
123   defp custom_emoji_react(object, data, emoji) do
124     if String.contains?(emoji, "@") do
125       remote_custom_emoji_react(object, data, emoji)
126     else
127       local_custom_emoji_react(data, emoji)
128     end
129   end
130
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
134       data =
135         if Emoji.unicode?(emoji) do
136           unicode_emoji_react(object, data, emoji)
137         else
138           custom_emoji_react(object, data, emoji)
139         end
140
141       {:ok, data, meta}
142     end
143   end
144
145   @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
146   def undo(actor, object) do
147     {:ok,
148      %{
149        "id" => Utils.generate_activity_id(),
150        "actor" => actor.ap_id,
151        "type" => "Undo",
152        "object" => object.data["id"],
153        "to" => object.data["to"] || [],
154        "cc" => object.data["cc"] || []
155      }, []}
156   end
157
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)
161
162     user = !object && User.get_cached_by_ap_id(object_id)
163
164     to =
165       case {object, user} do
166         {%Object{}, _} ->
167           # We are deleting an object, address everyone who was originally mentioned
168           (object.data["to"] || []) ++ (object.data["cc"] || [])
169
170         {_, %User{follower_address: follower_address}} ->
171           # We are deleting a user, address the followers of that user
172           [follower_address]
173       end
174
175     {:ok,
176      %{
177        "id" => Utils.generate_activity_id(),
178        "actor" => actor.ap_id,
179        "object" => object_id,
180        "to" => to,
181        "type" => "Delete"
182      }, []}
183   end
184
185   def create(actor, object, recipients) do
186     context =
187       if is_map(object) do
188         object["context"]
189       else
190         nil
191       end
192
193     {:ok,
194      %{
195        "id" => Utils.generate_activity_id(),
196        "actor" => actor.ap_id,
197        "to" => recipients,
198        "object" => object,
199        "type" => "Create",
200        "published" => DateTime.utc_now() |> DateTime.to_iso8601()
201      }
202      |> Pleroma.Maps.put_if_present("context", context), []}
203   end
204
205   @spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
206   def note(%ActivityDraft{} = draft) do
207     data =
208       %{
209         "type" => "Note",
210         "to" => draft.to,
211         "cc" => draft.cc,
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()
219       }
220       |> add_in_reply_to(draft.in_reply_to)
221       |> add_quote(draft.quote_post)
222       |> Map.merge(draft.extra)
223
224     {:ok, data, []}
225   end
226
227   defp add_in_reply_to(object, nil), do: object
228
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"])
232     else
233       _ -> object
234     end
235   end
236
237   defp add_quote(object, nil), do: object
238
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"])
242     else
243       _ -> object
244     end
245   end
246
247   def chat_message(actor, recipient, content, opts \\ []) do
248     basic = %{
249       "id" => Utils.generate_object_id(),
250       "actor" => actor.ap_id,
251       "type" => "ChatMessage",
252       "to" => [recipient],
253       "content" => content,
254       "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
255       "emoji" => Emoji.Formatter.get_emoji_map(content)
256     }
257
258     case opts[:attachment] do
259       %Object{data: attachment_data} ->
260         {
261           :ok,
262           Map.put(basic, "attachment", attachment_data),
263           []
264         }
265
266       _ ->
267         {:ok, basic, []}
268     end
269   end
270
271   def answer(user, object, name) do
272     {:ok,
273      %{
274        "type" => "Answer",
275        "actor" => user.ap_id,
276        "attributedTo" => user.ap_id,
277        "cc" => [object.data["actor"]],
278        "to" => [],
279        "name" => name,
280        "inReplyTo" => object.data["id"],
281        "context" => object.data["context"],
282        "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
283        "id" => Utils.generate_object_id()
284      }, []}
285   end
286
287   @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
288   def tombstone(actor, id) do
289     {:ok,
290      %{
291        "id" => id,
292        "actor" => actor,
293        "type" => "Tombstone"
294      }, []}
295   end
296
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
300       data =
301         data
302         |> Map.put("type", "Like")
303
304       {:ok, data, meta}
305     end
306   end
307
308   @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
309   def update(actor, object) do
310     {to, cc} =
311       if object["type"] in Pleroma.Constants.actor_types() do
312         # User updates, always public
313         {[Pleroma.Constants.as_public(), actor.follower_address], []}
314       else
315         # Status updates, follow the recipients in the object
316         {object["to"] || [], object["cc"] || []}
317       end
318
319     {:ok,
320      %{
321        "id" => Utils.generate_activity_id(),
322        "type" => "Update",
323        "actor" => actor.ap_id,
324        "object" => object,
325        "to" => to,
326        "cc" => cc
327      }, []}
328   end
329
330   @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
331   def block(blocker, blocked) do
332     {:ok,
333      %{
334        "id" => Utils.generate_activity_id(),
335        "type" => "Block",
336        "actor" => blocker.ap_id,
337        "object" => blocked.ap_id,
338        "to" => [blocked.ap_id]
339      }, []}
340   end
341
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)
345
346     to =
347       cond do
348         actor.ap_id == Relay.ap_id() ->
349           [actor.follower_address]
350
351         public? and Visibility.local_public?(object) ->
352           [actor.follower_address, object.data["actor"], Utils.as_local_public()]
353
354         public? ->
355           [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
356
357         true ->
358           [actor.follower_address, object.data["actor"]]
359       end
360
361     {:ok,
362      %{
363        "id" => Utils.generate_activity_id(),
364        "actor" => actor.ap_id,
365        "object" => object.data["id"],
366        "to" => to,
367        "context" => object.data["context"],
368        "type" => "Announce",
369        "published" => Utils.make_date()
370      }, []}
371   end
372
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"])
376
377     # Address the actor of the object, and our actor's follower collection if the post is public.
378     to =
379       if Visibility.public?(object) do
380         [actor.follower_address, object.data["actor"]]
381       else
382         [object.data["actor"]]
383       end
384
385     # CC everyone who's been addressed in the object, except ourself and the object actor's
386     # follower collection
387     cc =
388       (object.data["to"] ++ (object.data["cc"] || []))
389       |> List.delete(actor.ap_id)
390       |> List.delete(object_actor.follower_address)
391
392     {:ok,
393      %{
394        "id" => Utils.generate_activity_id(),
395        "actor" => actor.ap_id,
396        "object" => object.data["id"],
397        "to" => to,
398        "cc" => cc,
399        "context" => object.data["context"]
400      }, []}
401   end
402
403   @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
404   def pin(%User{} = user, object) do
405     {:ok,
406      %{
407        "id" => Utils.generate_activity_id(),
408        "target" => pinned_url(user.nickname),
409        "object" => object.data["id"],
410        "actor" => user.ap_id,
411        "type" => "Add",
412        "to" => [Pleroma.Constants.as_public()],
413        "cc" => [user.follower_address]
414      }, []}
415   end
416
417   @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
418   def unpin(%User{} = user, object) do
419     {:ok,
420      %{
421        "id" => Utils.generate_activity_id(),
422        "target" => pinned_url(user.nickname),
423        "object" => object.data["id"],
424        "actor" => user.ap_id,
425        "type" => "Remove",
426        "to" => [Pleroma.Constants.as_public()],
427        "cc" => [user.follower_address]
428      }, []}
429   end
430
431   defp pinned_url(nickname) when is_binary(nickname) do
432     Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
433   end
434 end