
It is possible for an earlier Update to be received by us later. For this, we now (1) only allows Updates to poll counts if there is no updated field, or the updated field is the same as the last updated date or creation date; (2) does not allow updating anything if the updated field is older than the last updated date or creation date; (3) allows updating updatable fields otherwise (normal updates); (4) if only the updated field is changed, it does not create a new history item on its own.
201 lines
6.1 KiB
Elixir
201 lines
6.1 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Pleroma.Object.Updater do
|
|
require Pleroma.Constants
|
|
|
|
def update_content_fields(orig_object_data, updated_object) do
|
|
Pleroma.Constants.status_updatable_fields()
|
|
|> Enum.reduce(
|
|
%{data: orig_object_data, updated: false},
|
|
fn field, %{data: data, updated: updated} ->
|
|
updated =
|
|
updated or
|
|
(field != "updated" and
|
|
Map.get(updated_object, field) != Map.get(orig_object_data, field))
|
|
|
|
data =
|
|
if Map.has_key?(updated_object, field) do
|
|
Map.put(data, field, updated_object[field])
|
|
else
|
|
Map.drop(data, [field])
|
|
end
|
|
|
|
%{data: data, updated: updated}
|
|
end
|
|
)
|
|
end
|
|
|
|
def maybe_history(object) do
|
|
with history <- Map.get(object, "formerRepresentations"),
|
|
true <- is_map(history),
|
|
"OrderedCollection" <- Map.get(history, "type"),
|
|
true <- is_list(Map.get(history, "orderedItems")),
|
|
true <- is_integer(Map.get(history, "totalItems")) do
|
|
history
|
|
else
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
def history_for(object) do
|
|
with history when not is_nil(history) <- maybe_history(object) do
|
|
history
|
|
else
|
|
_ -> history_skeleton()
|
|
end
|
|
end
|
|
|
|
defp history_skeleton do
|
|
%{
|
|
"type" => "OrderedCollection",
|
|
"totalItems" => 0,
|
|
"orderedItems" => []
|
|
}
|
|
end
|
|
|
|
def maybe_update_history(
|
|
updated_object,
|
|
orig_object_data,
|
|
opts
|
|
) do
|
|
updated = opts[:updated]
|
|
use_history_in_new_object? = opts[:use_history_in_new_object?]
|
|
|
|
if not updated do
|
|
%{updated_object: updated_object, used_history_in_new_object?: false}
|
|
else
|
|
# Put edit history
|
|
# Note that we may have got the edit history by first fetching the object
|
|
{new_history, used_history_in_new_object?} =
|
|
with true <- use_history_in_new_object?,
|
|
updated_history when not is_nil(updated_history) <- maybe_history(opts[:new_data]) do
|
|
{updated_history, true}
|
|
else
|
|
_ ->
|
|
history = history_for(orig_object_data)
|
|
|
|
latest_history_item =
|
|
orig_object_data
|
|
|> Map.drop(["id", "formerRepresentations"])
|
|
|
|
updated_history =
|
|
history
|
|
|> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
|
|
|> Map.put("totalItems", history["totalItems"] + 1)
|
|
|
|
{updated_history, false}
|
|
end
|
|
|
|
updated_object =
|
|
updated_object
|
|
|> Map.put("formerRepresentations", new_history)
|
|
|
|
%{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?}
|
|
end
|
|
end
|
|
|
|
defp maybe_update_poll(to_be_updated, updated_object) do
|
|
choice_key = fn data ->
|
|
if Map.has_key?(data, "anyOf"), do: "anyOf", else: "oneOf"
|
|
end
|
|
|
|
with true <- to_be_updated["type"] == "Question",
|
|
key <- choice_key.(updated_object),
|
|
true <- key == choice_key.(to_be_updated),
|
|
orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])),
|
|
new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])),
|
|
true <- orig_choices == new_choices do
|
|
# Choices are the same, but counts are different
|
|
to_be_updated
|
|
|> Map.put(key, updated_object[key])
|
|
else
|
|
# Choices (or vote type) have changed, do not allow this
|
|
_ -> to_be_updated
|
|
end
|
|
end
|
|
|
|
# This calculates the data to be sent as the object of an Update.
|
|
# new_data's formerRepresentations is not considered.
|
|
# formerRepresentations is added to the returned data.
|
|
def make_update_object_data(original_data, new_data, date) do
|
|
%{data: updated_data, updated: updated} =
|
|
original_data
|
|
|> update_content_fields(new_data)
|
|
|
|
if not updated do
|
|
updated_data
|
|
else
|
|
%{updated_object: updated_data} =
|
|
updated_data
|
|
|> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
|
|
|
|
updated_data
|
|
|> Map.put("updated", date)
|
|
end
|
|
end
|
|
|
|
# This calculates the data of the new Object from an Update.
|
|
# new_data's formerRepresentations is considered.
|
|
def make_new_object_data_from_update_object(original_data, new_data) do
|
|
update_is_reasonable =
|
|
with {_, updated} when not is_nil(updated) <- {:cur_updated, new_data["updated"]},
|
|
{_, {:ok, updated_time, _}} <- {:cur_updated, DateTime.from_iso8601(updated)},
|
|
{_, last_updated} when not is_nil(last_updated) <-
|
|
{:last_updated, original_data["updated"] || original_data["published"]},
|
|
{_, {:ok, last_updated_time, _}} <-
|
|
{:last_updated, DateTime.from_iso8601(last_updated)},
|
|
:gt <- DateTime.compare(updated_time, last_updated_time) do
|
|
:update_everything
|
|
else
|
|
# only allow poll updates
|
|
{:cur_updated, _} -> :no_content_update
|
|
:eq -> :no_content_update
|
|
# allow all updates
|
|
{:last_updated, _} -> :update_everything
|
|
# allow no updates
|
|
_ -> false
|
|
end
|
|
|
|
%{
|
|
updated_object: updated_data,
|
|
used_history_in_new_object?: used_history_in_new_object?,
|
|
updated: updated
|
|
} =
|
|
if update_is_reasonable == :update_everything do
|
|
%{data: updated_data, updated: updated} =
|
|
original_data
|
|
|> update_content_fields(new_data)
|
|
|
|
updated_data
|
|
|> maybe_update_history(original_data,
|
|
updated: updated,
|
|
use_history_in_new_object?: true,
|
|
new_data: new_data
|
|
)
|
|
|> Map.put(:updated, updated)
|
|
else
|
|
%{
|
|
updated_object: original_data,
|
|
used_history_in_new_object?: false,
|
|
updated: false
|
|
}
|
|
end
|
|
|
|
updated_data =
|
|
if update_is_reasonable != false do
|
|
updated_data
|
|
|> maybe_update_poll(new_data)
|
|
else
|
|
updated_data
|
|
end
|
|
|
|
%{
|
|
updated_data: updated_data,
|
|
updated: updated,
|
|
used_history_in_new_object?: used_history_in_new_object?
|
|
}
|
|
end
|
|
end
|