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