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