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