add outbound reacts
[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 @doc "Rewrite misskey likes into EmojiReacts"
419 def handle_incoming(
420 %{
421 "type" => "Like",
422 "_misskey_reaction" => reaction,
423 "tag" => _
424 } = data,
425 options
426 ) do
427 IO.inspect(data)
428 data
429 |> Map.put("type", "EmojiReact")
430 |> Map.put("content", reaction)
431 |> handle_incoming(options)
432 end
433
434 def handle_incoming(
435 %{
436 "type" => "Like",
437 "_misskey_reaction" => reaction,
438 } = data,
439 options
440 ) do
441 data
442 |> Map.put("type", "EmojiReact")
443 |> Map.put("content", reaction)
444 |> handle_incoming(options)
445 end
446
447 def handle_incoming(
448 %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
449 options
450 )
451 when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page} do
452 fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
453
454 object =
455 data["object"]
456 |> strip_internal_fields()
457 |> fix_type(fetch_options)
458 |> fix_in_reply_to(fetch_options)
459
460 data = Map.put(data, "object", object)
461 options = Keyword.put(options, :local, false)
462
463 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
464 nil <- Activity.get_create_by_object_ap_id(obj_id),
465 {:ok, activity, _} <- Pipeline.common_pipeline(data, options) do
466 {:ok, activity}
467 else
468 %Activity{} = activity -> {:ok, activity}
469 e -> e
470 end
471 end
472
473 def handle_incoming(%{"type" => type} = data, _options)
474 when type in ~w{Like EmojiReact Announce Add Remove} do
475 with :ok <- ObjectValidator.fetch_actor_and_object(data),
476 {:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do
477 {:ok, activity}
478 else
479 e ->
480 {:error, e}
481 end
482 end
483
484 def handle_incoming(
485 %{"type" => type} = data,
486 _options
487 )
488 when type in ~w{Update Block Follow Accept Reject} do
489 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
490 {:ok, activity, _} <-
491 Pipeline.common_pipeline(data, local: false) do
492 {:ok, activity}
493 end
494 end
495
496 def handle_incoming(
497 %{"type" => "Delete"} = data,
498 _options
499 ) do
500 with {:ok, activity, _} <-
501 Pipeline.common_pipeline(data, local: false) do
502 {:ok, activity}
503 else
504 {:error, {:validate, _}} = e ->
505 # Check if we have a create activity for this
506 with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
507 %Activity{data: %{"actor" => actor}} <-
508 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
509 # We have one, insert a tombstone and retry
510 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
511 {:ok, _tombstone} <- Object.create(tombstone_data) do
512 handle_incoming(data)
513 else
514 _ -> e
515 end
516 end
517 end
518
519 def handle_incoming(
520 %{
521 "type" => "Undo",
522 "object" => %{"type" => "Follow", "object" => followed},
523 "actor" => follower,
524 "id" => id
525 } = _data,
526 _options
527 ) do
528 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
529 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
530 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
531 User.unfollow(follower, followed)
532 {:ok, activity}
533 else
534 _e -> :error
535 end
536 end
537
538 def handle_incoming(
539 %{
540 "type" => "Undo",
541 "object" => %{"type" => type}
542 } = data,
543 _options
544 )
545 when type in ["Like", "EmojiReact", "Announce", "Block"] do
546 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
547 {:ok, activity}
548 end
549 end
550
551 # For Undos that don't have the complete object attached, try to find it in our database.
552 def handle_incoming(
553 %{
554 "type" => "Undo",
555 "object" => object
556 } = activity,
557 options
558 )
559 when is_binary(object) do
560 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
561 activity
562 |> Map.put("object", data)
563 |> handle_incoming(options)
564 else
565 _e -> :error
566 end
567 end
568
569 def handle_incoming(
570 %{
571 "type" => "Move",
572 "actor" => origin_actor,
573 "object" => origin_actor,
574 "target" => target_actor
575 },
576 _options
577 ) do
578 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
579 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
580 true <- origin_actor in target_user.also_known_as do
581 ActivityPub.move(origin_user, target_user, false)
582 else
583 _e -> :error
584 end
585 end
586
587 def handle_incoming(_, _), do: :error
588
589 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
590 def get_obj_helper(id, options \\ []) do
591 options = Keyword.put(options, :fetch, true)
592
593 case Object.normalize(id, options) do
594 %Object{} = object -> {:ok, object}
595 _ -> nil
596 end
597 end
598
599 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
600 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
601 ap_id: ap_id
602 })
603 when attributed_to == ap_id do
604 with {:ok, activity} <-
605 handle_incoming(%{
606 "type" => "Create",
607 "to" => data["to"],
608 "cc" => data["cc"],
609 "actor" => attributed_to,
610 "object" => data
611 }) do
612 {:ok, Object.normalize(activity, fetch: false)}
613 else
614 _ -> get_obj_helper(object_id)
615 end
616 end
617
618 def get_embedded_obj_helper(object_id, _) do
619 get_obj_helper(object_id)
620 end
621
622 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
623 with false <- String.starts_with?(in_reply_to, "http"),
624 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
625 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
626 else
627 _e -> object
628 end
629 end
630
631 def set_reply_to_uri(obj), do: obj
632
633 @doc """
634 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
635 Based on Mastodon's ActivityPub::NoteSerializer#replies.
636 """
637 def set_replies(obj_data) do
638 replies_uris =
639 with limit when limit > 0 <-
640 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
641 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
642 object
643 |> Object.self_replies()
644 |> select([o], fragment("?->>'id'", o.data))
645 |> limit(^limit)
646 |> Repo.all()
647 else
648 _ -> []
649 end
650
651 set_replies(obj_data, replies_uris)
652 end
653
654 defp set_replies(obj, []) do
655 obj
656 end
657
658 defp set_replies(obj, replies_uris) do
659 replies_collection = %{
660 "type" => "Collection",
661 "items" => replies_uris
662 }
663
664 Map.merge(obj, %{"replies" => replies_collection})
665 end
666
667 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
668 items
669 end
670
671 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
672 items
673 end
674
675 def replies(_), do: []
676
677 # Prepares the object of an outgoing create activity.
678 def prepare_object(object) do
679 object
680 |> add_hashtags
681 |> add_mention_tags
682 |> add_emoji_tags
683 |> add_attributed_to
684 |> prepare_attachments
685 |> set_conversation
686 |> set_reply_to_uri
687 |> set_replies
688 |> strip_internal_fields
689 |> strip_internal_tags
690 |> set_type
691 end
692
693 # @doc
694 # """
695 # internal -> Mastodon
696 # """
697
698 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
699 when activity_type in ["Create", "Listen"] do
700 object =
701 object_id
702 |> Object.normalize(fetch: false)
703 |> Map.get(:data)
704 |> prepare_object
705
706 data =
707 data
708 |> Map.put("object", object)
709 |> Map.merge(Utils.make_json_ld_header())
710 |> Map.delete("bcc")
711
712 {:ok, data}
713 end
714
715 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
716 object =
717 object_id
718 |> Object.normalize(fetch: false)
719
720 data =
721 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
722 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
723 else
724 data |> maybe_fix_object_url
725 end
726
727 data =
728 data
729 |> strip_internal_fields
730 |> Map.merge(Utils.make_json_ld_header())
731 |> Map.delete("bcc")
732
733 {:ok, data}
734 end
735
736 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
737 # because of course it does.
738 def prepare_outgoing(%{"type" => "Accept"} = data) do
739 with follow_activity <- Activity.normalize(data["object"]) do
740 object = %{
741 "actor" => follow_activity.actor,
742 "object" => follow_activity.data["object"],
743 "id" => follow_activity.data["id"],
744 "type" => "Follow"
745 }
746
747 data =
748 data
749 |> Map.put("object", object)
750 |> Map.merge(Utils.make_json_ld_header())
751
752 {:ok, data}
753 end
754 end
755
756 def prepare_outgoing(%{"type" => "Reject"} = data) do
757 with follow_activity <- Activity.normalize(data["object"]) do
758 object = %{
759 "actor" => follow_activity.actor,
760 "object" => follow_activity.data["object"],
761 "id" => follow_activity.data["id"],
762 "type" => "Follow"
763 }
764
765 data =
766 data
767 |> Map.put("object", object)
768 |> Map.merge(Utils.make_json_ld_header())
769
770 {:ok, data}
771 end
772 end
773
774 def prepare_outgoing(%{"type" => _type} = data) do
775 data =
776 data
777 |> strip_internal_fields
778 |> maybe_fix_object_url
779 |> Map.merge(Utils.make_json_ld_header())
780
781 {:ok, data}
782 end
783
784 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
785 with false <- String.starts_with?(object, "http"),
786 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
787 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
788 relative_object do
789 Map.put(data, "object", external_url)
790 else
791 {:fetch, e} ->
792 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
793 data
794
795 _ ->
796 data
797 end
798 end
799
800 def maybe_fix_object_url(data), do: data
801
802 def add_hashtags(object) do
803 tags =
804 (object["tag"] || [])
805 |> Enum.map(fn
806 # Expand internal representation tags into AS2 tags.
807 tag when is_binary(tag) ->
808 %{
809 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
810 "name" => "##{tag}",
811 "type" => "Hashtag"
812 }
813
814 # Do not process tags which are already AS2 tag objects.
815 tag when is_map(tag) ->
816 tag
817 end)
818
819 Map.put(object, "tag", tags)
820 end
821
822 # TODO These should be added on our side on insertion, it doesn't make much
823 # sense to regenerate these all the time
824 def add_mention_tags(object) do
825 to = object["to"] || []
826 cc = object["cc"] || []
827 mentioned = User.get_users_from_set(to ++ cc, local_only: false)
828
829 mentions = Enum.map(mentioned, &build_mention_tag/1)
830
831 tags = object["tag"] || []
832 Map.put(object, "tag", tags ++ mentions)
833 end
834
835 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
836 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
837 end
838
839 def take_emoji_tags(%User{emoji: emoji}) do
840 emoji
841 |> Map.to_list()
842 |> Enum.map(&build_emoji_tag/1)
843 end
844
845 # TODO: we should probably send mtime instead of unix epoch time for updated
846 def add_emoji_tags(%{"emoji" => emoji} = object) do
847 tags = object["tag"] || []
848
849 out = Enum.map(emoji, &build_emoji_tag/1)
850
851 Map.put(object, "tag", tags ++ out)
852 end
853
854 def add_emoji_tags(object), do: object
855
856 defp build_emoji_tag({name, url}) do
857 %{
858 "icon" => %{"url" => "#{URI.encode(url)}", "type" => "Image"},
859 "name" => ":" <> name <> ":",
860 "type" => "Emoji",
861 "updated" => "1970-01-01T00:00:00Z",
862 "id" => url
863 }
864 end
865
866 def set_conversation(object) do
867 Map.put(object, "conversation", object["context"])
868 end
869
870 def set_type(%{"type" => "Answer"} = object) do
871 Map.put(object, "type", "Note")
872 end
873
874 def set_type(object), do: object
875
876 def add_attributed_to(object) do
877 attributed_to = object["attributedTo"] || object["actor"]
878 Map.put(object, "attributedTo", attributed_to)
879 end
880
881 # TODO: Revisit this
882 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
883
884 def prepare_attachments(object) do
885 attachments =
886 object
887 |> Map.get("attachment", [])
888 |> Enum.map(fn data ->
889 [%{"mediaType" => media_type, "href" => href} = url | _] = data["url"]
890
891 %{
892 "url" => href,
893 "mediaType" => media_type,
894 "name" => data["name"],
895 "type" => "Document"
896 }
897 |> Maps.put_if_present("width", url["width"])
898 |> Maps.put_if_present("height", url["height"])
899 |> Maps.put_if_present("blurhash", data["blurhash"])
900 end)
901
902 Map.put(object, "attachment", attachments)
903 end
904
905 def strip_internal_fields(object) do
906 Map.drop(object, Pleroma.Constants.object_internal_fields())
907 end
908
909 defp strip_internal_tags(%{"tag" => tags} = object) do
910 tags = Enum.filter(tags, fn x -> is_map(x) end)
911
912 Map.put(object, "tag", tags)
913 end
914
915 defp strip_internal_tags(object), do: object
916
917 def perform(:user_upgrade, user) do
918 # we pass a fake user so that the followers collection is stripped away
919 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
920
921 from(
922 a in Activity,
923 where: ^old_follower_address in a.recipients,
924 update: [
925 set: [
926 recipients:
927 fragment(
928 "array_replace(?,?,?)",
929 a.recipients,
930 ^old_follower_address,
931 ^user.follower_address
932 )
933 ]
934 ]
935 )
936 |> Repo.update_all([])
937 end
938
939 def upgrade_user_from_ap_id(ap_id) do
940 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
941 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
942 {:ok, user} <- update_user(user, data) do
943 {:ok, _pid} = Task.start(fn -> ActivityPub.pinned_fetch_task(user) end)
944 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
945 {:ok, user}
946 else
947 %User{} = user -> {:ok, user}
948 e -> e
949 end
950 end
951
952 defp update_user(user, data) do
953 user
954 |> User.remote_user_changeset(data)
955 |> User.update_and_set_cache()
956 end
957
958 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
959 Map.put(data, "url", url["href"])
960 end
961
962 def maybe_fix_user_url(data), do: data
963
964 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
965 end