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