1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ActivityPub.Transmogrifier do
7 A module to handle coding from internal to wire ActivityPub and back.
10 alias Pleroma.EarmarkRenderer
11 alias Pleroma.FollowingRelationship
13 alias Pleroma.Notification
15 alias Pleroma.Object.Containment
18 alias Pleroma.Web.ActivityPub.ActivityPub
19 alias Pleroma.Web.ActivityPub.Builder
20 alias Pleroma.Web.ActivityPub.ObjectValidator
21 alias Pleroma.Web.ActivityPub.ObjectValidators.Types
22 alias Pleroma.Web.ActivityPub.Pipeline
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.Federator
26 alias Pleroma.Workers.TransmogrifierWorker
31 require Pleroma.Constants
34 Modifies an incoming AP object (mastodon format) to our internal format.
36 def fix_object(object, options \\ []) do
38 |> strip_internal_fields
43 |> fix_in_reply_to(options)
53 def fix_summary(%{"summary" => nil} = object) do
54 Map.put(object, "summary", "")
57 def fix_summary(%{"summary" => _} = object) do
58 # summary is present, nothing to do
62 def fix_summary(object), do: Map.put(object, "summary", "")
64 def fix_addressing_list(map, field) do
66 is_binary(map[field]) ->
67 Map.put(map, field, [map[field]])
70 Map.put(map, field, [])
77 def fix_explicit_addressing(
78 %{"to" => to, "cc" => cc} = object,
82 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)
88 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
92 |> Map.put("to", explicit_to)
93 |> Map.put("cc", final_cc)
96 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
98 # if directMessage flag is set to true, leave the addressing alone
99 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
101 def fix_explicit_addressing(object) do
102 explicit_mentions = Utils.determine_explicit_mentions(object)
104 %User{follower_address: follower_collection} =
106 |> Containment.get_actor()
107 |> User.get_cached_by_ap_id()
112 Pleroma.Constants.as_public(),
116 fix_explicit_addressing(object, explicit_mentions, follower_collection)
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
124 if followers_collection not in recipients do
126 Pleroma.Constants.as_public() in cc ->
127 to = to ++ [followers_collection]
128 Map.put(object, "to", to)
130 Pleroma.Constants.as_public() in to ->
131 cc = cc ++ [followers_collection]
132 Map.put(object, "cc", cc)
142 def fix_implicit_addressing(object, _), do: object
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)
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)
157 def fix_actor(%{"attributedTo" => actor} = object) do
158 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
161 def fix_in_reply_to(object, options \\ [])
163 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
164 when not is_nil(in_reply_to) do
165 in_reply_to_id = prepare_in_reply_to(in_reply_to)
166 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
167 depth = (options[:depth] || 0) + 1
169 if Federator.allowed_thread_distance?(depth) do
170 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
171 %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
173 |> Map.put("inReplyTo", replied_object.data["id"])
174 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
175 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
176 |> Map.put("context", replied_object.data["context"] || object["conversation"])
179 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
187 def fix_in_reply_to(object, _options), do: object
189 defp prepare_in_reply_to(in_reply_to) do
191 is_bitstring(in_reply_to) ->
194 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
197 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
198 Enum.at(in_reply_to, 0)
205 def fix_context(object) do
206 context = object["context"] || object["conversation"] || Utils.generate_context_id()
209 |> Map.put("context", context)
210 |> Map.put("conversation", context)
213 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
215 Enum.map(attachment, fn data ->
218 is_list(data["url"]) -> List.first(data["url"])
219 is_map(data["url"]) -> data["url"]
225 is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"]
226 MIME.valid?(data["mediaType"]) -> data["mediaType"]
227 MIME.valid?(data["mimeType"]) -> data["mimeType"]
233 is_map(url) && is_binary(url["href"]) -> url["href"]
234 is_binary(data["url"]) -> data["url"]
235 is_binary(data["href"]) -> data["href"]
240 |> Maps.put_if_present("mediaType", media_type)
241 |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
243 %{"url" => [attachment_url]}
244 |> Maps.put_if_present("mediaType", media_type)
245 |> Maps.put_if_present("type", data["type"])
246 |> Maps.put_if_present("name", data["name"])
249 Map.put(object, "attachment", attachments)
252 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
254 |> Map.put("attachment", [attachment])
258 def fix_attachments(object), do: object
260 def fix_url(%{"url" => url} = object) when is_map(url) do
261 Map.put(object, "url", url["href"])
264 def fix_url(%{"type" => object_type, "url" => url} = object)
265 when object_type in ["Video", "Audio"] and is_list(url) do
266 first_element = Enum.at(url, 0)
268 link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
271 |> Map.put("attachment", [first_element])
272 |> Map.put("url", link_element["href"])
275 def fix_url(%{"type" => object_type, "url" => url} = object)
276 when object_type != "Video" and is_list(url) do
277 first_element = Enum.at(url, 0)
281 is_bitstring(first_element) -> first_element
282 is_map(first_element) -> first_element["href"] || ""
286 Map.put(object, "url", url_string)
289 def fix_url(object), do: object
291 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
294 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
295 |> Enum.reduce(%{}, fn data, mapping ->
296 name = String.trim(data["name"], ":")
298 Map.put(mapping, name, data["icon"]["url"])
301 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
302 emoji = Map.merge(object["emoji"] || %{}, emoji)
304 Map.put(object, "emoji", emoji)
307 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
308 name = String.trim(tag["name"], ":")
309 emoji = %{name => tag["icon"]["url"]}
311 Map.put(object, "emoji", emoji)
314 def fix_emoji(object), do: object
316 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
319 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
320 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
322 Map.put(object, "tag", tag ++ tags)
325 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
326 combined = [tag, String.slice(hashtag, 1..-1)]
328 Map.put(object, "tag", combined)
331 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
333 def fix_tag(object), do: object
335 # content map usually only has one language so this will do for now.
336 def fix_content_map(%{"contentMap" => content_map} = object) do
337 content_groups = Map.to_list(content_map)
338 {_, content} = Enum.at(content_groups, 0)
340 Map.put(object, "content", content)
343 def fix_content_map(object), do: object
345 def fix_type(object, options \\ [])
347 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
348 when is_binary(reply_id) do
349 with true <- Federator.allowed_thread_distance?(options[:depth]),
350 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
351 Map.put(object, "type", "Answer")
357 def fix_type(object, _), do: object
359 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
360 when is_binary(content) do
363 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
364 |> Pleroma.HTML.filter_tags()
366 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
369 defp fix_content(object), do: object
371 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
372 with true <- id =~ "follows",
373 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
374 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
381 defp mastodon_follow_hack(_, _), do: {:error, nil}
383 defp get_follow_activity(follow_object, followed) do
384 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
385 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
388 # Can't find the activity. This might a Mastodon 2.3 "Accept"
390 mastodon_follow_hack(follow_object, followed)
397 # Reduce the object list to find the reported user.
398 defp get_reported(objects) do
399 Enum.reduce_while(objects, nil, fn ap_id, _ ->
400 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
408 def handle_incoming(data, options \\ [])
410 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
412 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
413 with context <- data["context"] || Utils.generate_context_id(),
414 content <- data["content"] || "",
415 %User{} = actor <- User.get_cached_by_ap_id(actor),
416 # Reduce the object list to find the reported user.
417 %User{} = account <- get_reported(objects),
418 # Remove the reported user from the object list.
419 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
426 additional: %{"cc" => [account.ap_id]}
428 |> ActivityPub.flag()
432 # disallow objects with bogus IDs
433 def handle_incoming(%{"id" => nil}, _options), do: :error
434 def handle_incoming(%{"id" => ""}, _options), do: :error
435 # length of https:// = 8, should validate better, but good enough for now.
436 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
439 # TODO: validate those with a Ecto scheme
443 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
446 when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
447 actor = Containment.get_actor(data)
450 Map.put(data, "actor", actor)
453 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
454 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
455 object = fix_object(object, options)
461 context: object["conversation"],
463 published: data["published"],
472 with {:ok, created_activity} <- ActivityPub.create(params) do
473 reply_depth = (options[:depth] || 0) + 1
475 if Federator.allowed_thread_distance?(reply_depth) do
476 for reply_id <- replies(object) do
477 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
479 "depth" => reply_depth
484 {:ok, created_activity}
487 %Activity{} = activity -> {:ok, activity}
493 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
496 actor = Containment.get_actor(data)
499 Map.put(data, "actor", actor)
502 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
503 reply_depth = (options[:depth] || 0) + 1
504 options = Keyword.put(options, :depth, reply_depth)
505 object = fix_object(object, options)
513 published: data["published"],
514 additional: Map.take(data, ["cc", "id"])
517 ActivityPub.listen(params)
524 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
527 with %User{local: true} = followed <-
528 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
529 {:ok, %User{} = follower} <-
530 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
532 ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
533 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
534 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
535 {_, false} <- {:user_locked, User.locked?(followed)},
536 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
538 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
539 {:ok, _relationship} <-
540 FollowingRelationship.update(follower, followed, :follow_accept) do
541 ActivityPub.accept(%{
542 to: [follower.ap_id],
548 {:user_blocked, true} ->
549 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
550 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
552 ActivityPub.reject(%{
553 to: [follower.ap_id],
559 {:follow, {:error, _}} ->
560 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
561 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
563 ActivityPub.reject(%{
564 to: [follower.ap_id],
570 {:user_locked, true} ->
571 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
575 ActivityPub.notify_and_stream(activity)
584 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
587 with actor <- Containment.get_actor(data),
588 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
589 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
590 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
591 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
592 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
593 User.update_follower_count(followed)
594 User.update_following_count(follower)
596 Notification.update_notification_type(followed, follow_activity)
598 ActivityPub.accept(%{
599 to: follow_activity.data["to"],
602 object: follow_activity.data["id"],
613 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
616 with actor <- Containment.get_actor(data),
617 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
618 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
619 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
620 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
621 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
623 ActivityPub.reject(%{
624 to: follow_activity.data["to"],
627 object: follow_activity.data["id"],
637 @misskey_reactions %{
651 @doc "Rewrite misskey likes into EmojiReacts"
655 "_misskey_reaction" => reaction
660 |> Map.put("type", "EmojiReact")
661 |> Map.put("content", @misskey_reactions[reaction] || reaction)
662 |> handle_incoming(options)
666 %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
669 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
670 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
675 def handle_incoming(%{"type" => type} = data, _options)
676 when type in ["Like", "EmojiReact", "Announce"] do
677 with :ok <- ObjectValidator.fetch_actor_and_object(data),
678 {:ok, activity, _meta} <-
679 Pipeline.common_pipeline(data, local: false) do
687 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
691 when object_type in [
697 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
698 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
701 |> User.remote_user_changeset(new_user_data)
702 |> User.update_and_set_cache()
704 ActivityPub.update(%{
706 to: data["to"] || [],
707 cc: data["cc"] || [],
710 activity_id: data["id"]
720 %{"type" => "Delete"} = data,
723 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
726 {:error, {:validate_object, _}} = e ->
727 # Check if we have a create activity for this
728 with {:ok, object_id} <- Types.ObjectID.cast(data["object"]),
729 %Activity{data: %{"actor" => actor}} <-
730 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
731 # We have one, insert a tombstone and retry
732 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
733 {:ok, _tombstone} <- Object.create(tombstone_data) do
734 handle_incoming(data)
744 "object" => %{"type" => "Follow", "object" => followed},
750 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
751 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
752 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
753 User.unfollow(follower, followed)
763 "object" => %{"type" => type}
767 when type in ["Like", "EmojiReact", "Announce", "Block"] do
768 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
773 # For Undos that don't have the complete object attached, try to find it in our database.
781 when is_binary(object) do
782 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
784 |> Map.put("object", data)
785 |> handle_incoming(options)
792 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
795 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
796 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
797 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
798 User.unfollow(blocker, blocked)
799 User.block(blocker, blocked)
809 "actor" => origin_actor,
810 "object" => origin_actor,
811 "target" => target_actor
815 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
816 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
817 true <- origin_actor in target_user.also_known_as do
818 ActivityPub.move(origin_user, target_user, false)
824 def handle_incoming(_, _), do: :error
826 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
827 def get_obj_helper(id, options \\ []) do
828 case Object.normalize(id, true, options) do
829 %Object{} = object -> {:ok, object}
834 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
835 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
838 when attributed_to == ap_id do
839 with {:ok, activity} <-
844 "actor" => attributed_to,
847 {:ok, Object.normalize(activity)}
849 _ -> get_obj_helper(object_id)
853 def get_embedded_obj_helper(object_id, _) do
854 get_obj_helper(object_id)
857 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
858 with false <- String.starts_with?(in_reply_to, "http"),
859 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
860 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
866 def set_reply_to_uri(obj), do: obj
869 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
870 Based on Mastodon's ActivityPub::NoteSerializer#replies.
872 def set_replies(obj_data) do
874 with limit when limit > 0 <-
875 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
876 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
878 |> Object.self_replies()
879 |> select([o], fragment("?->>'id'", o.data))
886 set_replies(obj_data, replies_uris)
889 defp set_replies(obj, []) do
893 defp set_replies(obj, replies_uris) do
894 replies_collection = %{
895 "type" => "Collection",
896 "items" => replies_uris
899 Map.merge(obj, %{"replies" => replies_collection})
902 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
906 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
910 def replies(_), do: []
912 # Prepares the object of an outgoing create activity.
913 def prepare_object(object) do
920 |> prepare_attachments
924 |> strip_internal_fields
925 |> strip_internal_tags
931 # internal -> Mastodon
934 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
935 when activity_type in ["Create", "Listen"] do
938 |> Object.normalize()
944 |> Map.put("object", object)
945 |> Map.merge(Utils.make_json_ld_header())
951 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
954 |> Object.normalize()
957 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
958 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
960 data |> maybe_fix_object_url
965 |> strip_internal_fields
966 |> Map.merge(Utils.make_json_ld_header())
972 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
973 # because of course it does.
974 def prepare_outgoing(%{"type" => "Accept"} = data) do
975 with follow_activity <- Activity.normalize(data["object"]) do
977 "actor" => follow_activity.actor,
978 "object" => follow_activity.data["object"],
979 "id" => follow_activity.data["id"],
985 |> Map.put("object", object)
986 |> Map.merge(Utils.make_json_ld_header())
992 def prepare_outgoing(%{"type" => "Reject"} = data) do
993 with follow_activity <- Activity.normalize(data["object"]) do
995 "actor" => follow_activity.actor,
996 "object" => follow_activity.data["object"],
997 "id" => follow_activity.data["id"],
1003 |> Map.put("object", object)
1004 |> Map.merge(Utils.make_json_ld_header())
1010 def prepare_outgoing(%{"type" => _type} = data) do
1013 |> strip_internal_fields
1014 |> maybe_fix_object_url
1015 |> Map.merge(Utils.make_json_ld_header())
1020 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
1021 with false <- String.starts_with?(object, "http"),
1022 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
1023 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
1025 Map.put(data, "object", external_url)
1028 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1036 def maybe_fix_object_url(data), do: data
1038 def add_hashtags(object) do
1040 (object["tag"] || [])
1042 # Expand internal representation tags into AS2 tags.
1043 tag when is_binary(tag) ->
1045 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1046 "name" => "##{tag}",
1050 # Do not process tags which are already AS2 tag objects.
1051 tag when is_map(tag) ->
1055 Map.put(object, "tag", tags)
1058 # TODO These should be added on our side on insertion, it doesn't make much
1059 # sense to regenerate these all the time
1060 def add_mention_tags(object) do
1061 to = object["to"] || []
1062 cc = object["cc"] || []
1063 mentioned = User.get_users_from_set(to ++ cc, local_only: false)
1065 mentions = Enum.map(mentioned, &build_mention_tag/1)
1067 tags = object["tag"] || []
1068 Map.put(object, "tag", tags ++ mentions)
1071 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1072 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1075 def take_emoji_tags(%User{emoji: emoji}) do
1078 |> Enum.map(&build_emoji_tag/1)
1081 # TODO: we should probably send mtime instead of unix epoch time for updated
1082 def add_emoji_tags(%{"emoji" => emoji} = object) do
1083 tags = object["tag"] || []
1085 out = Enum.map(emoji, &build_emoji_tag/1)
1087 Map.put(object, "tag", tags ++ out)
1090 def add_emoji_tags(object), do: object
1092 defp build_emoji_tag({name, url}) do
1094 "icon" => %{"url" => url, "type" => "Image"},
1095 "name" => ":" <> name <> ":",
1097 "updated" => "1970-01-01T00:00:00Z",
1102 def set_conversation(object) do
1103 Map.put(object, "conversation", object["context"])
1106 def set_sensitive(%{"sensitive" => true} = object) do
1110 def set_sensitive(object) do
1111 tags = object["tag"] || []
1112 Map.put(object, "sensitive", "nsfw" in tags)
1115 def set_type(%{"type" => "Answer"} = object) do
1116 Map.put(object, "type", "Note")
1119 def set_type(object), do: object
1121 def add_attributed_to(object) do
1122 attributed_to = object["attributedTo"] || object["actor"]
1123 Map.put(object, "attributedTo", attributed_to)
1126 # TODO: Revisit this
1127 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
1129 def prepare_attachments(object) do
1132 |> Map.get("attachment", [])
1133 |> Enum.map(fn data ->
1134 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1138 "mediaType" => media_type,
1139 "name" => data["name"],
1140 "type" => "Document"
1144 Map.put(object, "attachment", attachments)
1147 def strip_internal_fields(object) do
1148 Map.drop(object, Pleroma.Constants.object_internal_fields())
1151 defp strip_internal_tags(%{"tag" => tags} = object) do
1152 tags = Enum.filter(tags, fn x -> is_map(x) end)
1154 Map.put(object, "tag", tags)
1157 defp strip_internal_tags(object), do: object
1159 def perform(:user_upgrade, user) do
1160 # we pass a fake user so that the followers collection is stripped away
1161 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1165 where: ^old_follower_address in a.recipients,
1170 "array_replace(?,?,?)",
1172 ^old_follower_address,
1173 ^user.follower_address
1178 |> Repo.update_all([])
1181 def upgrade_user_from_ap_id(ap_id) do
1182 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1183 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1184 {:ok, user} <- update_user(user, data) do
1185 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1188 %User{} = user -> {:ok, user}
1193 defp update_user(user, data) do
1195 |> User.remote_user_changeset(data)
1196 |> User.update_and_set_cache()
1199 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1200 Map.put(data, "url", url["href"])
1203 def maybe_fix_user_url(data), do: data
1205 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)