Transmogrifier: For follows, create notifications last.
[akkoma] / lib / pleroma / web / activity_pub / transmogrifier.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.Transmogrifier do
6 @moduledoc """
7 A module to handle coding from internal to wire ActivityPub and back.
8 """
9 alias Pleroma.Activity
10 alias Pleroma.EarmarkRenderer
11 alias Pleroma.FollowingRelationship
12 alias Pleroma.Notification
13 alias Pleroma.Object
14 alias Pleroma.Object.Containment
15 alias Pleroma.Repo
16 alias Pleroma.User
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Builder
19 alias Pleroma.Web.ActivityPub.ObjectValidator
20 alias Pleroma.Web.ActivityPub.ObjectValidators.Types
21 alias Pleroma.Web.ActivityPub.Pipeline
22 alias Pleroma.Web.ActivityPub.Utils
23 alias Pleroma.Web.ActivityPub.Visibility
24 alias Pleroma.Web.Federator
25 alias Pleroma.Workers.TransmogrifierWorker
26
27 import Ecto.Query
28
29 require Logger
30 require Pleroma.Constants
31
32 @doc """
33 Modifies an incoming AP object (mastodon format) to our internal format.
34 """
35 def fix_object(object, options \\ []) do
36 object
37 |> strip_internal_fields
38 |> fix_actor
39 |> fix_url
40 |> fix_attachments
41 |> fix_context
42 |> fix_in_reply_to(options)
43 |> fix_emoji
44 |> fix_tag
45 |> fix_content_map
46 |> fix_addressing
47 |> fix_summary
48 |> fix_type(options)
49 |> fix_content
50 end
51
52 def fix_summary(%{"summary" => nil} = object) do
53 Map.put(object, "summary", "")
54 end
55
56 def fix_summary(%{"summary" => _} = object) do
57 # summary is present, nothing to do
58 object
59 end
60
61 def fix_summary(object), do: Map.put(object, "summary", "")
62
63 def fix_addressing_list(map, field) do
64 cond do
65 is_binary(map[field]) ->
66 Map.put(map, field, [map[field]])
67
68 is_nil(map[field]) ->
69 Map.put(map, field, [])
70
71 true ->
72 map
73 end
74 end
75
76 def fix_explicit_addressing(
77 %{"to" => to, "cc" => cc} = object,
78 explicit_mentions,
79 follower_collection
80 ) do
81 explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
82
83 explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
84
85 final_cc =
86 (cc ++ explicit_cc)
87 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
88 |> Enum.uniq()
89
90 object
91 |> Map.put("to", explicit_to)
92 |> Map.put("cc", final_cc)
93 end
94
95 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
96
97 # if directMessage flag is set to true, leave the addressing alone
98 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
99
100 def fix_explicit_addressing(object) do
101 explicit_mentions = Utils.determine_explicit_mentions(object)
102
103 %User{follower_address: follower_collection} =
104 object
105 |> Containment.get_actor()
106 |> User.get_cached_by_ap_id()
107
108 explicit_mentions =
109 explicit_mentions ++
110 [
111 Pleroma.Constants.as_public(),
112 follower_collection
113 ]
114
115 fix_explicit_addressing(object, explicit_mentions, follower_collection)
116 end
117
118 # if as:Public is addressed, then make sure the followers collection is also addressed
119 # so that the activities will be delivered to local users.
120 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
121 recipients = to ++ cc
122
123 if followers_collection not in recipients do
124 cond do
125 Pleroma.Constants.as_public() in cc ->
126 to = to ++ [followers_collection]
127 Map.put(object, "to", to)
128
129 Pleroma.Constants.as_public() in to ->
130 cc = cc ++ [followers_collection]
131 Map.put(object, "cc", cc)
132
133 true ->
134 object
135 end
136 else
137 object
138 end
139 end
140
141 def fix_implicit_addressing(object, _), do: object
142
143 def fix_addressing(object) do
144 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
145 followers_collection = User.ap_followers(user)
146
147 object
148 |> fix_addressing_list("to")
149 |> fix_addressing_list("cc")
150 |> fix_addressing_list("bto")
151 |> fix_addressing_list("bcc")
152 |> fix_explicit_addressing()
153 |> fix_implicit_addressing(followers_collection)
154 end
155
156 def fix_actor(%{"attributedTo" => actor} = object) do
157 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
158 end
159
160 def fix_in_reply_to(object, options \\ [])
161
162 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
163 when not is_nil(in_reply_to) do
164 in_reply_to_id = prepare_in_reply_to(in_reply_to)
165 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
166 depth = (options[:depth] || 0) + 1
167
168 if Federator.allowed_thread_distance?(depth) do
169 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
170 %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
171 object
172 |> Map.put("inReplyTo", replied_object.data["id"])
173 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
174 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
175 |> Map.put("context", replied_object.data["context"] || object["conversation"])
176 else
177 e ->
178 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
179 object
180 end
181 else
182 object
183 end
184 end
185
186 def fix_in_reply_to(object, _options), do: object
187
188 defp prepare_in_reply_to(in_reply_to) do
189 cond do
190 is_bitstring(in_reply_to) ->
191 in_reply_to
192
193 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
194 in_reply_to["id"]
195
196 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
197 Enum.at(in_reply_to, 0)
198
199 true ->
200 ""
201 end
202 end
203
204 def fix_context(object) do
205 context = object["context"] || object["conversation"] || Utils.generate_context_id()
206
207 object
208 |> Map.put("context", context)
209 |> Map.put("conversation", context)
210 end
211
212 defp add_if_present(map, _key, nil), do: map
213
214 defp add_if_present(map, key, value) do
215 Map.put(map, key, value)
216 end
217
218 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
219 attachments =
220 Enum.map(attachment, fn data ->
221 url =
222 cond do
223 is_list(data["url"]) -> List.first(data["url"])
224 is_map(data["url"]) -> data["url"]
225 true -> nil
226 end
227
228 media_type =
229 cond do
230 is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
231 is_binary(data["mediaType"]) -> data["mediaType"]
232 is_binary(data["mimeType"]) -> data["mimeType"]
233 true -> nil
234 end
235
236 href =
237 cond do
238 is_map(url) && is_binary(url["href"]) -> url["href"]
239 is_binary(data["url"]) -> data["url"]
240 is_binary(data["href"]) -> data["href"]
241 end
242
243 attachment_url =
244 %{"href" => href}
245 |> add_if_present("mediaType", media_type)
246 |> add_if_present("type", Map.get(url || %{}, "type"))
247
248 %{"url" => [attachment_url]}
249 |> add_if_present("mediaType", media_type)
250 |> add_if_present("type", data["type"])
251 |> add_if_present("name", data["name"])
252 end)
253
254 Map.put(object, "attachment", attachments)
255 end
256
257 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
258 object
259 |> Map.put("attachment", [attachment])
260 |> fix_attachments()
261 end
262
263 def fix_attachments(object), do: object
264
265 def fix_url(%{"url" => url} = object) when is_map(url) do
266 Map.put(object, "url", url["href"])
267 end
268
269 def fix_url(%{"type" => object_type, "url" => url} = object)
270 when object_type in ["Video", "Audio"] and is_list(url) do
271 first_element = Enum.at(url, 0)
272
273 link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
274
275 object
276 |> Map.put("attachment", [first_element])
277 |> Map.put("url", link_element["href"])
278 end
279
280 def fix_url(%{"type" => object_type, "url" => url} = object)
281 when object_type != "Video" and is_list(url) do
282 first_element = Enum.at(url, 0)
283
284 url_string =
285 cond do
286 is_bitstring(first_element) -> first_element
287 is_map(first_element) -> first_element["href"] || ""
288 true -> ""
289 end
290
291 Map.put(object, "url", url_string)
292 end
293
294 def fix_url(object), do: object
295
296 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
297 emoji =
298 tags
299 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
300 |> Enum.reduce(%{}, fn data, mapping ->
301 name = String.trim(data["name"], ":")
302
303 Map.put(mapping, name, data["icon"]["url"])
304 end)
305
306 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
307 emoji = Map.merge(object["emoji"] || %{}, emoji)
308
309 Map.put(object, "emoji", emoji)
310 end
311
312 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
313 name = String.trim(tag["name"], ":")
314 emoji = %{name => tag["icon"]["url"]}
315
316 Map.put(object, "emoji", emoji)
317 end
318
319 def fix_emoji(object), do: object
320
321 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
322 tags =
323 tag
324 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
325 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
326
327 Map.put(object, "tag", tag ++ tags)
328 end
329
330 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
331 combined = [tag, String.slice(hashtag, 1..-1)]
332
333 Map.put(object, "tag", combined)
334 end
335
336 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
337
338 def fix_tag(object), do: object
339
340 # content map usually only has one language so this will do for now.
341 def fix_content_map(%{"contentMap" => content_map} = object) do
342 content_groups = Map.to_list(content_map)
343 {_, content} = Enum.at(content_groups, 0)
344
345 Map.put(object, "content", content)
346 end
347
348 def fix_content_map(object), do: object
349
350 def fix_type(object, options \\ [])
351
352 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
353 when is_binary(reply_id) do
354 with true <- Federator.allowed_thread_distance?(options[:depth]),
355 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
356 Map.put(object, "type", "Answer")
357 else
358 _ -> object
359 end
360 end
361
362 def fix_type(object, _), do: object
363
364 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
365 when is_binary(content) do
366 html_content =
367 content
368 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
369 |> Pleroma.HTML.filter_tags()
370
371 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
372 end
373
374 defp fix_content(object), do: object
375
376 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
377 with true <- id =~ "follows",
378 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
379 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
380 {:ok, activity}
381 else
382 _ -> {:error, nil}
383 end
384 end
385
386 defp mastodon_follow_hack(_, _), do: {:error, nil}
387
388 defp get_follow_activity(follow_object, followed) do
389 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
390 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
391 {:ok, activity}
392 else
393 # Can't find the activity. This might a Mastodon 2.3 "Accept"
394 {:activity, nil} ->
395 mastodon_follow_hack(follow_object, followed)
396
397 _ ->
398 {:error, nil}
399 end
400 end
401
402 # Reduce the object list to find the reported user.
403 defp get_reported(objects) do
404 Enum.reduce_while(objects, nil, fn ap_id, _ ->
405 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
406 {:halt, user}
407 else
408 _ -> {:cont, nil}
409 end
410 end)
411 end
412
413 def handle_incoming(data, options \\ [])
414
415 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
416 # with nil ID.
417 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
418 with context <- data["context"] || Utils.generate_context_id(),
419 content <- data["content"] || "",
420 %User{} = actor <- User.get_cached_by_ap_id(actor),
421 # Reduce the object list to find the reported user.
422 %User{} = account <- get_reported(objects),
423 # Remove the reported user from the object list.
424 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
425 %{
426 actor: actor,
427 context: context,
428 account: account,
429 statuses: statuses,
430 content: content,
431 additional: %{"cc" => [account.ap_id]}
432 }
433 |> ActivityPub.flag()
434 end
435 end
436
437 # disallow objects with bogus IDs
438 def handle_incoming(%{"id" => nil}, _options), do: :error
439 def handle_incoming(%{"id" => ""}, _options), do: :error
440 # length of https:// = 8, should validate better, but good enough for now.
441 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
442 do: :error
443
444 # TODO: validate those with a Ecto scheme
445 # - tags
446 # - emoji
447 def handle_incoming(
448 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
449 options
450 )
451 when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
452 actor = Containment.get_actor(data)
453
454 data =
455 Map.put(data, "actor", actor)
456 |> fix_addressing
457
458 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
459 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
460 object = fix_object(object, options)
461
462 params = %{
463 to: data["to"],
464 object: object,
465 actor: user,
466 context: object["conversation"],
467 local: false,
468 published: data["published"],
469 additional:
470 Map.take(data, [
471 "cc",
472 "directMessage",
473 "id"
474 ])
475 }
476
477 with {:ok, created_activity} <- ActivityPub.create(params) do
478 reply_depth = (options[:depth] || 0) + 1
479
480 if Federator.allowed_thread_distance?(reply_depth) do
481 for reply_id <- replies(object) do
482 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
483 "id" => reply_id,
484 "depth" => reply_depth
485 })
486 end
487 end
488
489 {:ok, created_activity}
490 end
491 else
492 %Activity{} = activity -> {:ok, activity}
493 _e -> :error
494 end
495 end
496
497 def handle_incoming(
498 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
499 options
500 ) do
501 actor = Containment.get_actor(data)
502
503 data =
504 Map.put(data, "actor", actor)
505 |> fix_addressing
506
507 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
508 reply_depth = (options[:depth] || 0) + 1
509 options = Keyword.put(options, :depth, reply_depth)
510 object = fix_object(object, options)
511
512 params = %{
513 to: data["to"],
514 object: object,
515 actor: user,
516 context: nil,
517 local: false,
518 published: data["published"],
519 additional: Map.take(data, ["cc", "id"])
520 }
521
522 ActivityPub.listen(params)
523 else
524 _e -> :error
525 end
526 end
527
528 def handle_incoming(
529 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
530 _options
531 ) do
532 with %User{local: true} = followed <-
533 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
534 {:ok, %User{} = follower} <-
535 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
536 {:ok, activity} <-
537 ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
538 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
539 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
540 {_, false} <- {:user_locked, User.locked?(followed)},
541 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
542 {_, {:ok, _}} <-
543 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
544 {:ok, _relationship} <-
545 FollowingRelationship.update(follower, followed, :follow_accept) do
546 ActivityPub.accept(%{
547 to: [follower.ap_id],
548 actor: followed,
549 object: data,
550 local: true
551 })
552 else
553 {:user_blocked, true} ->
554 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
555 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
556
557 ActivityPub.reject(%{
558 to: [follower.ap_id],
559 actor: followed,
560 object: data,
561 local: true
562 })
563
564 {:follow, {:error, _}} ->
565 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
566 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
567
568 ActivityPub.reject(%{
569 to: [follower.ap_id],
570 actor: followed,
571 object: data,
572 local: true
573 })
574
575 {:user_locked, true} ->
576 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
577 :noop
578 end
579
580 ActivityPub.notify_and_stream(activity)
581 {:ok, activity}
582 else
583 _e ->
584 :error
585 end
586 end
587
588 def handle_incoming(
589 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
590 _options
591 ) do
592 with actor <- Containment.get_actor(data),
593 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
594 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
595 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
596 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
597 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
598 User.update_follower_count(followed)
599 User.update_following_count(follower)
600
601 Notification.update_notification_type(followed, follow_activity)
602
603 ActivityPub.accept(%{
604 to: follow_activity.data["to"],
605 type: "Accept",
606 actor: followed,
607 object: follow_activity.data["id"],
608 local: false,
609 activity_id: id
610 })
611 else
612 _e ->
613 :error
614 end
615 end
616
617 def handle_incoming(
618 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
619 _options
620 ) do
621 with actor <- Containment.get_actor(data),
622 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
623 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
624 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
625 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
626 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
627 {:ok, activity} <-
628 ActivityPub.reject(%{
629 to: follow_activity.data["to"],
630 type: "Reject",
631 actor: followed,
632 object: follow_activity.data["id"],
633 local: false,
634 activity_id: id
635 }) do
636 {:ok, activity}
637 else
638 _e -> :error
639 end
640 end
641
642 @misskey_reactions %{
643 "like" => "👍",
644 "love" => "❤️",
645 "laugh" => "😆",
646 "hmm" => "🤔",
647 "surprise" => "😮",
648 "congrats" => "🎉",
649 "angry" => "💢",
650 "confused" => "😥",
651 "rip" => "😇",
652 "pudding" => "🍮",
653 "star" => "⭐"
654 }
655
656 @doc "Rewrite misskey likes into EmojiReacts"
657 def handle_incoming(
658 %{
659 "type" => "Like",
660 "_misskey_reaction" => reaction
661 } = data,
662 options
663 ) do
664 data
665 |> Map.put("type", "EmojiReact")
666 |> Map.put("content", @misskey_reactions[reaction] || reaction)
667 |> handle_incoming(options)
668 end
669
670 def handle_incoming(
671 %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
672 _options
673 ) do
674 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
675 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
676 {:ok, activity}
677 end
678 end
679
680 def handle_incoming(%{"type" => type} = data, _options)
681 when type in ["Like", "EmojiReact", "Announce"] do
682 with :ok <- ObjectValidator.fetch_actor_and_object(data),
683 {:ok, activity, _meta} <-
684 Pipeline.common_pipeline(data, local: false) do
685 {:ok, activity}
686 else
687 e -> {:error, e}
688 end
689 end
690
691 def handle_incoming(
692 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
693 data,
694 _options
695 )
696 when object_type in [
697 "Person",
698 "Application",
699 "Service",
700 "Organization"
701 ] do
702 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
703 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
704
705 actor
706 |> User.remote_user_changeset(new_user_data)
707 |> User.update_and_set_cache()
708
709 ActivityPub.update(%{
710 local: false,
711 to: data["to"] || [],
712 cc: data["cc"] || [],
713 object: object,
714 actor: actor_id,
715 activity_id: data["id"]
716 })
717 else
718 e ->
719 Logger.error(e)
720 :error
721 end
722 end
723
724 def handle_incoming(
725 %{"type" => "Delete"} = data,
726 _options
727 ) do
728 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
729 {:ok, activity}
730 else
731 {:error, {:validate_object, _}} = e ->
732 # Check if we have a create activity for this
733 with {:ok, object_id} <- Types.ObjectID.cast(data["object"]),
734 %Activity{data: %{"actor" => actor}} <-
735 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
736 # We have one, insert a tombstone and retry
737 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
738 {:ok, _tombstone} <- Object.create(tombstone_data) do
739 handle_incoming(data)
740 else
741 _ -> e
742 end
743 end
744 end
745
746 def handle_incoming(
747 %{
748 "type" => "Undo",
749 "object" => %{"type" => "Follow", "object" => followed},
750 "actor" => follower,
751 "id" => id
752 } = _data,
753 _options
754 ) do
755 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
756 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
757 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
758 User.unfollow(follower, followed)
759 {:ok, activity}
760 else
761 _e -> :error
762 end
763 end
764
765 def handle_incoming(
766 %{
767 "type" => "Undo",
768 "object" => %{"type" => type}
769 } = data,
770 _options
771 )
772 when type in ["Like", "EmojiReact", "Announce", "Block"] do
773 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
774 {:ok, activity}
775 end
776 end
777
778 # For Undos that don't have the complete object attached, try to find it in our database.
779 def handle_incoming(
780 %{
781 "type" => "Undo",
782 "object" => object
783 } = activity,
784 options
785 )
786 when is_binary(object) do
787 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
788 activity
789 |> Map.put("object", data)
790 |> handle_incoming(options)
791 else
792 _e -> :error
793 end
794 end
795
796 def handle_incoming(
797 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
798 _options
799 ) do
800 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
801 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
802 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
803 User.unfollow(blocker, blocked)
804 User.block(blocker, blocked)
805 {:ok, activity}
806 else
807 _e -> :error
808 end
809 end
810
811 def handle_incoming(
812 %{
813 "type" => "Move",
814 "actor" => origin_actor,
815 "object" => origin_actor,
816 "target" => target_actor
817 },
818 _options
819 ) do
820 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
821 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
822 true <- origin_actor in target_user.also_known_as do
823 ActivityPub.move(origin_user, target_user, false)
824 else
825 _e -> :error
826 end
827 end
828
829 def handle_incoming(_, _), do: :error
830
831 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
832 def get_obj_helper(id, options \\ []) do
833 case Object.normalize(id, true, options) do
834 %Object{} = object -> {:ok, object}
835 _ -> nil
836 end
837 end
838
839 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
840 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
841 ap_id: ap_id
842 })
843 when attributed_to == ap_id do
844 with {:ok, activity} <-
845 handle_incoming(%{
846 "type" => "Create",
847 "to" => data["to"],
848 "cc" => data["cc"],
849 "actor" => attributed_to,
850 "object" => data
851 }) do
852 {:ok, Object.normalize(activity)}
853 else
854 _ -> get_obj_helper(object_id)
855 end
856 end
857
858 def get_embedded_obj_helper(object_id, _) do
859 get_obj_helper(object_id)
860 end
861
862 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
863 with false <- String.starts_with?(in_reply_to, "http"),
864 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
865 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
866 else
867 _e -> object
868 end
869 end
870
871 def set_reply_to_uri(obj), do: obj
872
873 @doc """
874 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
875 Based on Mastodon's ActivityPub::NoteSerializer#replies.
876 """
877 def set_replies(obj_data) do
878 replies_uris =
879 with limit when limit > 0 <-
880 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
881 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
882 object
883 |> Object.self_replies()
884 |> select([o], fragment("?->>'id'", o.data))
885 |> limit(^limit)
886 |> Repo.all()
887 else
888 _ -> []
889 end
890
891 set_replies(obj_data, replies_uris)
892 end
893
894 defp set_replies(obj, []) do
895 obj
896 end
897
898 defp set_replies(obj, replies_uris) do
899 replies_collection = %{
900 "type" => "Collection",
901 "items" => replies_uris
902 }
903
904 Map.merge(obj, %{"replies" => replies_collection})
905 end
906
907 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
908 items
909 end
910
911 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
912 items
913 end
914
915 def replies(_), do: []
916
917 # Prepares the object of an outgoing create activity.
918 def prepare_object(object) do
919 object
920 |> set_sensitive
921 |> add_hashtags
922 |> add_mention_tags
923 |> add_emoji_tags
924 |> add_attributed_to
925 |> prepare_attachments
926 |> set_conversation
927 |> set_reply_to_uri
928 |> set_replies
929 |> strip_internal_fields
930 |> strip_internal_tags
931 |> set_type
932 end
933
934 # @doc
935 # """
936 # internal -> Mastodon
937 # """
938
939 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
940 when activity_type in ["Create", "Listen"] do
941 object =
942 object_id
943 |> Object.normalize()
944 |> Map.get(:data)
945 |> prepare_object
946
947 data =
948 data
949 |> Map.put("object", object)
950 |> Map.merge(Utils.make_json_ld_header())
951 |> Map.delete("bcc")
952
953 {:ok, data}
954 end
955
956 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
957 object =
958 object_id
959 |> Object.normalize()
960
961 data =
962 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
963 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
964 else
965 data |> maybe_fix_object_url
966 end
967
968 data =
969 data
970 |> strip_internal_fields
971 |> Map.merge(Utils.make_json_ld_header())
972 |> Map.delete("bcc")
973
974 {:ok, data}
975 end
976
977 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
978 # because of course it does.
979 def prepare_outgoing(%{"type" => "Accept"} = data) do
980 with follow_activity <- Activity.normalize(data["object"]) do
981 object = %{
982 "actor" => follow_activity.actor,
983 "object" => follow_activity.data["object"],
984 "id" => follow_activity.data["id"],
985 "type" => "Follow"
986 }
987
988 data =
989 data
990 |> Map.put("object", object)
991 |> Map.merge(Utils.make_json_ld_header())
992
993 {:ok, data}
994 end
995 end
996
997 def prepare_outgoing(%{"type" => "Reject"} = data) do
998 with follow_activity <- Activity.normalize(data["object"]) do
999 object = %{
1000 "actor" => follow_activity.actor,
1001 "object" => follow_activity.data["object"],
1002 "id" => follow_activity.data["id"],
1003 "type" => "Follow"
1004 }
1005
1006 data =
1007 data
1008 |> Map.put("object", object)
1009 |> Map.merge(Utils.make_json_ld_header())
1010
1011 {:ok, data}
1012 end
1013 end
1014
1015 def prepare_outgoing(%{"type" => _type} = data) do
1016 data =
1017 data
1018 |> strip_internal_fields
1019 |> maybe_fix_object_url
1020 |> Map.merge(Utils.make_json_ld_header())
1021
1022 {:ok, data}
1023 end
1024
1025 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
1026 with false <- String.starts_with?(object, "http"),
1027 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
1028 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
1029 relative_object do
1030 Map.put(data, "object", external_url)
1031 else
1032 {:fetch, e} ->
1033 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1034 data
1035
1036 _ ->
1037 data
1038 end
1039 end
1040
1041 def maybe_fix_object_url(data), do: data
1042
1043 def add_hashtags(object) do
1044 tags =
1045 (object["tag"] || [])
1046 |> Enum.map(fn
1047 # Expand internal representation tags into AS2 tags.
1048 tag when is_binary(tag) ->
1049 %{
1050 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1051 "name" => "##{tag}",
1052 "type" => "Hashtag"
1053 }
1054
1055 # Do not process tags which are already AS2 tag objects.
1056 tag when is_map(tag) ->
1057 tag
1058 end)
1059
1060 Map.put(object, "tag", tags)
1061 end
1062
1063 # TODO These should be added on our side on insertion, it doesn't make much
1064 # sense to regenerate these all the time
1065 def add_mention_tags(object) do
1066 to = object["to"] || []
1067 cc = object["cc"] || []
1068 mentioned = User.get_users_from_set(to ++ cc, local_only: false)
1069
1070 mentions = Enum.map(mentioned, &build_mention_tag/1)
1071
1072 tags = object["tag"] || []
1073 Map.put(object, "tag", tags ++ mentions)
1074 end
1075
1076 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1077 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1078 end
1079
1080 def take_emoji_tags(%User{emoji: emoji}) do
1081 emoji
1082 |> Map.to_list()
1083 |> Enum.map(&build_emoji_tag/1)
1084 end
1085
1086 # TODO: we should probably send mtime instead of unix epoch time for updated
1087 def add_emoji_tags(%{"emoji" => emoji} = object) do
1088 tags = object["tag"] || []
1089
1090 out = Enum.map(emoji, &build_emoji_tag/1)
1091
1092 Map.put(object, "tag", tags ++ out)
1093 end
1094
1095 def add_emoji_tags(object), do: object
1096
1097 defp build_emoji_tag({name, url}) do
1098 %{
1099 "icon" => %{"url" => url, "type" => "Image"},
1100 "name" => ":" <> name <> ":",
1101 "type" => "Emoji",
1102 "updated" => "1970-01-01T00:00:00Z",
1103 "id" => url
1104 }
1105 end
1106
1107 def set_conversation(object) do
1108 Map.put(object, "conversation", object["context"])
1109 end
1110
1111 def set_sensitive(%{"sensitive" => true} = object) do
1112 object
1113 end
1114
1115 def set_sensitive(object) do
1116 tags = object["tag"] || []
1117 Map.put(object, "sensitive", "nsfw" in tags)
1118 end
1119
1120 def set_type(%{"type" => "Answer"} = object) do
1121 Map.put(object, "type", "Note")
1122 end
1123
1124 def set_type(object), do: object
1125
1126 def add_attributed_to(object) do
1127 attributed_to = object["attributedTo"] || object["actor"]
1128 Map.put(object, "attributedTo", attributed_to)
1129 end
1130
1131 # TODO: Revisit this
1132 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
1133
1134 def prepare_attachments(object) do
1135 attachments =
1136 object
1137 |> Map.get("attachment", [])
1138 |> Enum.map(fn data ->
1139 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1140
1141 %{
1142 "url" => href,
1143 "mediaType" => media_type,
1144 "name" => data["name"],
1145 "type" => "Document"
1146 }
1147 end)
1148
1149 Map.put(object, "attachment", attachments)
1150 end
1151
1152 def strip_internal_fields(object) do
1153 Map.drop(object, Pleroma.Constants.object_internal_fields())
1154 end
1155
1156 defp strip_internal_tags(%{"tag" => tags} = object) do
1157 tags = Enum.filter(tags, fn x -> is_map(x) end)
1158
1159 Map.put(object, "tag", tags)
1160 end
1161
1162 defp strip_internal_tags(object), do: object
1163
1164 def perform(:user_upgrade, user) do
1165 # we pass a fake user so that the followers collection is stripped away
1166 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1167
1168 from(
1169 a in Activity,
1170 where: ^old_follower_address in a.recipients,
1171 update: [
1172 set: [
1173 recipients:
1174 fragment(
1175 "array_replace(?,?,?)",
1176 a.recipients,
1177 ^old_follower_address,
1178 ^user.follower_address
1179 )
1180 ]
1181 ]
1182 )
1183 |> Repo.update_all([])
1184 end
1185
1186 def upgrade_user_from_ap_id(ap_id) do
1187 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1188 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1189 {:ok, user} <- update_user(user, data) do
1190 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1191 {:ok, user}
1192 else
1193 %User{} = user -> {:ok, user}
1194 e -> e
1195 end
1196 end
1197
1198 defp update_user(user, data) do
1199 user
1200 |> User.remote_user_changeset(data)
1201 |> User.update_and_set_cache()
1202 end
1203
1204 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1205 Map.put(data, "url", url["href"])
1206 end
1207
1208 def maybe_fix_user_url(data), do: data
1209
1210 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1211 end