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.Object.Containment
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.ObjectValidator
18 alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
19 alias Pleroma.Web.ActivityPub.Pipeline
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.Federator
23 alias Pleroma.Workers.TransmogrifierWorker
28 require Pleroma.Constants
31 Modifies an incoming AP object (mastodon format) to our internal format.
33 def fix_object(object, options \\ []) do
35 |> strip_internal_fields
40 |> fix_in_reply_to(options)
50 def fix_summary(%{"summary" => nil} = object) do
51 Map.put(object, "summary", "")
54 def fix_summary(%{"summary" => _} = object) do
55 # summary is present, nothing to do
59 def fix_summary(object), do: Map.put(object, "summary", "")
61 def fix_addressing_list(map, field) do
63 is_binary(map[field]) ->
64 Map.put(map, field, [map[field]])
67 Map.put(map, field, [])
74 def fix_explicit_addressing(
75 %{"to" => to, "cc" => cc} = object,
79 explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
81 explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
85 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
89 |> Map.put("to", explicit_to)
90 |> Map.put("cc", final_cc)
93 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
95 # if directMessage flag is set to true, leave the addressing alone
96 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
98 def fix_explicit_addressing(object) do
99 explicit_mentions = Utils.determine_explicit_mentions(object)
101 %User{follower_address: follower_collection} =
103 |> Containment.get_actor()
104 |> User.get_cached_by_ap_id()
109 Pleroma.Constants.as_public(),
113 fix_explicit_addressing(object, explicit_mentions, follower_collection)
116 # if as:Public is addressed, then make sure the followers collection is also addressed
117 # so that the activities will be delivered to local users.
118 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
119 recipients = to ++ cc
121 if followers_collection not in recipients do
123 Pleroma.Constants.as_public() in cc ->
124 to = to ++ [followers_collection]
125 Map.put(object, "to", to)
127 Pleroma.Constants.as_public() in to ->
128 cc = cc ++ [followers_collection]
129 Map.put(object, "cc", cc)
139 def fix_implicit_addressing(object, _), do: object
141 def fix_addressing(object) do
142 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
143 followers_collection = User.ap_followers(user)
146 |> fix_addressing_list("to")
147 |> fix_addressing_list("cc")
148 |> fix_addressing_list("bto")
149 |> fix_addressing_list("bcc")
150 |> fix_explicit_addressing()
151 |> fix_implicit_addressing(followers_collection)
154 def fix_actor(%{"attributedTo" => actor} = object) do
155 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
158 def fix_in_reply_to(object, options \\ [])
160 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
161 when not is_nil(in_reply_to) do
162 in_reply_to_id = prepare_in_reply_to(in_reply_to)
163 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
164 depth = (options[:depth] || 0) + 1
166 if Federator.allowed_thread_distance?(depth) do
167 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
168 %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
170 |> Map.put("inReplyTo", replied_object.data["id"])
171 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
172 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
173 |> Map.put("context", replied_object.data["context"] || object["conversation"])
176 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
184 def fix_in_reply_to(object, _options), do: object
186 defp prepare_in_reply_to(in_reply_to) do
188 is_bitstring(in_reply_to) ->
191 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
194 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
195 Enum.at(in_reply_to, 0)
202 def fix_context(object) do
203 context = object["context"] || object["conversation"] || Utils.generate_context_id()
206 |> Map.put("context", context)
207 |> Map.put("conversation", context)
210 defp add_if_present(map, _key, nil), do: map
212 defp add_if_present(map, key, value) do
213 Map.put(map, key, value)
216 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
218 Enum.map(attachment, fn data ->
221 is_list(data["url"]) -> List.first(data["url"])
222 is_map(data["url"]) -> data["url"]
228 is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
229 is_binary(data["mediaType"]) -> data["mediaType"]
230 is_binary(data["mimeType"]) -> data["mimeType"]
236 is_map(url) && is_binary(url["href"]) -> url["href"]
237 is_binary(data["url"]) -> data["url"]
238 is_binary(data["href"]) -> data["href"]
243 |> add_if_present("mediaType", media_type)
244 |> add_if_present("type", Map.get(url || %{}, "type"))
246 %{"url" => [attachment_url]}
247 |> add_if_present("mediaType", media_type)
248 |> add_if_present("type", data["type"])
249 |> add_if_present("name", data["name"])
252 Map.put(object, "attachment", attachments)
255 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
257 |> Map.put("attachment", [attachment])
261 def fix_attachments(object), do: object
263 def fix_url(%{"url" => url} = object) when is_map(url) do
264 Map.put(object, "url", url["href"])
267 def fix_url(%{"type" => object_type, "url" => url} = object)
268 when object_type in ["Video", "Audio"] and is_list(url) do
269 first_element = Enum.at(url, 0)
271 link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
274 |> Map.put("attachment", [first_element])
275 |> Map.put("url", link_element["href"])
278 def fix_url(%{"type" => object_type, "url" => url} = object)
279 when object_type != "Video" and is_list(url) do
280 first_element = Enum.at(url, 0)
284 is_bitstring(first_element) -> first_element
285 is_map(first_element) -> first_element["href"] || ""
289 Map.put(object, "url", url_string)
292 def fix_url(object), do: object
294 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
297 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
298 |> Enum.reduce(%{}, fn data, mapping ->
299 name = String.trim(data["name"], ":")
301 Map.put(mapping, name, data["icon"]["url"])
304 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
305 emoji = Map.merge(object["emoji"] || %{}, emoji)
307 Map.put(object, "emoji", emoji)
310 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
311 name = String.trim(tag["name"], ":")
312 emoji = %{name => tag["icon"]["url"]}
314 Map.put(object, "emoji", emoji)
317 def fix_emoji(object), do: object
319 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
322 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
323 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
325 Map.put(object, "tag", tag ++ tags)
328 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
329 combined = [tag, String.slice(hashtag, 1..-1)]
331 Map.put(object, "tag", combined)
334 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
336 def fix_tag(object), do: object
338 # content map usually only has one language so this will do for now.
339 def fix_content_map(%{"contentMap" => content_map} = object) do
340 content_groups = Map.to_list(content_map)
341 {_, content} = Enum.at(content_groups, 0)
343 Map.put(object, "content", content)
346 def fix_content_map(object), do: object
348 def fix_type(object, options \\ [])
350 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
351 when is_binary(reply_id) do
352 with true <- Federator.allowed_thread_distance?(options[:depth]),
353 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
354 Map.put(object, "type", "Answer")
360 def fix_type(object, _), do: object
362 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
363 when is_binary(content) do
366 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
367 |> Pleroma.HTML.filter_tags()
369 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
372 defp fix_content(object), do: object
374 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
375 with true <- id =~ "follows",
376 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
377 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
384 defp mastodon_follow_hack(_, _), do: {:error, nil}
386 defp get_follow_activity(follow_object, followed) do
387 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
388 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
391 # Can't find the activity. This might a Mastodon 2.3 "Accept"
393 mastodon_follow_hack(follow_object, followed)
400 # Reduce the object list to find the reported user.
401 defp get_reported(objects) do
402 Enum.reduce_while(objects, nil, fn ap_id, _ ->
403 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
411 def handle_incoming(data, options \\ [])
413 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
415 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
416 with context <- data["context"] || Utils.generate_context_id(),
417 content <- data["content"] || "",
418 %User{} = actor <- User.get_cached_by_ap_id(actor),
419 # Reduce the object list to find the reported user.
420 %User{} = account <- get_reported(objects),
421 # Remove the reported user from the object list.
422 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
429 additional: %{"cc" => [account.ap_id]}
431 |> ActivityPub.flag()
435 # disallow objects with bogus IDs
436 def handle_incoming(%{"id" => nil}, _options), do: :error
437 def handle_incoming(%{"id" => ""}, _options), do: :error
438 # length of https:// = 8, should validate better, but good enough for now.
439 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
442 # TODO: validate those with a Ecto scheme
446 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
449 when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
450 actor = Containment.get_actor(data)
453 Map.put(data, "actor", actor)
456 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
457 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
458 object = fix_object(object, options)
464 context: object["conversation"],
466 published: data["published"],
475 with {:ok, created_activity} <- ActivityPub.create(params) do
476 reply_depth = (options[:depth] || 0) + 1
478 if Federator.allowed_thread_distance?(reply_depth) do
479 for reply_id <- replies(object) do
480 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
482 "depth" => reply_depth
487 {:ok, created_activity}
490 %Activity{} = activity -> {:ok, activity}
496 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
499 actor = Containment.get_actor(data)
502 Map.put(data, "actor", actor)
505 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
506 reply_depth = (options[:depth] || 0) + 1
507 options = Keyword.put(options, :depth, reply_depth)
508 object = fix_object(object, options)
516 published: data["published"],
517 additional: Map.take(data, ["cc", "id"])
520 ActivityPub.listen(params)
527 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
530 with %User{local: true} = followed <-
531 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
532 {:ok, %User{} = follower} <-
533 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
534 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
535 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
536 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
537 {_, false} <- {:user_locked, User.locked?(followed)},
538 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
540 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
541 {:ok, _relationship} <-
542 FollowingRelationship.update(follower, followed, :follow_accept) do
543 ActivityPub.accept(%{
544 to: [follower.ap_id],
550 {:user_blocked, true} ->
551 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
552 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
554 ActivityPub.reject(%{
555 to: [follower.ap_id],
561 {:follow, {:error, _}} ->
562 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
563 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
565 ActivityPub.reject(%{
566 to: [follower.ap_id],
572 {:user_locked, true} ->
573 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
585 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
588 with actor <- Containment.get_actor(data),
589 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
590 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
591 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
592 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
593 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
594 ActivityPub.accept(%{
595 to: follow_activity.data["to"],
598 object: follow_activity.data["id"],
608 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
611 with actor <- Containment.get_actor(data),
612 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
613 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
614 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
615 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
616 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
618 ActivityPub.reject(%{
619 to: follow_activity.data["to"],
622 object: follow_activity.data["id"],
632 @misskey_reactions %{
646 @doc "Rewrite misskey likes into EmojiReacts"
650 "_misskey_reaction" => reaction
655 |> Map.put("type", "EmojiReact")
656 |> Map.put("content", @misskey_reactions[reaction] || reaction)
657 |> handle_incoming(options)
660 def handle_incoming(%{"type" => "Like"} = data, _options) do
661 with {_, {:ok, cast_data_sym}} <-
663 data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
664 cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
665 :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
666 {_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
667 {_, {:ok, cast_data}} <-
668 {:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
669 {_, {:ok, activity, _meta}} <-
670 {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
679 "type" => "EmojiReact",
680 "object" => object_id,
687 with actor <- Containment.get_actor(data),
688 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
689 {:ok, object} <- get_obj_helper(object_id),
690 {:ok, activity, _object} <-
691 ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
699 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
702 with actor <- Containment.get_actor(data),
703 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
704 {:ok, object} <- get_embedded_obj_helper(object_id, actor),
705 public <- Visibility.is_public?(data),
706 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
714 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
718 when object_type in [
724 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
725 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
728 |> User.upgrade_changeset(new_user_data, true)
729 |> User.update_and_set_cache()
731 ActivityPub.update(%{
733 to: data["to"] || [],
734 cc: data["cc"] || [],
737 activity_id: data["id"]
746 # TODO: We presently assume that any actor on the same origin domain as the object being
747 # deleted has the rights to delete that object. A better way to validate whether or not
748 # the object should be deleted is to refetch the object URI, which should return either
749 # an error or a tombstone. This would allow us to verify that a deletion actually took
752 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
755 object_id = Utils.get_ap_id(object_id)
757 with actor <- Containment.get_actor(data),
758 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
759 {:ok, object} <- get_obj_helper(object_id),
760 :ok <- Containment.contain_origin(actor.ap_id, object.data),
762 ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
766 case User.get_cached_by_ap_id(object_id) do
767 %User{ap_id: ^actor} = user ->
782 "object" => %{"type" => "Announce", "object" => object_id},
788 with actor <- Containment.get_actor(data),
789 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
790 {:ok, object} <- get_obj_helper(object_id),
791 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
801 "object" => %{"type" => "Follow", "object" => followed},
807 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
808 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
809 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
810 User.unfollow(follower, followed)
820 "object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
826 with actor <- Containment.get_actor(data),
827 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
828 {:ok, activity, _} <-
829 ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
842 "object" => %{"type" => "Block", "object" => blocked},
848 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
849 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
850 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
851 User.unblock(blocker, blocked)
859 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
862 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
863 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
864 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
865 User.unfollow(blocker, blocked)
866 User.block(blocker, blocked)
876 "object" => %{"type" => "Like", "object" => object_id},
882 with actor <- Containment.get_actor(data),
883 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
884 {:ok, object} <- get_obj_helper(object_id),
885 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
892 # For Undos that don't have the complete object attached, try to find it in our database.
900 when is_binary(object) do
901 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
903 |> Map.put("object", data)
904 |> handle_incoming(options)
913 "actor" => origin_actor,
914 "object" => origin_actor,
915 "target" => target_actor
919 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
920 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
921 true <- origin_actor in target_user.also_known_as do
922 ActivityPub.move(origin_user, target_user, false)
928 def handle_incoming(_, _), do: :error
930 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
931 def get_obj_helper(id, options \\ []) do
932 case Object.normalize(id, true, options) do
933 %Object{} = object -> {:ok, object}
938 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
939 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
942 when attributed_to == ap_id do
943 with {:ok, activity} <-
948 "actor" => attributed_to,
951 {:ok, Object.normalize(activity)}
953 _ -> get_obj_helper(object_id)
957 def get_embedded_obj_helper(object_id, _) do
958 get_obj_helper(object_id)
961 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
962 with false <- String.starts_with?(in_reply_to, "http"),
963 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
964 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
970 def set_reply_to_uri(obj), do: obj
973 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
974 Based on Mastodon's ActivityPub::NoteSerializer#replies.
976 def set_replies(obj_data) do
978 with limit when limit > 0 <-
979 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
980 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
982 |> Object.self_replies()
983 |> select([o], fragment("?->>'id'", o.data))
990 set_replies(obj_data, replies_uris)
993 defp set_replies(obj, []) do
997 defp set_replies(obj, replies_uris) do
998 replies_collection = %{
999 "type" => "Collection",
1000 "items" => replies_uris
1003 Map.merge(obj, %{"replies" => replies_collection})
1006 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
1010 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
1014 def replies(_), do: []
1016 # Prepares the object of an outgoing create activity.
1017 def prepare_object(object) do
1023 |> add_attributed_to
1024 |> prepare_attachments
1028 |> strip_internal_fields
1029 |> strip_internal_tags
1035 # internal -> Mastodon
1038 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
1039 when activity_type in ["Create", "Listen"] do
1042 |> Object.normalize()
1048 |> Map.put("object", object)
1049 |> Map.merge(Utils.make_json_ld_header())
1050 |> Map.delete("bcc")
1055 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
1058 |> Object.normalize()
1061 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
1062 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
1064 data |> maybe_fix_object_url
1069 |> strip_internal_fields
1070 |> Map.merge(Utils.make_json_ld_header())
1071 |> Map.delete("bcc")
1076 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
1077 # because of course it does.
1078 def prepare_outgoing(%{"type" => "Accept"} = data) do
1079 with follow_activity <- Activity.normalize(data["object"]) do
1081 "actor" => follow_activity.actor,
1082 "object" => follow_activity.data["object"],
1083 "id" => follow_activity.data["id"],
1089 |> Map.put("object", object)
1090 |> Map.merge(Utils.make_json_ld_header())
1096 def prepare_outgoing(%{"type" => "Reject"} = data) do
1097 with follow_activity <- Activity.normalize(data["object"]) do
1099 "actor" => follow_activity.actor,
1100 "object" => follow_activity.data["object"],
1101 "id" => follow_activity.data["id"],
1107 |> Map.put("object", object)
1108 |> Map.merge(Utils.make_json_ld_header())
1114 def prepare_outgoing(%{"type" => _type} = data) do
1117 |> strip_internal_fields
1118 |> maybe_fix_object_url
1119 |> Map.merge(Utils.make_json_ld_header())
1124 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
1125 with false <- String.starts_with?(object, "http"),
1126 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
1127 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
1129 Map.put(data, "object", external_url)
1132 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1140 def maybe_fix_object_url(data), do: data
1142 def add_hashtags(object) do
1144 (object["tag"] || [])
1146 # Expand internal representation tags into AS2 tags.
1147 tag when is_binary(tag) ->
1149 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1150 "name" => "##{tag}",
1154 # Do not process tags which are already AS2 tag objects.
1155 tag when is_map(tag) ->
1159 Map.put(object, "tag", tags)
1162 def add_mention_tags(object) do
1163 {enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
1164 potential_receivers = enabled_receivers ++ disabled_receivers
1165 mentions = Enum.map(potential_receivers, &build_mention_tag/1)
1167 tags = object["tag"] || []
1168 Map.put(object, "tag", tags ++ mentions)
1171 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1172 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1175 def take_emoji_tags(%User{emoji: emoji}) do
1177 |> Enum.flat_map(&Map.to_list/1)
1178 |> Enum.map(&build_emoji_tag/1)
1181 # TODO: we should probably send mtime instead of unix epoch time for updated
1182 def add_emoji_tags(%{"emoji" => emoji} = object) do
1183 tags = object["tag"] || []
1185 out = Enum.map(emoji, &build_emoji_tag/1)
1187 Map.put(object, "tag", tags ++ out)
1190 def add_emoji_tags(object), do: object
1192 defp build_emoji_tag({name, url}) do
1194 "icon" => %{"url" => url, "type" => "Image"},
1195 "name" => ":" <> name <> ":",
1197 "updated" => "1970-01-01T00:00:00Z",
1202 def set_conversation(object) do
1203 Map.put(object, "conversation", object["context"])
1206 def set_sensitive(object) do
1207 tags = object["tag"] || []
1208 Map.put(object, "sensitive", "nsfw" in tags)
1211 def set_type(%{"type" => "Answer"} = object) do
1212 Map.put(object, "type", "Note")
1215 def set_type(object), do: object
1217 def add_attributed_to(object) do
1218 attributed_to = object["attributedTo"] || object["actor"]
1219 Map.put(object, "attributedTo", attributed_to)
1222 def prepare_attachments(object) do
1225 |> Map.get("attachment", [])
1226 |> Enum.map(fn data ->
1227 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1231 "mediaType" => media_type,
1232 "name" => data["name"],
1233 "type" => "Document"
1237 Map.put(object, "attachment", attachments)
1240 def strip_internal_fields(object) do
1241 Map.drop(object, Pleroma.Constants.object_internal_fields())
1244 defp strip_internal_tags(%{"tag" => tags} = object) do
1245 tags = Enum.filter(tags, fn x -> is_map(x) end)
1247 Map.put(object, "tag", tags)
1250 defp strip_internal_tags(object), do: object
1252 def perform(:user_upgrade, user) do
1253 # we pass a fake user so that the followers collection is stripped away
1254 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1258 where: ^old_follower_address in a.recipients,
1263 "array_replace(?,?,?)",
1265 ^old_follower_address,
1266 ^user.follower_address
1271 |> Repo.update_all([])
1274 def upgrade_user_from_ap_id(ap_id) do
1275 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1276 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1277 already_ap <- User.ap_enabled?(user),
1278 {:ok, user} <- upgrade_user(user, data) do
1279 if not already_ap do
1280 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1285 %User{} = user -> {:ok, user}
1290 defp upgrade_user(user, data) do
1292 |> User.upgrade_changeset(data, true)
1293 |> User.update_and_set_cache()
1296 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1297 Map.put(data, "url", url["href"])
1300 def maybe_fix_user_url(data), do: data
1302 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1304 defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
1307 defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
1308 with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
1309 {:ok, Map.put(data, "context", context)}
1312 {:error, :no_context}
1316 defp ensure_context_presence(_) do
1317 {:error, :no_context}
1320 defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
1323 defp ensure_recipients_presence(%{"object" => object} = data) do
1324 case Object.normalize(object) do
1325 %{data: %{"actor" => actor}} ->
1328 |> Map.put("to", [actor])
1329 |> Map.put("cc", data["cc"] || [])
1334 {:error, :no_object}
1341 defp ensure_recipients_presence(_) do
1342 {:error, :no_object}