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