# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.Fetcher do
alias Pleroma.HTTP
+ alias Pleroma.Instances
+ alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
- alias Pleroma.Web.FedSockets
require Logger
require Pleroma.Constants
end
defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
+ has_history? = fn
+ %{"formerRepresentations" => %{"orderedItems" => list}} when is_list(list) -> true
+ _ -> false
+ end
+
internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
+ remote_history_exists? = has_history?.(new_data)
+
+ # If the remote history exists, we treat that as the only source of truth.
+ new_data =
+ if has_history?.(old_data) and not remote_history_exists? do
+ Map.put(new_data, "formerRepresentations", old_data["formerRepresentations"])
+ else
+ new_data
+ end
+
+ # If the remote does not have history information, we need to manage it ourselves
+ new_data =
+ if not remote_history_exists? do
+ changed? =
+ Pleroma.Constants.status_updatable_fields()
+ |> Enum.any?(fn field -> Map.get(old_data, field) != Map.get(new_data, field) end)
+
+ %{updated_object: updated_object} =
+ new_data
+ |> Object.Updater.maybe_update_history(old_data,
+ updated: changed?,
+ use_history_in_new_object?: false
+ )
+
+ updated_object
+ else
+ new_data
+ end
+
Map.merge(new_data, internal_fields)
end
# Note: will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id, options \\ []) do
- with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
+ with %URI{} = uri <- URI.parse(id),
+ # If we have instance restrictions, apply them here to prevent fetching from unwanted instances
+ {:ok, nil} <- Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_reject(uri),
+ {:ok, _} <- Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri),
+ {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
{_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
- {_, nil} <- {:normalize, Object.normalize(data, false)},
+ {_, nil} <- {:normalize, Object.normalize(data, fetch: false)},
params <- prepare_activity_params(data),
{_, :ok} <- {:containment, Containment.contain_origin(id, params)},
{_, {:ok, activity}} <-
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
{_, _data, %Object{} = object} <-
- {:object, data, Object.normalize(activity, false)} do
+ {:object, data, Object.normalize(activity, fetch: false)} do
{:ok, object}
else
{:allowed_depth, false} ->
{:transmogrifier, {:error, {:reject, e}}} ->
{:reject, e}
+ {:transmogrifier, {:reject, e}} ->
+ {:reject, e}
+
{:transmogrifier, _} = e ->
{:error, e}
{:fetch, {:error, error}} ->
{:error, error}
+ {:reject, reason} ->
+ {:reject, reason}
+
e ->
e
end
defp prepare_activity_params(data) do
%{
"type" => "Create",
- "to" => data["to"] || [],
- "cc" => data["cc"] || [],
# Should we seriously keep this attributedTo thing?
"actor" => data["actor"] || data["attributedTo"],
"object" => data
}
+ |> Maps.put_if_present("to", data["to"])
+ |> Maps.put_if_present("cc", data["cc"])
+ |> Maps.put_if_present("bto", data["bto"])
+ |> Maps.put_if_present("bcc", data["bcc"])
end
def fetch_object_from_id!(id, options \\ []) do
{:error, %Tesla.Mock.Error{}} ->
nil
- {:error, "Object has been deleted"} ->
+ {:error, {"Object has been deleted", _id, _code}} ->
nil
{:reject, reason} ->
- Logger.info("Rejected #{id} while fetching: #{inspect(reason)}")
+ Logger.debug("Rejected #{id} while fetching: #{inspect(reason)}")
nil
e ->
end
end
- def fetch_and_contain_remote_object_from_id(prm, opts \\ [])
+ def fetch_and_contain_remote_object_from_id(id)
- def fetch_and_contain_remote_object_from_id(%{"id" => id}, opts),
- do: fetch_and_contain_remote_object_from_id(id, opts)
+ def fetch_and_contain_remote_object_from_id(%{"id" => id}),
+ do: fetch_and_contain_remote_object_from_id(id)
- def fetch_and_contain_remote_object_from_id(id, opts) when is_binary(id) do
+ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
Logger.debug("Fetching object #{id} via AP")
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
- {:ok, body} <- get_object(id, opts),
+ {:ok, body} <- get_object(id),
{:ok, data} <- safe_json_decode(body),
:ok <- Containment.contain_origin_from_id(id, data) do
+ unless Instances.reachable?(id) do
+ Instances.set_reachable(id)
+ end
+
{:ok, data}
else
{:scheme, _} ->
end
end
- def fetch_and_contain_remote_object_from_id(_id, _opts),
+ def fetch_and_contain_remote_object_from_id(_id),
do: {:error, "id must be a string"}
- defp get_object(id, opts) do
- with false <- Keyword.get(opts, :force_http, false),
- {:ok, fedsocket} <- FedSockets.get_or_create_fed_socket(id) do
- Logger.debug("fetching via fedsocket - #{inspect(id)}")
- FedSockets.fetch(fedsocket, id)
- else
- _other ->
- Logger.debug("fetching via http - #{inspect(id)}")
- get_object_http(id)
- end
- end
-
- defp get_object_http(id) do
+ def get_object(id) do
date = Pleroma.Signature.signed_date()
headers =
|> sign_fetch(id, date)
case HTTP.get(id, headers) do
- {:ok, %{body: body, status: code}} when code in 200..299 ->
- {:ok, body}
+ {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
+ case List.keyfind(headers, "content-type", 0) do
+ {_, content_type} ->
+ case Plug.Conn.Utils.media_type(content_type) do
+ {:ok, "application", "activity+json", _} ->
+ {:ok, body}
+
+ {:ok, "application", "ld+json",
+ %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
+ {:ok, body}
+
+ # pixelfed sometimes (and only sometimes) responds with http instead of https
+ {:ok, "application", "ld+json",
+ %{"profile" => "http://www.w3.org/ns/activitystreams"}} ->
+ {:ok, body}
+
+ _ ->
+ {:error, {:content_type, content_type}}
+ end
+
+ _ ->
+ {:error, {:content_type, nil}}
+ end
{:ok, %{status: code}} when code in [404, 410] ->
- {:error, "Object has been deleted"}
+ {:error, {"Object has been deleted", id, code}}
{:error, e} ->
{:error, e}