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 def handle_incoming(%{"type" => "Like"} = data, _options) do
573 with {_, {:ok, cast_data_sym}} <-
575 data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
577 {:stringify_keys, ObjectValidator.stringify_keys(cast_data_sym |> Map.from_struct())},
578 :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
579 {_, {:ok, cast_data}} <- {:maybe_add_context, maybe_add_context_from_object(cast_data)},
580 {_, {:ok, cast_data}} <-
581 {:maybe_add_recipients, maybe_add_recipients_from_object(cast_data)},
582 {_, {:ok, activity, _meta}} <-
583 {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
591 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
594 with actor <- Containment.get_actor(data),
595 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
596 {:ok, object} <- get_embedded_obj_helper(object_id, actor),
597 public <- Visibility.is_public?(data),
598 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
606 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
610 when object_type in [
616 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
617 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
619 locked = new_user_data[:locked] || false
620 attachment = get_in(new_user_data, [:source_data, "attachment"]) || []
621 invisible = new_user_data[:invisible] || false
625 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
626 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
630 |> Map.take([:avatar, :banner, :bio, :name])
631 |> Map.put(:fields, fields)
632 |> Map.put(:locked, locked)
633 |> Map.put(:invisible, invisible)
636 |> User.upgrade_changeset(update_data, true)
637 |> User.update_and_set_cache()
639 ActivityPub.update(%{
641 to: data["to"] || [],
642 cc: data["cc"] || [],
645 activity_id: data["id"]
654 # TODO: We presently assume that any actor on the same origin domain as the object being
655 # deleted has the rights to delete that object. A better way to validate whether or not
656 # the object should be deleted is to refetch the object URI, which should return either
657 # an error or a tombstone. This would allow us to verify that a deletion actually took
660 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
663 object_id = Utils.get_ap_id(object_id)
665 with actor <- Containment.get_actor(data),
666 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
667 {:ok, object} <- get_obj_helper(object_id),
668 :ok <- Containment.contain_origin(actor.ap_id, object.data),
670 ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
674 case User.get_cached_by_ap_id(object_id) do
675 %User{ap_id: ^actor} = user ->
690 "object" => %{"type" => "Announce", "object" => object_id},
696 with actor <- Containment.get_actor(data),
697 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
698 {:ok, object} <- get_obj_helper(object_id),
699 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
709 "object" => %{"type" => "Follow", "object" => followed},
715 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
716 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
717 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
718 User.unfollow(follower, followed)
728 "object" => %{"type" => "Block", "object" => blocked},
734 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
735 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
736 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
737 User.unblock(blocker, blocked)
745 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
748 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
749 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
750 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
751 User.unfollow(blocker, blocked)
752 User.block(blocker, blocked)
762 "object" => %{"type" => "Like", "object" => object_id},
768 with actor <- Containment.get_actor(data),
769 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
770 {:ok, object} <- get_obj_helper(object_id),
771 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
778 # For Undos that don't have the complete object attached, try to find it in our database.
786 when is_binary(object) do
787 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
789 |> Map.put("object", data)
790 |> handle_incoming(options)
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
840 # Prepares the object of an outgoing create activity.
841 def prepare_object(object) do
848 |> prepare_attachments
851 |> strip_internal_fields
852 |> strip_internal_tags
858 # internal -> Mastodon
861 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
862 when activity_type in ["Create", "Listen"] do
865 |> Object.normalize()
871 |> Map.put("object", object)
872 |> Map.merge(Utils.make_json_ld_header())
878 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
881 |> Object.normalize()
884 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
885 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
887 data |> maybe_fix_object_url
892 |> strip_internal_fields
893 |> Map.merge(Utils.make_json_ld_header())
899 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
900 # because of course it does.
901 def prepare_outgoing(%{"type" => "Accept"} = data) do
902 with follow_activity <- Activity.normalize(data["object"]) do
904 "actor" => follow_activity.actor,
905 "object" => follow_activity.data["object"],
906 "id" => follow_activity.data["id"],
912 |> Map.put("object", object)
913 |> Map.merge(Utils.make_json_ld_header())
919 def prepare_outgoing(%{"type" => "Reject"} = data) do
920 with follow_activity <- Activity.normalize(data["object"]) do
922 "actor" => follow_activity.actor,
923 "object" => follow_activity.data["object"],
924 "id" => follow_activity.data["id"],
930 |> Map.put("object", object)
931 |> Map.merge(Utils.make_json_ld_header())
937 def prepare_outgoing(%{"type" => _type} = data) do
940 |> strip_internal_fields
941 |> maybe_fix_object_url
942 |> Map.merge(Utils.make_json_ld_header())
947 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
948 with false <- String.starts_with?(object, "http"),
949 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
950 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
952 Map.put(data, "object", external_url)
955 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
963 def maybe_fix_object_url(data), do: data
965 def add_hashtags(object) do
967 (object["tag"] || [])
969 # Expand internal representation tags into AS2 tags.
970 tag when is_binary(tag) ->
972 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
977 # Do not process tags which are already AS2 tag objects.
978 tag when is_map(tag) ->
982 Map.put(object, "tag", tags)
985 def add_mention_tags(object) do
988 |> Utils.get_notified_from_object()
989 |> Enum.map(&build_mention_tag/1)
991 tags = object["tag"] || []
993 Map.put(object, "tag", tags ++ mentions)
996 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
997 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1000 def take_emoji_tags(%User{emoji: emoji}) do
1002 |> Enum.flat_map(&Map.to_list/1)
1003 |> Enum.map(&build_emoji_tag/1)
1006 # TODO: we should probably send mtime instead of unix epoch time for updated
1007 def add_emoji_tags(%{"emoji" => emoji} = object) do
1008 tags = object["tag"] || []
1010 out = Enum.map(emoji, &build_emoji_tag/1)
1012 Map.put(object, "tag", tags ++ out)
1015 def add_emoji_tags(object), do: object
1017 defp build_emoji_tag({name, url}) do
1019 "icon" => %{"url" => url, "type" => "Image"},
1020 "name" => ":" <> name <> ":",
1022 "updated" => "1970-01-01T00:00:00Z",
1027 def set_conversation(object) do
1028 Map.put(object, "conversation", object["context"])
1031 def set_sensitive(object) do
1032 tags = object["tag"] || []
1033 Map.put(object, "sensitive", "nsfw" in tags)
1036 def set_type(%{"type" => "Answer"} = object) do
1037 Map.put(object, "type", "Note")
1040 def set_type(object), do: object
1042 def add_attributed_to(object) do
1043 attributed_to = object["attributedTo"] || object["actor"]
1044 Map.put(object, "attributedTo", attributed_to)
1047 def prepare_attachments(object) do
1049 (object["attachment"] || [])
1050 |> Enum.map(fn data ->
1051 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1052 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
1055 Map.put(object, "attachment", attachments)
1058 defp strip_internal_fields(object) do
1060 |> Map.drop(Pleroma.Constants.object_internal_fields())
1063 defp strip_internal_tags(%{"tag" => tags} = object) do
1064 tags = Enum.filter(tags, fn x -> is_map(x) end)
1066 Map.put(object, "tag", tags)
1069 defp strip_internal_tags(object), do: object
1071 def perform(:user_upgrade, user) do
1072 # we pass a fake user so that the followers collection is stripped away
1073 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1077 where: ^old_follower_address in a.recipients,
1082 "array_replace(?,?,?)",
1084 ^old_follower_address,
1085 ^user.follower_address
1090 |> Repo.update_all([])
1093 def upgrade_user_from_ap_id(ap_id) do
1094 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1095 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1096 already_ap <- User.ap_enabled?(user),
1097 {:ok, user} <- upgrade_user(user, data) do
1098 if not already_ap do
1099 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1104 %User{} = user -> {:ok, user}
1109 defp upgrade_user(user, data) do
1111 |> User.upgrade_changeset(data, true)
1112 |> User.update_and_set_cache()
1115 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1116 Map.put(data, "url", url["href"])
1119 def maybe_fix_user_url(data), do: data
1121 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1123 defp maybe_add_context_from_object(%{"context" => context} = data) when is_binary(context),
1126 defp maybe_add_context_from_object(%{"object" => object} = data) when is_binary(object) do
1127 if object = Object.normalize(object) do
1130 |> Map.put("context", object.data["context"])
1134 {:error, "No context on referenced object"}
1138 defp maybe_add_context_from_object(_) do
1139 {:error, "No referenced object"}
1142 defp maybe_add_recipients_from_object(%{"object" => object} = data) do
1143 to = data["to"] || []
1144 cc = data["cc"] || []
1146 if to == [] && cc == [] do
1147 if object = Object.normalize(object) do
1150 |> Map.put("to", [object.data["actor"]])
1151 |> Map.put("cc", cc)
1155 {:error, "No actor on referenced object"}
1162 defp maybe_add_recipients_from_object(_) do
1163 {:error, "No referenced object"}