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
|> 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
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()
|> 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
|> 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
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) ->
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 =
"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],
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
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
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}
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
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.
|> 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
# """
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)
{: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
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
_ ->
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,
"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)