1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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.FollowingRelationship
12 alias Pleroma.Object.Containment
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.ObjectValidator
17 alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
18 alias Pleroma.Web.ActivityPub.Pipeline
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.ActivityPub.Visibility
21 alias Pleroma.Web.Federator
22 alias Pleroma.Workers.TransmogrifierWorker
27 require Pleroma.Constants
30 Modifies an incoming AP object (mastodon format) to our internal format.
32 def fix_object(object, options \\ []) do
34 |> strip_internal_fields
39 |> fix_in_reply_to(options)
48 def fix_summary(%{"summary" => nil} = object) do
49 Map.put(object, "summary", "")
52 def fix_summary(%{"summary" => _} = object) do
53 # summary is present, nothing to do
57 def fix_summary(object), do: Map.put(object, "summary", "")
59 def fix_addressing_list(map, field) do
61 is_binary(map[field]) ->
62 Map.put(map, field, [map[field]])
65 Map.put(map, field, [])
72 def fix_explicit_addressing(
73 %{"to" => to, "cc" => cc} = object,
77 explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
79 explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
83 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
87 |> Map.put("to", explicit_to)
88 |> Map.put("cc", final_cc)
91 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
93 # if directMessage flag is set to true, leave the addressing alone
94 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
96 def fix_explicit_addressing(object) do
97 explicit_mentions = Utils.determine_explicit_mentions(object)
99 %User{follower_address: follower_collection} =
101 |> Containment.get_actor()
102 |> User.get_cached_by_ap_id()
107 Pleroma.Constants.as_public(),
111 fix_explicit_addressing(object, explicit_mentions, follower_collection)
114 # if as:Public is addressed, then make sure the followers collection is also addressed
115 # so that the activities will be delivered to local users.
116 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
117 recipients = to ++ cc
119 if followers_collection not in recipients do
121 Pleroma.Constants.as_public() in cc ->
122 to = to ++ [followers_collection]
123 Map.put(object, "to", to)
125 Pleroma.Constants.as_public() in to ->
126 cc = cc ++ [followers_collection]
127 Map.put(object, "cc", cc)
137 def fix_implicit_addressing(object, _), do: object
139 def fix_addressing(object) do
140 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
141 followers_collection = User.ap_followers(user)
144 |> fix_addressing_list("to")
145 |> fix_addressing_list("cc")
146 |> fix_addressing_list("bto")
147 |> fix_addressing_list("bcc")
148 |> fix_explicit_addressing()
149 |> fix_implicit_addressing(followers_collection)
152 def fix_actor(%{"attributedTo" => actor} = object) do
153 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
156 def fix_in_reply_to(object, options \\ [])
158 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
159 when not is_nil(in_reply_to) do
160 in_reply_to_id = prepare_in_reply_to(in_reply_to)
161 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
163 if Federator.allowed_incoming_reply_depth?(options[:depth]) do
164 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
165 %Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
167 |> Map.put("inReplyTo", replied_object.data["id"])
168 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
169 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
170 |> Map.put("context", replied_object.data["context"] || object["conversation"])
173 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
181 def fix_in_reply_to(object, _options), do: object
183 defp prepare_in_reply_to(in_reply_to) do
185 is_bitstring(in_reply_to) ->
188 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
191 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
192 Enum.at(in_reply_to, 0)
199 def fix_context(object) do
200 context = object["context"] || object["conversation"] || Utils.generate_context_id()
203 |> Map.put("context", context)
204 |> Map.put("conversation", context)
207 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
209 Enum.map(attachment, fn data ->
210 media_type = data["mediaType"] || data["mimeType"]
211 href = data["url"] || data["href"]
212 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
215 |> Map.put("mediaType", media_type)
216 |> Map.put("url", url)
219 Map.put(object, "attachment", attachments)
222 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
224 |> Map.put("attachment", [attachment])
228 def fix_attachments(object), do: object
230 def fix_url(%{"url" => url} = object) when is_map(url) do
231 Map.put(object, "url", url["href"])
234 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
235 first_element = Enum.at(url, 0)
237 link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
240 |> Map.put("attachment", [first_element])
241 |> Map.put("url", link_element["href"])
244 def fix_url(%{"type" => object_type, "url" => url} = object)
245 when object_type != "Video" and is_list(url) do
246 first_element = Enum.at(url, 0)
250 is_bitstring(first_element) -> first_element
251 is_map(first_element) -> first_element["href"] || ""
255 Map.put(object, "url", url_string)
258 def fix_url(object), do: object
260 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
263 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
264 |> Enum.reduce(%{}, fn data, mapping ->
265 name = String.trim(data["name"], ":")
267 Map.put(mapping, name, data["icon"]["url"])
270 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
271 emoji = Map.merge(object["emoji"] || %{}, emoji)
273 Map.put(object, "emoji", emoji)
276 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
277 name = String.trim(tag["name"], ":")
278 emoji = %{name => tag["icon"]["url"]}
280 Map.put(object, "emoji", emoji)
283 def fix_emoji(object), do: object
285 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
288 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
289 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
291 Map.put(object, "tag", tag ++ tags)
294 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
295 combined = [tag, String.slice(hashtag, 1..-1)]
297 Map.put(object, "tag", combined)
300 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
302 def fix_tag(object), do: object
304 # content map usually only has one language so this will do for now.
305 def fix_content_map(%{"contentMap" => content_map} = object) do
306 content_groups = Map.to_list(content_map)
307 {_, content} = Enum.at(content_groups, 0)
309 Map.put(object, "content", content)
312 def fix_content_map(object), do: object
314 def fix_type(object, options \\ [])
316 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
317 when is_binary(reply_id) do
318 with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
319 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
320 Map.put(object, "type", "Answer")
326 def fix_type(object, _), do: object
328 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
329 with true <- id =~ "follows",
330 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
331 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
338 defp mastodon_follow_hack(_, _), do: {:error, nil}
340 defp get_follow_activity(follow_object, followed) do
341 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
342 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
345 # Can't find the activity. This might a Mastodon 2.3 "Accept"
347 mastodon_follow_hack(follow_object, followed)
354 # Reduce the object list to find the reported user.
355 defp get_reported(objects) do
356 Enum.reduce_while(objects, nil, fn ap_id, _ ->
357 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
365 def handle_incoming(data, options \\ [])
367 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
369 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
370 with context <- data["context"] || Utils.generate_context_id(),
371 content <- data["content"] || "",
372 %User{} = actor <- User.get_cached_by_ap_id(actor),
373 # Reduce the object list to find the reported user.
374 %User{} = account <- get_reported(objects),
375 # Remove the reported user from the object list.
376 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
383 additional: %{"cc" => [account.ap_id]}
385 |> ActivityPub.flag()
389 # disallow objects with bogus IDs
390 def handle_incoming(%{"id" => nil}, _options), do: :error
391 def handle_incoming(%{"id" => ""}, _options), do: :error
392 # length of https:// = 8, should validate better, but good enough for now.
393 def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
396 # TODO: validate those with a Ecto scheme
400 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
403 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
404 actor = Containment.get_actor(data)
407 Map.put(data, "actor", actor)
410 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
411 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
412 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
413 object = fix_object(data["object"], options)
419 context: object["conversation"],
421 published: data["published"],
430 ActivityPub.create(params)
432 %Activity{} = activity -> {:ok, activity}
438 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
441 actor = Containment.get_actor(data)
444 Map.put(data, "actor", actor)
447 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
448 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
449 object = fix_object(object, options)
457 published: data["published"],
458 additional: Map.take(data, ["cc", "id"])
461 ActivityPub.listen(params)
468 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
471 with %User{local: true} = followed <-
472 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
473 {:ok, %User{} = follower} <-
474 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
475 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
476 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
477 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
478 {_, false} <- {:user_locked, User.locked?(followed)},
479 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
481 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
482 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
483 ActivityPub.accept(%{
484 to: [follower.ap_id],
490 {:user_blocked, true} ->
491 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
492 {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
494 ActivityPub.reject(%{
495 to: [follower.ap_id],
501 {:follow, {:error, _}} ->
502 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
503 {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
505 ActivityPub.reject(%{
506 to: [follower.ap_id],
512 {:user_locked, true} ->
513 {:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending")
525 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
528 with actor <- Containment.get_actor(data),
529 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
530 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
531 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
532 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
533 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
534 ActivityPub.accept(%{
535 to: follow_activity.data["to"],
538 object: follow_activity.data["id"],
548 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
551 with actor <- Containment.get_actor(data),
552 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
553 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
554 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
555 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
556 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
558 ActivityPub.reject(%{
559 to: follow_activity.data["to"],
562 object: follow_activity.data["id"],
572 @misskey_reactions %{
586 @doc "Rewrite misskey likes into EmojiReactions"
590 "_misskey_reaction" => reaction
595 |> Map.put("type", "EmojiReaction")
596 |> Map.put("content", @misskey_reactions[reaction] || reaction)
597 |> handle_incoming(options)
600 def handle_incoming(%{"type" => "Like"} = data, _options) do
601 with {_, {:ok, cast_data_sym}} <-
603 data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
605 {:stringify_keys, ObjectValidator.stringify_keys(cast_data_sym |> Map.from_struct())},
606 :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
607 {_, {:ok, cast_data}} <- {:maybe_add_context, maybe_add_context_from_object(cast_data)},
608 {_, {:ok, cast_data}} <-
609 {:maybe_add_recipients, maybe_add_recipients_from_object(cast_data)},
610 {_, {:ok, activity, _meta}} <-
611 {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
620 "type" => "EmojiReaction",
621 "object" => object_id,
628 with actor <- Containment.get_actor(data),
629 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
630 {:ok, object} <- get_obj_helper(object_id),
631 {:ok, activity, _object} <-
632 ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
640 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
643 with actor <- Containment.get_actor(data),
644 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
645 {:ok, object} <- get_embedded_obj_helper(object_id, actor),
646 public <- Visibility.is_public?(data),
647 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
655 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
659 when object_type in [
665 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
666 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
668 locked = new_user_data[:locked] || false
669 attachment = get_in(new_user_data, [:source_data, "attachment"]) || []
670 invisible = new_user_data[:invisible] || false
674 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
675 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
679 |> Map.take([:avatar, :banner, :bio, :name, :also_known_as])
680 |> Map.put(:fields, fields)
681 |> Map.put(:locked, locked)
682 |> Map.put(:invisible, invisible)
685 |> User.upgrade_changeset(update_data, true)
686 |> User.update_and_set_cache()
688 ActivityPub.update(%{
690 to: data["to"] || [],
691 cc: data["cc"] || [],
694 activity_id: data["id"]
703 # TODO: We presently assume that any actor on the same origin domain as the object being
704 # deleted has the rights to delete that object. A better way to validate whether or not
705 # the object should be deleted is to refetch the object URI, which should return either
706 # an error or a tombstone. This would allow us to verify that a deletion actually took
709 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
712 object_id = Utils.get_ap_id(object_id)
714 with actor <- Containment.get_actor(data),
715 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
716 {:ok, object} <- get_obj_helper(object_id),
717 :ok <- Containment.contain_origin(actor.ap_id, object.data),
719 ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
723 case User.get_cached_by_ap_id(object_id) do
724 %User{ap_id: ^actor} = user ->
739 "object" => %{"type" => "Announce", "object" => object_id},
745 with actor <- Containment.get_actor(data),
746 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
747 {:ok, object} <- get_obj_helper(object_id),
748 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
758 "object" => %{"type" => "Follow", "object" => followed},
764 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
765 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
766 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
767 User.unfollow(follower, followed)
777 "object" => %{"type" => "EmojiReaction", "id" => reaction_activity_id},
783 with actor <- Containment.get_actor(data),
784 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
785 {:ok, activity, _} <-
786 ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
799 "object" => %{"type" => "Block", "object" => blocked},
805 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
806 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
807 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
808 User.unblock(blocker, blocked)
816 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
819 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
820 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
821 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
822 User.unfollow(blocker, blocked)
823 User.block(blocker, blocked)
833 "object" => %{"type" => "Like", "object" => object_id},
839 with actor <- Containment.get_actor(data),
840 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
841 {:ok, object} <- get_obj_helper(object_id),
842 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
849 # For Undos that don't have the complete object attached, try to find it in our database.
857 when is_binary(object) do
858 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
860 |> Map.put("object", data)
861 |> handle_incoming(options)
870 "actor" => origin_actor,
871 "object" => origin_actor,
872 "target" => target_actor
876 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
877 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
878 true <- origin_actor in target_user.also_known_as do
879 ActivityPub.move(origin_user, target_user, false)
885 def handle_incoming(_, _), do: :error
887 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
888 def get_obj_helper(id, options \\ []) do
889 case Object.normalize(id, true, options) do
890 %Object{} = object -> {:ok, object}
895 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
896 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
899 when attributed_to == ap_id do
900 with {:ok, activity} <-
905 "actor" => attributed_to,
908 {:ok, Object.normalize(activity)}
910 _ -> get_obj_helper(object_id)
914 def get_embedded_obj_helper(object_id, _) do
915 get_obj_helper(object_id)
918 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
919 with false <- String.starts_with?(in_reply_to, "http"),
920 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
921 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
927 def set_reply_to_uri(obj), do: obj
929 # Prepares the object of an outgoing create activity.
930 def prepare_object(object) do
937 |> prepare_attachments
940 |> strip_internal_fields
941 |> strip_internal_tags
947 # internal -> Mastodon
950 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
951 when activity_type in ["Create", "Listen"] do
954 |> Object.normalize()
960 |> Map.put("object", object)
961 |> Map.merge(Utils.make_json_ld_header())
967 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
970 |> Object.normalize()
973 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
974 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
976 data |> maybe_fix_object_url
981 |> strip_internal_fields
982 |> Map.merge(Utils.make_json_ld_header())
988 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
989 # because of course it does.
990 def prepare_outgoing(%{"type" => "Accept"} = data) do
991 with follow_activity <- Activity.normalize(data["object"]) do
993 "actor" => follow_activity.actor,
994 "object" => follow_activity.data["object"],
995 "id" => follow_activity.data["id"],
1001 |> Map.put("object", object)
1002 |> Map.merge(Utils.make_json_ld_header())
1008 def prepare_outgoing(%{"type" => "Reject"} = data) do
1009 with follow_activity <- Activity.normalize(data["object"]) do
1011 "actor" => follow_activity.actor,
1012 "object" => follow_activity.data["object"],
1013 "id" => follow_activity.data["id"],
1019 |> Map.put("object", object)
1020 |> Map.merge(Utils.make_json_ld_header())
1026 def prepare_outgoing(%{"type" => _type} = data) do
1029 |> strip_internal_fields
1030 |> maybe_fix_object_url
1031 |> Map.merge(Utils.make_json_ld_header())
1036 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
1037 with false <- String.starts_with?(object, "http"),
1038 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
1039 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
1041 Map.put(data, "object", external_url)
1044 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1052 def maybe_fix_object_url(data), do: data
1054 def add_hashtags(object) do
1056 (object["tag"] || [])
1058 # Expand internal representation tags into AS2 tags.
1059 tag when is_binary(tag) ->
1061 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1062 "name" => "##{tag}",
1066 # Do not process tags which are already AS2 tag objects.
1067 tag when is_map(tag) ->
1071 Map.put(object, "tag", tags)
1074 def add_mention_tags(object) do
1077 |> Utils.get_notified_from_object()
1078 |> Enum.map(&build_mention_tag/1)
1080 tags = object["tag"] || []
1082 Map.put(object, "tag", tags ++ mentions)
1085 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1086 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1089 def take_emoji_tags(%User{emoji: emoji}) do
1091 |> Enum.flat_map(&Map.to_list/1)
1092 |> Enum.map(&build_emoji_tag/1)
1095 # TODO: we should probably send mtime instead of unix epoch time for updated
1096 def add_emoji_tags(%{"emoji" => emoji} = object) do
1097 tags = object["tag"] || []
1099 out = Enum.map(emoji, &build_emoji_tag/1)
1101 Map.put(object, "tag", tags ++ out)
1104 def add_emoji_tags(object), do: object
1106 defp build_emoji_tag({name, url}) do
1108 "icon" => %{"url" => url, "type" => "Image"},
1109 "name" => ":" <> name <> ":",
1111 "updated" => "1970-01-01T00:00:00Z",
1116 def set_conversation(object) do
1117 Map.put(object, "conversation", object["context"])
1120 def set_sensitive(object) do
1121 tags = object["tag"] || []
1122 Map.put(object, "sensitive", "nsfw" in tags)
1125 def set_type(%{"type" => "Answer"} = object) do
1126 Map.put(object, "type", "Note")
1129 def set_type(object), do: object
1131 def add_attributed_to(object) do
1132 attributed_to = object["attributedTo"] || object["actor"]
1133 Map.put(object, "attributedTo", attributed_to)
1136 def prepare_attachments(object) do
1138 (object["attachment"] || [])
1139 |> Enum.map(fn data ->
1140 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1141 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
1144 Map.put(object, "attachment", attachments)
1147 def strip_internal_fields(object) do
1149 |> Map.drop(Pleroma.Constants.object_internal_fields())
1152 defp strip_internal_tags(%{"tag" => tags} = object) do
1153 tags = Enum.filter(tags, fn x -> is_map(x) end)
1155 Map.put(object, "tag", tags)
1158 defp strip_internal_tags(object), do: object
1160 def perform(:user_upgrade, user) do
1161 # we pass a fake user so that the followers collection is stripped away
1162 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1166 where: ^old_follower_address in a.recipients,
1171 "array_replace(?,?,?)",
1173 ^old_follower_address,
1174 ^user.follower_address
1179 |> Repo.update_all([])
1182 def upgrade_user_from_ap_id(ap_id) do
1183 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1184 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1185 already_ap <- User.ap_enabled?(user),
1186 {:ok, user} <- upgrade_user(user, data) do
1187 if not already_ap do
1188 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1193 %User{} = user -> {:ok, user}
1198 defp upgrade_user(user, data) do
1200 |> User.upgrade_changeset(data, true)
1201 |> User.update_and_set_cache()
1204 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1205 Map.put(data, "url", url["href"])
1208 def maybe_fix_user_url(data), do: data
1210 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1212 defp maybe_add_context_from_object(%{"context" => context} = data) when is_binary(context),
1215 defp maybe_add_context_from_object(%{"object" => object} = data) when is_binary(object) do
1216 if object = Object.normalize(object) do
1219 |> Map.put("context", object.data["context"])
1223 {:error, "No context on referenced object"}
1227 defp maybe_add_context_from_object(_) do
1228 {:error, "No referenced object"}
1231 defp maybe_add_recipients_from_object(%{"object" => object} = data) do
1232 to = data["to"] || []
1233 cc = data["cc"] || []
1235 if to == [] && cc == [] do
1236 if object = Object.normalize(object) do
1239 |> Map.put("to", [object.data["actor"]])
1240 |> Map.put("cc", cc)
1244 {:error, "No actor on referenced object"}
1251 defp maybe_add_recipients_from_object(_) do
1252 {:error, "No referenced object"}