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