Fix MRF policies to also work with Update
[akkoma] / lib / pleroma / web / activity_pub / builder.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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 alias Pleroma.Web.Endpoint
20
21 require Pleroma.Constants
22
23 def accept_or_reject(actor, activity, type) do
24 data = %{
25 "id" => Utils.generate_activity_id(),
26 "actor" => actor.ap_id,
27 "type" => type,
28 "object" => activity.data["id"],
29 "to" => [activity.actor]
30 }
31
32 {:ok, data, []}
33 end
34
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")
38 end
39
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")
43 end
44
45 @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
46 def follow(follower, followed) do
47 data = %{
48 "id" => Utils.generate_activity_id(),
49 "actor" => follower.ap_id,
50 "type" => "Follow",
51 "object" => followed.ap_id,
52 "to" => [followed.ap_id]
53 }
54
55 {:ok, data, []}
56 end
57
58 defp unicode_emoji_react(_object, data, emoji) do
59 data
60 |> Map.put("content", emoji)
61 |> Map.put("type", "EmojiReact")
62 end
63
64 defp add_emoji_content(data, emoji, url) do
65 data
66 |> Map.put("content", Emoji.maybe_quote(emoji))
67 |> Map.put("type", "EmojiReact")
68 |> Map.put("tag", [
69 %{}
70 |> Map.put("id", url)
71 |> Map.put("type", "Emoji")
72 |> Map.put("name", Emoji.maybe_quote(emoji))
73 |> Map.put(
74 "icon",
75 %{}
76 |> Map.put("type", "Image")
77 |> Map.put("url", url)
78 )
79 ])
80 end
81
82 defp remote_custom_emoji_react(
83 %{data: %{"reactions" => existing_reactions}},
84 data,
85 emoji
86 ) do
87 [emoji_code, instance] = String.split(Emoji.stripped_name(emoji), "@")
88
89 matching_reaction =
90 Enum.find(
91 existing_reactions,
92 fn [name, _, url] ->
93 url = URI.parse(url)
94 url.host == instance && name == emoji_code
95 end
96 )
97
98 if matching_reaction do
99 [name, _, url] = matching_reaction
100 add_emoji_content(data, name, url)
101 else
102 {:error, "Could not react"}
103 end
104 end
105
106 defp remote_custom_emoji_react(_object, _data, _emoji) do
107 {:error, "Could not react"}
108 end
109
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)
115 else
116 _ -> {:error, "Emoji does not exist"}
117 end
118 end
119
120 defp custom_emoji_react(object, data, emoji) do
121 if String.contains?(emoji, "@") do
122 remote_custom_emoji_react(object, data, emoji)
123 else
124 local_custom_emoji_react(data, emoji)
125 end
126 end
127
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
131 data =
132 if Emoji.is_unicode_emoji?(emoji) do
133 unicode_emoji_react(object, data, emoji)
134 else
135 custom_emoji_react(object, data, emoji)
136 end
137
138 {:ok, data, meta}
139 end
140 end
141
142 @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
143 def undo(actor, object) do
144 {:ok,
145 %{
146 "id" => Utils.generate_activity_id(),
147 "actor" => actor.ap_id,
148 "type" => "Undo",
149 "object" => object.data["id"],
150 "to" => object.data["to"] || [],
151 "cc" => object.data["cc"] || []
152 }, []}
153 end
154
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)
158
159 user = !object && User.get_cached_by_ap_id(object_id)
160
161 to =
162 case {object, user} do
163 {%Object{}, _} ->
164 # We are deleting an object, address everyone who was originally mentioned
165 (object.data["to"] || []) ++ (object.data["cc"] || [])
166
167 {_, %User{follower_address: follower_address}} ->
168 # We are deleting a user, address the followers of that user
169 [follower_address]
170 end
171
172 {:ok,
173 %{
174 "id" => Utils.generate_activity_id(),
175 "actor" => actor.ap_id,
176 "object" => object_id,
177 "to" => to,
178 "type" => "Delete"
179 }, []}
180 end
181
182 def create(actor, object, recipients) do
183 context =
184 if is_map(object) do
185 object["context"]
186 else
187 nil
188 end
189
190 {:ok,
191 %{
192 "id" => Utils.generate_activity_id(),
193 "actor" => actor.ap_id,
194 "to" => recipients,
195 "object" => object,
196 "type" => "Create",
197 "published" => DateTime.utc_now() |> DateTime.to_iso8601()
198 }
199 |> Pleroma.Maps.put_if_present("context", context), []}
200 end
201
202 @spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
203 def note(%ActivityDraft{} = draft) do
204 data =
205 %{
206 "type" => "Note",
207 "to" => draft.to,
208 "cc" => draft.cc,
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()
216 }
217 |> add_in_reply_to(draft.in_reply_to)
218 |> add_quote(draft.quote)
219 |> Map.merge(draft.extra)
220
221 {:ok, data, []}
222 end
223
224 defp add_in_reply_to(object, nil), do: object
225
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"])
229 else
230 _ -> object
231 end
232 end
233
234 defp add_quote(object, nil), do: object
235
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"])
239 else
240 _ -> object
241 end
242 end
243
244 def answer(user, object, name) do
245 {:ok,
246 %{
247 "type" => "Answer",
248 "actor" => user.ap_id,
249 "attributedTo" => user.ap_id,
250 "cc" => [object.data["actor"]],
251 "to" => [],
252 "name" => name,
253 "inReplyTo" => object.data["id"],
254 "context" => object.data["context"],
255 "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
256 "id" => Utils.generate_object_id()
257 }, []}
258 end
259
260 @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
261 def tombstone(actor, id) do
262 {:ok,
263 %{
264 "id" => id,
265 "actor" => actor,
266 "type" => "Tombstone"
267 }, []}
268 end
269
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
273 data =
274 data
275 |> Map.put("type", "Like")
276
277 {:ok, data, meta}
278 end
279 end
280
281 @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
282 def update(actor, object) do
283 {to, cc} =
284 if object["type"] in Pleroma.Constants.actor_types() do
285 # User updates, always public
286 {[Pleroma.Constants.as_public(), actor.follower_address], []}
287 else
288 # Status updates, follow the recipients in the object
289 {object["to"] || [], object["cc"] || []}
290 end
291
292 {:ok,
293 %{
294 "id" => Utils.generate_activity_id(),
295 "type" => "Update",
296 "actor" => actor.ap_id,
297 "object" => object,
298 "to" => to,
299 "cc" => cc
300 }, []}
301 end
302
303 @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
304 def block(blocker, blocked) do
305 {:ok,
306 %{
307 "id" => Utils.generate_activity_id(),
308 "type" => "Block",
309 "actor" => blocker.ap_id,
310 "object" => blocked.ap_id,
311 "to" => [blocked.ap_id]
312 }, []}
313 end
314
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)
318
319 to =
320 cond do
321 actor.ap_id == Relay.ap_id() ->
322 [actor.follower_address]
323
324 public? and Visibility.is_local_public?(object) ->
325 [actor.follower_address, object.data["actor"], Utils.as_local_public()]
326
327 public? ->
328 [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
329
330 true ->
331 [actor.follower_address, object.data["actor"]]
332 end
333
334 {:ok,
335 %{
336 "id" => Utils.generate_activity_id(),
337 "actor" => actor.ap_id,
338 "object" => object.data["id"],
339 "to" => to,
340 "context" => object.data["context"],
341 "type" => "Announce",
342 "published" => Utils.make_date()
343 }, []}
344 end
345
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"])
349
350 # Address the actor of the object, and our actor's follower collection if the post is public.
351 to =
352 if Visibility.is_public?(object) do
353 [actor.follower_address, object.data["actor"]]
354 else
355 [object.data["actor"]]
356 end
357
358 # CC everyone who's been addressed in the object, except ourself and the object actor's
359 # follower collection
360 cc =
361 (object.data["to"] ++ (object.data["cc"] || []))
362 |> List.delete(actor.ap_id)
363 |> List.delete(object_actor.follower_address)
364
365 {:ok,
366 %{
367 "id" => Utils.generate_activity_id(),
368 "actor" => actor.ap_id,
369 "object" => object.data["id"],
370 "to" => to,
371 "cc" => cc,
372 "context" => object.data["context"]
373 }, []}
374 end
375
376 @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
377 def pin(%User{} = user, object) do
378 {:ok,
379 %{
380 "id" => Utils.generate_activity_id(),
381 "target" => pinned_url(user.nickname),
382 "object" => object.data["id"],
383 "actor" => user.ap_id,
384 "type" => "Add",
385 "to" => [Pleroma.Constants.as_public()],
386 "cc" => [user.follower_address]
387 }, []}
388 end
389
390 @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
391 def unpin(%User{} = user, object) do
392 {:ok,
393 %{
394 "id" => Utils.generate_activity_id(),
395 "target" => pinned_url(user.nickname),
396 "object" => object.data["id"],
397 "actor" => user.ap_id,
398 "type" => "Remove",
399 "to" => [Pleroma.Constants.as_public()],
400 "cc" => [user.follower_address]
401 }, []}
402 end
403
404 defp pinned_url(nickname) when is_binary(nickname) do
405 Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
406 end
407 end