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