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