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