transmogrifier: Fixing votes from Note to Answer
[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.valid?(url["mediaType"]) -> url["mediaType"]
207 MIME.valid?(data["mediaType"]) -> data["mediaType"]
208 MIME.valid?(data["mimeType"]) -> data["mimeType"]
209 true -> nil
210 end
211
212 href =
213 cond do
214 is_map(url) && is_binary(url["href"]) -> url["href"]
215 is_binary(data["url"]) -> data["url"]
216 is_binary(data["href"]) -> data["href"]
217 true -> nil
218 end
219
220 if href do
221 attachment_url =
222 %{
223 "href" => href,
224 "type" => Map.get(url || %{}, "type", "Link")
225 }
226 |> Maps.put_if_present("mediaType", media_type)
227
228 %{
229 "url" => [attachment_url],
230 "type" => data["type"] || "Document"
231 }
232 |> Maps.put_if_present("mediaType", media_type)
233 |> Maps.put_if_present("name", data["name"])
234 |> Maps.put_if_present("blurhash", data["blurhash"])
235 else
236 nil
237 end
238 end)
239 |> Enum.filter(& &1)
240
241 Map.put(object, "attachment", attachments)
242 end
243
244 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
245 object
246 |> Map.put("attachment", [attachment])
247 |> fix_attachments()
248 end
249
250 def fix_attachments(object), do: object
251
252 def fix_url(%{"url" => url} = object) when is_map(url) do
253 Map.put(object, "url", url["href"])
254 end
255
256 def fix_url(%{"url" => url} = object) when is_list(url) do
257 first_element = Enum.at(url, 0)
258
259 url_string =
260 cond do
261 is_bitstring(first_element) -> first_element
262 is_map(first_element) -> first_element["href"] || ""
263 true -> ""
264 end
265
266 Map.put(object, "url", url_string)
267 end
268
269 def fix_url(object), do: object
270
271 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
272 emoji =
273 tags
274 |> Enum.filter(fn data -> is_map(data) and data["type"] == "Emoji" and data["icon"] end)
275 |> Enum.reduce(%{}, fn data, mapping ->
276 name = String.trim(data["name"], ":")
277
278 Map.put(mapping, name, data["icon"]["url"])
279 end)
280
281 Map.put(object, "emoji", emoji)
282 end
283
284 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
285 name = String.trim(tag["name"], ":")
286 emoji = %{name => tag["icon"]["url"]}
287
288 Map.put(object, "emoji", emoji)
289 end
290
291 def fix_emoji(object), do: object
292
293 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
294 tags =
295 tag
296 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
297 |> Enum.map(fn
298 %{"name" => "#" <> hashtag} -> String.downcase(hashtag)
299 %{"name" => hashtag} -> String.downcase(hashtag)
300 end)
301
302 Map.put(object, "tag", tag ++ tags)
303 end
304
305 def fix_tag(%{"tag" => %{} = tag} = object) do
306 object
307 |> Map.put("tag", [tag])
308 |> fix_tag
309 end
310
311 def fix_tag(object), do: object
312
313 # content map usually only has one language so this will do for now.
314 def fix_content_map(%{"contentMap" => content_map} = object) do
315 content_groups = Map.to_list(content_map)
316 {_, content} = Enum.at(content_groups, 0)
317
318 Map.put(object, "content", content)
319 end
320
321 def fix_content_map(object), do: object
322
323 defp fix_type(%{"type" => "Note", "inReplyTo" => reply_id, "name" => _} = object, options)
324 when is_binary(reply_id) do
325 options = Keyword.put(options, :fetch, true)
326
327 with %Object{data: %{"type" => "Question"}} <- Object.normalize(reply_id, options) do
328 Map.put(object, "type", "Answer")
329 else
330 _ -> object
331 end
332 end
333
334 defp fix_type(object, _options), do: object
335
336 # Reduce the object list to find the reported user.
337 defp get_reported(objects) do
338 Enum.reduce_while(objects, nil, fn ap_id, _ ->
339 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
340 {:halt, user}
341 else
342 _ -> {:cont, nil}
343 end
344 end)
345 end
346
347 # Compatibility wrapper for Mastodon votes
348 defp handle_create(%{"object" => %{"type" => "Answer"}} = data, _user) do
349 handle_incoming(data)
350 end
351
352 defp handle_create(%{"object" => object} = data, user) do
353 %{
354 to: data["to"],
355 object: object,
356 actor: user,
357 context: object["context"],
358 local: false,
359 published: data["published"],
360 additional:
361 Map.take(data, [
362 "cc",
363 "directMessage",
364 "id"
365 ])
366 }
367 |> ActivityPub.create()
368 end
369
370 def handle_incoming(data, options \\ [])
371
372 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
373 # with nil ID.
374 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
375 with context <- data["context"] || Utils.generate_context_id(),
376 content <- data["content"] || "",
377 %User{} = actor <- User.get_cached_by_ap_id(actor),
378 # Reduce the object list to find the reported user.
379 %User{} = account <- get_reported(objects),
380 # Remove the reported user from the object list.
381 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
382 %{
383 actor: actor,
384 context: context,
385 account: account,
386 statuses: statuses,
387 content: content,
388 additional: %{"cc" => [account.ap_id]}
389 }
390 |> ActivityPub.flag()
391 end
392 end
393
394 # disallow objects with bogus IDs
395 def handle_incoming(%{"id" => nil}, _options), do: :error
396 def handle_incoming(%{"id" => ""}, _options), do: :error
397 # length of https:// = 8, should validate better, but good enough for now.
398 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
399 do: :error
400
401 # TODO: validate those with a Ecto scheme
402 # - tags
403 # - emoji
404 def handle_incoming(
405 %{"type" => "Create", "object" => %{"type" => "Page"} = object} = data,
406 options
407 ) do
408 actor = Containment.get_actor(data)
409
410 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
411 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do
412 data =
413 data
414 |> Map.put("object", fix_object(object, options))
415 |> Map.put("actor", actor)
416 |> fix_addressing()
417
418 with {:ok, created_activity} <- handle_create(data, user) do
419 reply_depth = (options[:depth] || 0) + 1
420
421 if Federator.allowed_thread_distance?(reply_depth) do
422 for reply_id <- replies(object) do
423 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
424 "id" => reply_id,
425 "depth" => reply_depth
426 })
427 end
428 end
429
430 {:ok, created_activity}
431 end
432 else
433 %Activity{} = activity -> {:ok, activity}
434 _e -> :error
435 end
436 end
437
438 def handle_incoming(
439 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
440 options
441 ) do
442 actor = Containment.get_actor(data)
443
444 data =
445 Map.put(data, "actor", actor)
446 |> fix_addressing
447
448 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
449 reply_depth = (options[:depth] || 0) + 1
450 options = Keyword.put(options, :depth, reply_depth)
451 object = fix_object(object, options)
452
453 params = %{
454 to: data["to"],
455 object: object,
456 actor: user,
457 context: nil,
458 local: false,
459 published: data["published"],
460 additional: Map.take(data, ["cc", "id"])
461 }
462
463 ActivityPub.listen(params)
464 else
465 _e -> :error
466 end
467 end
468
469 @misskey_reactions %{
470 "like" => "👍",
471 "love" => "❤️",
472 "laugh" => "😆",
473 "hmm" => "🤔",
474 "surprise" => "😮",
475 "congrats" => "🎉",
476 "angry" => "💢",
477 "confused" => "😥",
478 "rip" => "😇",
479 "pudding" => "🍮",
480 "star" => "⭐"
481 }
482
483 @doc "Rewrite misskey likes into EmojiReacts"
484 def handle_incoming(
485 %{
486 "type" => "Like",
487 "_misskey_reaction" => reaction
488 } = data,
489 options
490 ) do
491 data
492 |> Map.put("type", "EmojiReact")
493 |> Map.put("content", @misskey_reactions[reaction] || reaction)
494 |> handle_incoming(options)
495 end
496
497 def handle_incoming(
498 %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
499 options
500 )
501 when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note} do
502 fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
503
504 object =
505 data["object"]
506 |> strip_internal_fields()
507 |> fix_type(fetch_options)
508 |> fix_in_reply_to(fetch_options)
509
510 data = Map.put(data, "object", object)
511 options = Keyword.put(options, :local, false)
512
513 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
514 nil <- Activity.get_create_by_object_ap_id(obj_id),
515 {:ok, activity, _} <- Pipeline.common_pipeline(data, options) do
516 {:ok, activity}
517 else
518 %Activity{} = activity -> {:ok, activity}
519 e -> e
520 end
521 end
522
523 def handle_incoming(%{"type" => type} = data, _options)
524 when type in ~w{Like EmojiReact Announce} do
525 with :ok <- ObjectValidator.fetch_actor_and_object(data),
526 {:ok, activity, _meta} <-
527 Pipeline.common_pipeline(data, local: false) do
528 {:ok, activity}
529 else
530 e -> {:error, e}
531 end
532 end
533
534 def handle_incoming(
535 %{"type" => type} = data,
536 _options
537 )
538 when type in ~w{Update Block Follow Accept Reject} do
539 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
540 {:ok, activity, _} <-
541 Pipeline.common_pipeline(data, local: false) do
542 {:ok, activity}
543 end
544 end
545
546 def handle_incoming(
547 %{"type" => "Delete"} = data,
548 _options
549 ) do
550 with {:ok, activity, _} <-
551 Pipeline.common_pipeline(data, local: false) do
552 {:ok, activity}
553 else
554 {:error, {:validate, _}} = e ->
555 # Check if we have a create activity for this
556 with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
557 %Activity{data: %{"actor" => actor}} <-
558 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
559 # We have one, insert a tombstone and retry
560 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
561 {:ok, _tombstone} <- Object.create(tombstone_data) do
562 handle_incoming(data)
563 else
564 _ -> e
565 end
566 end
567 end
568
569 def handle_incoming(
570 %{
571 "type" => "Undo",
572 "object" => %{"type" => "Follow", "object" => followed},
573 "actor" => follower,
574 "id" => id
575 } = _data,
576 _options
577 ) do
578 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
579 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
580 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
581 User.unfollow(follower, followed)
582 {:ok, activity}
583 else
584 _e -> :error
585 end
586 end
587
588 def handle_incoming(
589 %{
590 "type" => "Undo",
591 "object" => %{"type" => type}
592 } = data,
593 _options
594 )
595 when type in ["Like", "EmojiReact", "Announce", "Block"] do
596 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
597 {:ok, activity}
598 end
599 end
600
601 # For Undos that don't have the complete object attached, try to find it in our database.
602 def handle_incoming(
603 %{
604 "type" => "Undo",
605 "object" => object
606 } = activity,
607 options
608 )
609 when is_binary(object) do
610 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
611 activity
612 |> Map.put("object", data)
613 |> handle_incoming(options)
614 else
615 _e -> :error
616 end
617 end
618
619 def handle_incoming(
620 %{
621 "type" => "Move",
622 "actor" => origin_actor,
623 "object" => origin_actor,
624 "target" => target_actor
625 },
626 _options
627 ) do
628 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
629 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
630 true <- origin_actor in target_user.also_known_as do
631 ActivityPub.move(origin_user, target_user, false)
632 else
633 _e -> :error
634 end
635 end
636
637 def handle_incoming(_, _), do: :error
638
639 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
640 def get_obj_helper(id, options \\ []) do
641 options = Keyword.put(options, :fetch, true)
642
643 case Object.normalize(id, options) do
644 %Object{} = object -> {:ok, object}
645 _ -> nil
646 end
647 end
648
649 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
650 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
651 ap_id: ap_id
652 })
653 when attributed_to == ap_id do
654 with {:ok, activity} <-
655 handle_incoming(%{
656 "type" => "Create",
657 "to" => data["to"],
658 "cc" => data["cc"],
659 "actor" => attributed_to,
660 "object" => data
661 }) do
662 {:ok, Object.normalize(activity, fetch: false)}
663 else
664 _ -> get_obj_helper(object_id)
665 end
666 end
667
668 def get_embedded_obj_helper(object_id, _) do
669 get_obj_helper(object_id)
670 end
671
672 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
673 with false <- String.starts_with?(in_reply_to, "http"),
674 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
675 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
676 else
677 _e -> object
678 end
679 end
680
681 def set_reply_to_uri(obj), do: obj
682
683 @doc """
684 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
685 Based on Mastodon's ActivityPub::NoteSerializer#replies.
686 """
687 def set_replies(obj_data) do
688 replies_uris =
689 with limit when limit > 0 <-
690 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
691 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
692 object
693 |> Object.self_replies()
694 |> select([o], fragment("?->>'id'", o.data))
695 |> limit(^limit)
696 |> Repo.all()
697 else
698 _ -> []
699 end
700
701 set_replies(obj_data, replies_uris)
702 end
703
704 defp set_replies(obj, []) do
705 obj
706 end
707
708 defp set_replies(obj, replies_uris) do
709 replies_collection = %{
710 "type" => "Collection",
711 "items" => replies_uris
712 }
713
714 Map.merge(obj, %{"replies" => replies_collection})
715 end
716
717 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
718 items
719 end
720
721 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
722 items
723 end
724
725 def replies(_), do: []
726
727 # Prepares the object of an outgoing create activity.
728 def prepare_object(object) do
729 object
730 |> add_hashtags
731 |> add_mention_tags
732 |> add_emoji_tags
733 |> add_attributed_to
734 |> prepare_attachments
735 |> set_conversation
736 |> set_reply_to_uri
737 |> set_replies
738 |> strip_internal_fields
739 |> strip_internal_tags
740 |> set_type
741 end
742
743 # @doc
744 # """
745 # internal -> Mastodon
746 # """
747
748 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
749 when activity_type in ["Create", "Listen"] do
750 object =
751 object_id
752 |> Object.normalize(fetch: false)
753 |> Map.get(:data)
754 |> prepare_object
755
756 data =
757 data
758 |> Map.put("object", object)
759 |> Map.merge(Utils.make_json_ld_header())
760 |> Map.delete("bcc")
761
762 {:ok, data}
763 end
764
765 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
766 object =
767 object_id
768 |> Object.normalize(fetch: false)
769
770 data =
771 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
772 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
773 else
774 data |> maybe_fix_object_url
775 end
776
777 data =
778 data
779 |> strip_internal_fields
780 |> Map.merge(Utils.make_json_ld_header())
781 |> Map.delete("bcc")
782
783 {:ok, data}
784 end
785
786 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
787 # because of course it does.
788 def prepare_outgoing(%{"type" => "Accept"} = data) do
789 with follow_activity <- Activity.normalize(data["object"]) do
790 object = %{
791 "actor" => follow_activity.actor,
792 "object" => follow_activity.data["object"],
793 "id" => follow_activity.data["id"],
794 "type" => "Follow"
795 }
796
797 data =
798 data
799 |> Map.put("object", object)
800 |> Map.merge(Utils.make_json_ld_header())
801
802 {:ok, data}
803 end
804 end
805
806 def prepare_outgoing(%{"type" => "Reject"} = data) do
807 with follow_activity <- Activity.normalize(data["object"]) do
808 object = %{
809 "actor" => follow_activity.actor,
810 "object" => follow_activity.data["object"],
811 "id" => follow_activity.data["id"],
812 "type" => "Follow"
813 }
814
815 data =
816 data
817 |> Map.put("object", object)
818 |> Map.merge(Utils.make_json_ld_header())
819
820 {:ok, data}
821 end
822 end
823
824 def prepare_outgoing(%{"type" => _type} = data) do
825 data =
826 data
827 |> strip_internal_fields
828 |> maybe_fix_object_url
829 |> Map.merge(Utils.make_json_ld_header())
830
831 {:ok, data}
832 end
833
834 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
835 with false <- String.starts_with?(object, "http"),
836 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
837 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
838 relative_object do
839 Map.put(data, "object", external_url)
840 else
841 {:fetch, e} ->
842 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
843 data
844
845 _ ->
846 data
847 end
848 end
849
850 def maybe_fix_object_url(data), do: data
851
852 def add_hashtags(object) do
853 tags =
854 (object["tag"] || [])
855 |> Enum.map(fn
856 # Expand internal representation tags into AS2 tags.
857 tag when is_binary(tag) ->
858 %{
859 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
860 "name" => "##{tag}",
861 "type" => "Hashtag"
862 }
863
864 # Do not process tags which are already AS2 tag objects.
865 tag when is_map(tag) ->
866 tag
867 end)
868
869 Map.put(object, "tag", tags)
870 end
871
872 # TODO These should be added on our side on insertion, it doesn't make much
873 # sense to regenerate these all the time
874 def add_mention_tags(object) do
875 to = object["to"] || []
876 cc = object["cc"] || []
877 mentioned = User.get_users_from_set(to ++ cc, local_only: false)
878
879 mentions = Enum.map(mentioned, &build_mention_tag/1)
880
881 tags = object["tag"] || []
882 Map.put(object, "tag", tags ++ mentions)
883 end
884
885 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
886 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
887 end
888
889 def take_emoji_tags(%User{emoji: emoji}) do
890 emoji
891 |> Map.to_list()
892 |> Enum.map(&build_emoji_tag/1)
893 end
894
895 # TODO: we should probably send mtime instead of unix epoch time for updated
896 def add_emoji_tags(%{"emoji" => emoji} = object) do
897 tags = object["tag"] || []
898
899 out = Enum.map(emoji, &build_emoji_tag/1)
900
901 Map.put(object, "tag", tags ++ out)
902 end
903
904 def add_emoji_tags(object), do: object
905
906 defp build_emoji_tag({name, url}) do
907 %{
908 "icon" => %{"url" => "#{URI.encode(url)}", "type" => "Image"},
909 "name" => ":" <> name <> ":",
910 "type" => "Emoji",
911 "updated" => "1970-01-01T00:00:00Z",
912 "id" => url
913 }
914 end
915
916 def set_conversation(object) do
917 Map.put(object, "conversation", object["context"])
918 end
919
920 def set_type(%{"type" => "Answer"} = object) do
921 Map.put(object, "type", "Note")
922 end
923
924 def set_type(object), do: object
925
926 def add_attributed_to(object) do
927 attributed_to = object["attributedTo"] || object["actor"]
928 Map.put(object, "attributedTo", attributed_to)
929 end
930
931 # TODO: Revisit this
932 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
933
934 def prepare_attachments(object) do
935 attachments =
936 object
937 |> Map.get("attachment", [])
938 |> Enum.map(fn data ->
939 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
940
941 %{
942 "url" => href,
943 "mediaType" => media_type,
944 "name" => data["name"],
945 "type" => "Document"
946 }
947 end)
948
949 Map.put(object, "attachment", attachments)
950 end
951
952 def strip_internal_fields(object) do
953 Map.drop(object, Pleroma.Constants.object_internal_fields())
954 end
955
956 defp strip_internal_tags(%{"tag" => tags} = object) do
957 tags = Enum.filter(tags, fn x -> is_map(x) end)
958
959 Map.put(object, "tag", tags)
960 end
961
962 defp strip_internal_tags(object), do: object
963
964 def perform(:user_upgrade, user) do
965 # we pass a fake user so that the followers collection is stripped away
966 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
967
968 from(
969 a in Activity,
970 where: ^old_follower_address in a.recipients,
971 update: [
972 set: [
973 recipients:
974 fragment(
975 "array_replace(?,?,?)",
976 a.recipients,
977 ^old_follower_address,
978 ^user.follower_address
979 )
980 ]
981 ]
982 )
983 |> Repo.update_all([])
984 end
985
986 def upgrade_user_from_ap_id(ap_id) do
987 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
988 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
989 {:ok, user} <- update_user(user, data) do
990 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
991 {:ok, user}
992 else
993 %User{} = user -> {:ok, user}
994 e -> e
995 end
996 end
997
998 defp update_user(user, data) do
999 user
1000 |> User.remote_user_changeset(data)
1001 |> User.update_and_set_cache()
1002 end
1003
1004 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1005 Map.put(data, "url", url["href"])
1006 end
1007
1008 def maybe_fix_user_url(data), do: data
1009
1010 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1011 end