X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fweb%2Factivity_pub%2Ftransmogrifier.ex;h=7081213e7be459ce15f8c2f89016db72db25616f;hb=e124a109c1897529b4b9eae563f550b9fb5dfb50;hp=d27d0bed4e11f096b52e1afaa404ddffa4b24305;hpb=7ac4da8dd4471455c7d243983d0e1ca6fa32a14e;p=akkoma diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d27d0bed4..7081213e7 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.Federator alias Pleroma.Workers.TransmogrifierWorker @@ -38,12 +39,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> fix_attachments() |> fix_context() |> fix_in_reply_to(options) + |> fix_quote_url(options) |> fix_emoji() |> fix_tag() |> fix_content_map() |> fix_addressing() |> fix_summary() - |> fix_type(options) end def fix_summary(%{"summary" => nil} = object) do @@ -72,17 +73,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def fix_explicit_addressing( - %{"to" => to, "cc" => cc} = object, - explicit_mentions, - follower_collection - ) do - explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end) + # if directMessage flag is set to true, leave the addressing alone + def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection), + do: object + + def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collection) do + explicit_mentions = + Utils.determine_explicit_mentions(object) ++ + [Pleroma.Constants.as_public(), follower_collection] + explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end) explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end) final_cc = (cc ++ explicit_cc) + |> Enum.filter(& &1) |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end) |> Enum.uniq() @@ -91,65 +96,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> Map.put("cc", final_cc) end - def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object - - # if directMessage flag is set to true, leave the addressing alone - def fix_explicit_addressing(%{"directMessage" => true} = object), do: object - - def fix_explicit_addressing(object) do - explicit_mentions = Utils.determine_explicit_mentions(object) - - %User{follower_address: follower_collection} = + def fix_addressing(object) do + {:ok, %User{follower_address: follower_collection}} = object |> Containment.get_actor() - |> User.get_cached_by_ap_id() - - explicit_mentions = - explicit_mentions ++ - [ - Pleroma.Constants.as_public(), - follower_collection - ] - - fix_explicit_addressing(object, explicit_mentions, follower_collection) - end - - # if as:Public is addressed, then make sure the followers collection is also addressed - # so that the activities will be delivered to local users. - def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do - recipients = to ++ cc - - if followers_collection not in recipients do - cond do - Pleroma.Constants.as_public() in cc -> - to = to ++ [followers_collection] - Map.put(object, "to", to) - - Pleroma.Constants.as_public() in to -> - cc = cc ++ [followers_collection] - Map.put(object, "cc", cc) - - true -> - object - end - else - object - end - end - - def fix_implicit_addressing(object, _), do: object - - def fix_addressing(object) do - {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"]) - followers_collection = User.ap_followers(user) + |> User.get_or_fetch_by_ap_id() object |> fix_addressing_list("to") |> fix_addressing_list("cc") |> fix_addressing_list("bto") |> fix_addressing_list("bcc") - |> fix_explicit_addressing() - |> fix_implicit_addressing(followers_collection) + |> fix_explicit_addressing(follower_collection) + |> CommonFixes.fix_implicit_addressing(follower_collection) end def fix_actor(%{"attributedTo" => actor} = object) do @@ -177,7 +136,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> Map.drop(["conversation", "inReplyToAtomUri"]) else e -> - Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}") + Logger.warn("Couldn't fetch reply@#{inspect(in_reply_to_id)}, error: #{inspect(e)}") object end else @@ -187,6 +146,53 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_in_reply_to(object, _options), do: object + def fix_quote_url(object, options \\ []) + + def fix_quote_url(%{"quoteUri" => quote_url} = object, options) + when not is_nil(quote_url) do + depth = (options[:depth] || 0) + 1 + + if Federator.allowed_thread_distance?(depth) do + with {:ok, quoted_object} <- get_obj_helper(quote_url, options), + %Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do + object + |> Map.put("quoteUri", quoted_object.data["id"]) + else + e -> + Logger.warn("Couldn't fetch quote@#{inspect(quote_url)}, error: #{inspect(e)}") + object + end + else + object + end + end + + # Soapbox + def fix_quote_url(%{"quoteUrl" => quote_url} = object, options) do + object + |> Map.put("quoteUri", quote_url) + |> Map.delete("quoteUrl") + |> fix_quote_url(options) + end + + # Old Fedibird (bug) + # https://github.com/fedibird/mastodon/issues/9 + def fix_quote_url(%{"quoteURL" => quote_url} = object, options) do + object + |> Map.put("quoteUri", quote_url) + |> Map.delete("quoteURL") + |> fix_quote_url(options) + end + + def fix_quote_url(%{"_misskey_quote" => quote_url} = object, options) do + object + |> Map.put("quoteUri", quote_url) + |> Map.delete("_misskey_quote") + |> fix_quote_url(options) + end + + def fix_quote_url(object, _), do: object + defp prepare_in_reply_to(in_reply_to) do cond do is_bitstring(in_reply_to) -> @@ -223,10 +229,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do media_type = cond do - is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"] - MIME.valid?(data["mediaType"]) -> data["mediaType"] - MIME.valid?(data["mimeType"]) -> data["mimeType"] - true -> nil + is_map(url) && MIME.extensions(url["mediaType"]) != [] -> + url["mediaType"] + + is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] -> + data["mediaType"] + + is_bitstring(data["mimeType"]) && MIME.extensions(data["mimeType"]) != [] -> + data["mimeType"] + + true -> + nil end href = @@ -244,6 +257,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "type" => Map.get(url || %{}, "type", "Link") } |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("width", (url || %{})["width"] || data["width"]) + |> Maps.put_if_present("height", (url || %{})["height"] || data["height"]) %{ "url" => [attachment_url], @@ -331,28 +346,32 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_tag(object), do: object # content map usually only has one language so this will do for now. - def fix_content_map(%{"contentMap" => content_map} = object) do + def fix_content_map(%{"contentMap" => content_map} = object) when is_map(content_map) do content_groups = Map.to_list(content_map) - {_, content} = Enum.at(content_groups, 0) - Map.put(object, "content", content) + if Enum.empty?(content_groups) do + object + else + {_, content} = Enum.at(content_groups, 0) + + Map.put(object, "content", content) + end end def fix_content_map(object), do: object - def fix_type(object, options \\ []) + defp fix_type(%{"type" => "Note", "inReplyTo" => reply_id, "name" => _} = object, options) + when is_binary(reply_id) do + options = Keyword.put(options, :fetch, true) - def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options) - when is_binary(reply_id) do - with true <- Federator.allowed_thread_distance?(options[:depth]), - {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do + with %Object{data: %{"type" => "Question"}} <- Object.normalize(reply_id, options) do Map.put(object, "type", "Answer") else _ -> object end end - def fix_type(object, _), do: object + defp fix_type(object, _options), do: object # Reduce the object list to find the reported user. defp get_reported(objects) do @@ -365,29 +384,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end) end - # Compatibility wrapper for Mastodon votes - defp handle_create(%{"object" => %{"type" => "Answer"}} = data, _user) do - handle_incoming(data) - end - - defp handle_create(%{"object" => object} = data, user) do - %{ - to: data["to"], - object: object, - actor: user, - context: object["context"], - local: false, - published: data["published"], - additional: - Map.take(data, [ - "cc", - "directMessage", - "id" - ]) - } - |> ActivityPub.create() - end - def handle_incoming(data, options \\ []) # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them @@ -419,113 +415,54 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8, do: :error - # TODO: validate those with a Ecto scheme - # - tags - # - emoji - def handle_incoming( - %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, - options - ) - when objtype in ~w{Note Page} do - actor = Containment.get_actor(data) - - with nil <- Activity.get_create_by_object_ap_id(object["id"]), - {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do - data = - data - |> Map.put("object", fix_object(object, options)) - |> Map.put("actor", actor) - |> fix_addressing() - - with {:ok, created_activity} <- handle_create(data, user) do - reply_depth = (options[:depth] || 0) + 1 - - if Federator.allowed_thread_distance?(reply_depth) do - for reply_id <- replies(object) do - Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{ - "id" => reply_id, - "depth" => reply_depth - }) - end - end - - {:ok, created_activity} - end - else - %Activity{} = activity -> {:ok, activity} - _e -> :error - end - end - - def handle_incoming( - %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data, - options - ) do - actor = Containment.get_actor(data) - - data = - Map.put(data, "actor", actor) - |> fix_addressing - - with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do - reply_depth = (options[:depth] || 0) + 1 - options = Keyword.put(options, :depth, reply_depth) - object = fix_object(object, options) - - params = %{ - to: data["to"], - object: object, - actor: user, - context: nil, - local: false, - published: data["published"], - additional: Map.take(data, ["cc", "id"]) - } - - ActivityPub.listen(params) - else - _e -> :error - end - end - - @misskey_reactions %{ - "like" => "👍", - "love" => "❤️", - "laugh" => "😆", - "hmm" => "🤔", - "surprise" => "😮", - "congrats" => "🎉", - "angry" => "💢", - "confused" => "😥", - "rip" => "😇", - "pudding" => "🍮", - "star" => "⭐" - } - @doc "Rewrite misskey likes into EmojiReacts" def handle_incoming( %{ "type" => "Like", - "_misskey_reaction" => reaction + "content" => reaction } = data, options ) do - data - |> Map.put("type", "EmojiReact") - |> Map.put("content", @misskey_reactions[reaction] || reaction) - |> handle_incoming(options) + if Pleroma.Emoji.is_unicode_emoji?(reaction) or Pleroma.Emoji.matches_shortcode?(reaction) do + data + |> Map.put("type", "EmojiReact") + |> handle_incoming(options) + else + data + |> Map.delete("content") + |> handle_incoming(options) + end end def handle_incoming( %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, - _options + options ) - when objtype in ~w{Question Answer ChatMessage Audio Video Event Article} do - data = Map.put(data, "object", strip_internal_fields(data["object"])) + when objtype in ~w{Question Answer Audio Video Event Article Note Page} do + fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) + + object = + data["object"] + |> strip_internal_fields() + |> fix_type(fetch_options) + |> fix_in_reply_to(fetch_options) + |> fix_quote_url(fetch_options) + + # Only change the Create's context if the object's context has been modified. + data = + if data["object"]["context"] != object["context"] do + data + |> Map.put("object", object) + |> Map.put("context", object["context"]) + else + Map.put(data, "object", object) + end + + options = Keyword.put(options, :local, false) with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), nil <- Activity.get_create_by_object_ap_id(obj_id), - {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do + {:ok, activity, _} <- Pipeline.common_pipeline(data, options) do {:ok, activity} else %Activity{} = activity -> {:ok, activity} @@ -536,11 +473,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming(%{"type" => type} = data, _options) when type in ~w{Like EmojiReact Announce Add Remove} do with :ok <- ObjectValidator.fetch_actor_and_object(data), - {:ok, activity, _meta} <- - Pipeline.common_pipeline(data, local: false) do + {:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} else - e -> {:error, e} + e -> + {:error, e} end end @@ -693,6 +630,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def set_reply_to_uri(obj), do: obj + def set_quote_url(%{"quoteUri" => quote} = object) when is_binary(quote) do + Map.put(object, "quoteUrl", quote) + end + + def set_quote_url(obj), do: obj + @doc """ Serialized Mastodon-compatible `replies` collection containing _self-replies_. Based on Mastodon's ActivityPub::NoteSerializer#replies. @@ -747,10 +690,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> prepare_attachments |> set_conversation |> set_reply_to_uri + |> set_quote_url() |> set_replies |> strip_internal_fields |> strip_internal_tags |> set_type + |> maybe_process_history + end + + defp maybe_process_history(%{"formerRepresentations" => %{"orderedItems" => history}} = object) do + processed_history = + Enum.map( + history, + fn + item when is_map(item) -> prepare_object(item) + item -> item + end + ) + + put_in(object, ["formerRepresentations", "orderedItems"], processed_history) + end + + defp maybe_process_history(object) do + object end # @doc @@ -759,7 +721,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # """ def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data) - when activity_type in ["Create", "Listen"] do + when activity_type in ["Create"] do object = object_id |> Object.normalize(fetch: false) @@ -775,6 +737,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, data} end + def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) + when objtype in Pleroma.Constants.updatable_object_types() do + object = + object + |> prepare_object + + data = + data + |> Map.put("object", object) + |> Map.merge(Utils.make_json_ld_header()) + |> Map.delete("bcc") + + {:ok, data} + end + def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do object = object_id @@ -852,7 +829,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do Map.put(data, "object", external_url) else {:fetch, e} -> - Logger.error("Couldn't fetch #{object} #{inspect(e)}") + Logger.error("Couldn't fetch fixed_object@#{object} #{inspect(e)}") data _ -> @@ -941,15 +918,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do Map.put(object, "attributedTo", attributed_to) end - # TODO: Revisit this - def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object - def prepare_attachments(object) do attachments = object |> Map.get("attachment", []) |> Enum.map(fn data -> - [%{"mediaType" => media_type, "href" => href} | _] = data["url"] + [%{"mediaType" => media_type, "href" => href} = url | _] = data["url"] %{ "url" => href, @@ -957,6 +931,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "name" => data["name"], "type" => "Document" } + |> Maps.put_if_present("width", url["width"]) + |> Maps.put_if_present("height", url["height"]) + |> Maps.put_if_present("blurhash", data["blurhash"]) end) Map.put(object, "attachment", attachments)