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.EctoType.ActivityPub.ObjectValidators
12 alias Pleroma.FollowingRelationship
14 alias Pleroma.Notification
16 alias Pleroma.Object.Containment
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Builder
21 alias Pleroma.Web.ActivityPub.ObjectValidator
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("context", replied_object.data["context"] || object["conversation"])
176 |> Map.drop(["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.drop(["conversation"])
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"]
242 |> Maps.put_if_present("mediaType", media_type)
243 |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
245 %{"url" => [attachment_url]}
246 |> Maps.put_if_present("mediaType", media_type)
247 |> Maps.put_if_present("type", data["type"])
248 |> Maps.put_if_present("name", data["name"])
255 Map.put(object, "attachment", attachments)
258 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
260 |> Map.put("attachment", [attachment])
264 def fix_attachments(object), do: object
266 def fix_url(%{"url" => url} = object) when is_map(url) do
267 Map.put(object, "url", url["href"])
270 def fix_url(%{"type" => object_type, "url" => url} = object)
271 when object_type in ["Video", "Audio"] and is_list(url) do
273 Enum.find(url, fn x ->
274 media_type = x["mediaType"] || x["mimeType"] || ""
276 is_map(x) and String.starts_with?(media_type, ["audio/", "video/"])
280 Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
283 |> Map.put("attachment", [attachment])
284 |> Map.put("url", link_element["href"])
287 def fix_url(%{"type" => object_type, "url" => url} = object)
288 when object_type != "Video" and is_list(url) do
289 first_element = Enum.at(url, 0)
293 is_bitstring(first_element) -> first_element
294 is_map(first_element) -> first_element["href"] || ""
298 Map.put(object, "url", url_string)
301 def fix_url(object), do: object
303 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
306 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
307 |> Enum.reduce(%{}, fn data, mapping ->
308 name = String.trim(data["name"], ":")
310 Map.put(mapping, name, data["icon"]["url"])
313 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
314 emoji = Map.merge(object["emoji"] || %{}, emoji)
316 Map.put(object, "emoji", emoji)
319 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
320 name = String.trim(tag["name"], ":")
321 emoji = %{name => tag["icon"]["url"]}
323 Map.put(object, "emoji", emoji)
326 def fix_emoji(object), do: object
328 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
331 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
332 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
334 Map.put(object, "tag", tag ++ tags)
337 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
338 combined = [tag, String.slice(hashtag, 1..-1)]
340 Map.put(object, "tag", combined)
343 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
345 def fix_tag(object), do: object
347 # content map usually only has one language so this will do for now.
348 def fix_content_map(%{"contentMap" => content_map} = object) do
349 content_groups = Map.to_list(content_map)
350 {_, content} = Enum.at(content_groups, 0)
352 Map.put(object, "content", content)
355 def fix_content_map(object), do: object
357 def fix_type(object, options \\ [])
359 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
360 when is_binary(reply_id) do
361 with true <- Federator.allowed_thread_distance?(options[:depth]),
362 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
363 Map.put(object, "type", "Answer")
369 def fix_type(object, _), do: object
371 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
372 when is_binary(content) do
375 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
376 |> Pleroma.HTML.filter_tags()
378 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
381 defp fix_content(object), do: object
383 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
384 with true <- id =~ "follows",
385 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
386 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
393 defp mastodon_follow_hack(_, _), do: {:error, nil}
395 defp get_follow_activity(follow_object, followed) do
396 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
397 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
400 # Can't find the activity. This might a Mastodon 2.3 "Accept"
402 mastodon_follow_hack(follow_object, followed)
409 # Reduce the object list to find the reported user.
410 defp get_reported(objects) do
411 Enum.reduce_while(objects, nil, fn ap_id, _ ->
412 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
420 def handle_incoming(data, options \\ [])
422 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
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
438 additional: %{"cc" => [account.ap_id]}
440 |> ActivityPub.flag()
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,
451 # TODO: validate those with a Ecto scheme
455 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
458 when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
459 actor = Containment.get_actor(data)
461 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
462 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor),
463 data <- Map.put(data, "actor", actor) |> fix_addressing() do
464 object = fix_object(object, options)
470 context: object["context"],
472 published: data["published"],
481 with {:ok, created_activity} <- ActivityPub.create(params) do
482 reply_depth = (options[:depth] || 0) + 1
484 if Federator.allowed_thread_distance?(reply_depth) do
485 for reply_id <- replies(object) do
486 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
488 "depth" => reply_depth
493 {:ok, created_activity}
496 %Activity{} = activity -> {:ok, activity}
502 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
505 actor = Containment.get_actor(data)
508 Map.put(data, "actor", actor)
511 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
512 reply_depth = (options[:depth] || 0) + 1
513 options = Keyword.put(options, :depth, reply_depth)
514 object = fix_object(object, options)
522 published: data["published"],
523 additional: Map.take(data, ["cc", "id"])
526 ActivityPub.listen(params)
533 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
536 with %User{local: true} = followed <-
537 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
538 {:ok, %User{} = follower} <-
539 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
541 ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
542 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
543 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
544 {_, false} <- {:user_locked, User.locked?(followed)},
545 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
547 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
548 {:ok, _relationship} <-
549 FollowingRelationship.update(follower, followed, :follow_accept) do
550 ActivityPub.accept(%{
551 to: [follower.ap_id],
557 {:user_blocked, true} ->
558 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
559 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
561 ActivityPub.reject(%{
562 to: [follower.ap_id],
568 {:follow, {:error, _}} ->
569 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
570 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
572 ActivityPub.reject(%{
573 to: [follower.ap_id],
579 {:user_locked, true} ->
580 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
584 ActivityPub.notify_and_stream(activity)
593 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
596 with actor <- Containment.get_actor(data),
597 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
598 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
599 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
600 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
601 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
602 User.update_follower_count(followed)
603 User.update_following_count(follower)
605 Notification.update_notification_type(followed, follow_activity)
607 ActivityPub.accept(%{
608 to: follow_activity.data["to"],
611 object: follow_activity.data["id"],
622 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
625 with actor <- Containment.get_actor(data),
626 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
627 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
628 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
629 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
630 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
632 ActivityPub.reject(%{
633 to: follow_activity.data["to"],
636 object: follow_activity.data["id"],
646 @misskey_reactions %{
660 @doc "Rewrite misskey likes into EmojiReacts"
664 "_misskey_reaction" => reaction
669 |> Map.put("type", "EmojiReact")
670 |> Map.put("content", @misskey_reactions[reaction] || reaction)
671 |> handle_incoming(options)
675 %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
678 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
679 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
684 def handle_incoming(%{"type" => type} = data, _options)
685 when type in ~w{Like EmojiReact Announce} do
686 with :ok <- ObjectValidator.fetch_actor_and_object(data),
687 {:ok, activity, _meta} <-
688 Pipeline.common_pipeline(data, local: false) do
696 %{"type" => type} = data,
699 when type in ~w{Update Block} do
700 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
701 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
707 %{"type" => "Delete"} = data,
710 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
713 {:error, {:validate_object, _}} = e ->
714 # Check if we have a create activity for this
715 with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
716 %Activity{data: %{"actor" => actor}} <-
717 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
718 # We have one, insert a tombstone and retry
719 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
720 {:ok, _tombstone} <- Object.create(tombstone_data) do
721 handle_incoming(data)
731 "object" => %{"type" => "Follow", "object" => followed},
737 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
738 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
739 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
740 User.unfollow(follower, followed)
750 "object" => %{"type" => type}
754 when type in ["Like", "EmojiReact", "Announce", "Block"] do
755 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
760 # For Undos that don't have the complete object attached, try to find it in our database.
768 when is_binary(object) do
769 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
771 |> Map.put("object", data)
772 |> handle_incoming(options)
781 "actor" => origin_actor,
782 "object" => origin_actor,
783 "target" => target_actor
787 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
788 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
789 true <- origin_actor in target_user.also_known_as do
790 ActivityPub.move(origin_user, target_user, false)
796 def handle_incoming(_, _), do: :error
798 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
799 def get_obj_helper(id, options \\ []) do
800 case Object.normalize(id, true, options) do
801 %Object{} = object -> {:ok, object}
806 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
807 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
810 when attributed_to == ap_id do
811 with {:ok, activity} <-
816 "actor" => attributed_to,
819 {:ok, Object.normalize(activity)}
821 _ -> get_obj_helper(object_id)
825 def get_embedded_obj_helper(object_id, _) do
826 get_obj_helper(object_id)
829 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
830 with false <- String.starts_with?(in_reply_to, "http"),
831 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
832 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
838 def set_reply_to_uri(obj), do: obj
841 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
842 Based on Mastodon's ActivityPub::NoteSerializer#replies.
844 def set_replies(obj_data) do
846 with limit when limit > 0 <-
847 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
848 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
850 |> Object.self_replies()
851 |> select([o], fragment("?->>'id'", o.data))
858 set_replies(obj_data, replies_uris)
861 defp set_replies(obj, []) do
865 defp set_replies(obj, replies_uris) do
866 replies_collection = %{
867 "type" => "Collection",
868 "items" => replies_uris
871 Map.merge(obj, %{"replies" => replies_collection})
874 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
878 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
882 def replies(_), do: []
884 # Prepares the object of an outgoing create activity.
885 def prepare_object(object) do
892 |> prepare_attachments
896 |> strip_internal_fields
897 |> strip_internal_tags
903 # internal -> Mastodon
906 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
907 when activity_type in ["Create", "Listen"] do
910 |> Object.normalize()
916 |> Map.put("object", object)
917 |> Map.merge(Utils.make_json_ld_header())
923 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
926 |> Object.normalize()
929 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
930 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
932 data |> maybe_fix_object_url
937 |> strip_internal_fields
938 |> Map.merge(Utils.make_json_ld_header())
944 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
945 # because of course it does.
946 def prepare_outgoing(%{"type" => "Accept"} = data) do
947 with follow_activity <- Activity.normalize(data["object"]) do
949 "actor" => follow_activity.actor,
950 "object" => follow_activity.data["object"],
951 "id" => follow_activity.data["id"],
957 |> Map.put("object", object)
958 |> Map.merge(Utils.make_json_ld_header())
964 def prepare_outgoing(%{"type" => "Reject"} = data) do
965 with follow_activity <- Activity.normalize(data["object"]) do
967 "actor" => follow_activity.actor,
968 "object" => follow_activity.data["object"],
969 "id" => follow_activity.data["id"],
975 |> Map.put("object", object)
976 |> Map.merge(Utils.make_json_ld_header())
982 def prepare_outgoing(%{"type" => _type} = data) do
985 |> strip_internal_fields
986 |> maybe_fix_object_url
987 |> Map.merge(Utils.make_json_ld_header())
992 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
993 with false <- String.starts_with?(object, "http"),
994 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
995 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
997 Map.put(data, "object", external_url)
1000 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1008 def maybe_fix_object_url(data), do: data
1010 def add_hashtags(object) do
1012 (object["tag"] || [])
1014 # Expand internal representation tags into AS2 tags.
1015 tag when is_binary(tag) ->
1017 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1018 "name" => "##{tag}",
1022 # Do not process tags which are already AS2 tag objects.
1023 tag when is_map(tag) ->
1027 Map.put(object, "tag", tags)
1030 # TODO These should be added on our side on insertion, it doesn't make much
1031 # sense to regenerate these all the time
1032 def add_mention_tags(object) do
1033 to = object["to"] || []
1034 cc = object["cc"] || []
1035 mentioned = User.get_users_from_set(to ++ cc, local_only: false)
1037 mentions = Enum.map(mentioned, &build_mention_tag/1)
1039 tags = object["tag"] || []
1040 Map.put(object, "tag", tags ++ mentions)
1043 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1044 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1047 def take_emoji_tags(%User{emoji: emoji}) do
1050 |> Enum.map(&build_emoji_tag/1)
1053 # TODO: we should probably send mtime instead of unix epoch time for updated
1054 def add_emoji_tags(%{"emoji" => emoji} = object) do
1055 tags = object["tag"] || []
1057 out = Enum.map(emoji, &build_emoji_tag/1)
1059 Map.put(object, "tag", tags ++ out)
1062 def add_emoji_tags(object), do: object
1064 defp build_emoji_tag({name, url}) do
1066 "icon" => %{"url" => url, "type" => "Image"},
1067 "name" => ":" <> name <> ":",
1069 "updated" => "1970-01-01T00:00:00Z",
1074 def set_conversation(object) do
1075 Map.put(object, "conversation", object["context"])
1078 def set_sensitive(%{"sensitive" => true} = object) do
1082 def set_sensitive(object) do
1083 tags = object["tag"] || []
1084 Map.put(object, "sensitive", "nsfw" in tags)
1087 def set_type(%{"type" => "Answer"} = object) do
1088 Map.put(object, "type", "Note")
1091 def set_type(object), do: object
1093 def add_attributed_to(object) do
1094 attributed_to = object["attributedTo"] || object["actor"]
1095 Map.put(object, "attributedTo", attributed_to)
1098 # TODO: Revisit this
1099 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
1101 def prepare_attachments(object) do
1104 |> Map.get("attachment", [])
1105 |> Enum.map(fn data ->
1106 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1110 "mediaType" => media_type,
1111 "name" => data["name"],
1112 "type" => "Document"
1116 Map.put(object, "attachment", attachments)
1119 def strip_internal_fields(object) do
1120 Map.drop(object, Pleroma.Constants.object_internal_fields())
1123 defp strip_internal_tags(%{"tag" => tags} = object) do
1124 tags = Enum.filter(tags, fn x -> is_map(x) end)
1126 Map.put(object, "tag", tags)
1129 defp strip_internal_tags(object), do: object
1131 def perform(:user_upgrade, user) do
1132 # we pass a fake user so that the followers collection is stripped away
1133 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1137 where: ^old_follower_address in a.recipients,
1142 "array_replace(?,?,?)",
1144 ^old_follower_address,
1145 ^user.follower_address
1150 |> Repo.update_all([])
1153 def upgrade_user_from_ap_id(ap_id) do
1154 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1155 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1156 {:ok, user} <- update_user(user, data) do
1157 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1160 %User{} = user -> {:ok, user}
1165 defp update_user(user, data) do
1167 |> User.remote_user_changeset(data)
1168 |> User.update_and_set_cache()
1171 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1172 Map.put(data, "url", url["href"])
1175 def maybe_fix_user_url(data), do: data
1177 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)