1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Object.Updater do
6 require Pleroma.Constants
8 def update_content_fields(orig_object_data, updated_object) do
9 Pleroma.Constants.status_updatable_fields()
11 %{data: orig_object_data, updated: false},
12 fn field, %{data: data, updated: updated} ->
15 (field != "updated" and
16 Map.get(updated_object, field) != Map.get(orig_object_data, field))
19 if Map.has_key?(updated_object, field) do
20 Map.put(data, field, updated_object[field])
22 Map.drop(data, [field])
25 %{data: data, updated: updated}
30 def maybe_history(object) do
31 with history <- Map.get(object, "formerRepresentations"),
32 true <- is_map(history),
33 "OrderedCollection" <- Map.get(history, "type"),
34 true <- is_list(Map.get(history, "orderedItems")),
35 true <- is_integer(Map.get(history, "totalItems")) do
42 def history_for(object) do
43 with history when not is_nil(history) <- maybe_history(object) do
46 _ -> history_skeleton()
50 defp history_skeleton do
52 "type" => "OrderedCollection",
58 def maybe_update_history(
63 updated = opts[:updated]
64 use_history_in_new_object? = opts[:use_history_in_new_object?]
67 %{updated_object: updated_object, used_history_in_new_object?: false}
70 # Note that we may have got the edit history by first fetching the object
71 {new_history, used_history_in_new_object?} =
72 with true <- use_history_in_new_object?,
73 updated_history when not is_nil(updated_history) <- maybe_history(opts[:new_data]) do
74 {updated_history, true}
77 history = history_for(orig_object_data)
81 |> Map.drop(["id", "formerRepresentations"])
85 |> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
86 |> Map.put("totalItems", history["totalItems"] + 1)
88 {updated_history, false}
93 |> Map.put("formerRepresentations", new_history)
95 %{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?}
99 defp maybe_update_poll(to_be_updated, updated_object) do
100 choice_key = fn data ->
101 if Map.has_key?(data, "anyOf"), do: "anyOf", else: "oneOf"
104 with true <- to_be_updated["type"] == "Question",
105 key <- choice_key.(updated_object),
106 true <- key == choice_key.(to_be_updated),
107 orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])),
108 new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])),
109 true <- orig_choices == new_choices do
110 # Choices are the same, but counts are different
112 |> Map.put(key, updated_object[key])
114 # Choices (or vote type) have changed, do not allow this
119 # This calculates the data to be sent as the object of an Update.
120 # new_data's formerRepresentations is not considered.
121 # formerRepresentations is added to the returned data.
122 def make_update_object_data(original_data, new_data, date) do
123 %{data: updated_data, updated: updated} =
125 |> update_content_fields(new_data)
130 %{updated_object: updated_data} =
132 |> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
135 |> Map.put("updated", date)
139 # This calculates the data of the new Object from an Update.
140 # new_data's formerRepresentations is considered.
141 def make_new_object_data_from_update_object(original_data, new_data) do
142 update_is_reasonable =
143 with {_, updated} when not is_nil(updated) <- {:cur_updated, new_data["updated"]},
144 {_, {:ok, updated_time, _}} <- {:cur_updated, DateTime.from_iso8601(updated)},
145 {_, last_updated} when not is_nil(last_updated) <-
146 {:last_updated, original_data["updated"] || original_data["published"]},
147 {_, {:ok, last_updated_time, _}} <-
148 {:last_updated, DateTime.from_iso8601(last_updated)},
149 :gt <- DateTime.compare(updated_time, last_updated_time) do
152 # only allow poll updates
153 {:cur_updated, _} -> :no_content_update
154 :eq -> :no_content_update
156 {:last_updated, _} -> :update_everything
162 updated_object: updated_data,
163 used_history_in_new_object?: used_history_in_new_object?,
166 if update_is_reasonable == :update_everything do
167 %{data: updated_data, updated: updated} =
169 |> update_content_fields(new_data)
172 |> maybe_update_history(original_data,
174 use_history_in_new_object?: true,
177 |> Map.put(:updated, updated)
180 updated_object: original_data,
181 used_history_in_new_object?: false,
187 if update_is_reasonable != false do
189 |> maybe_update_poll(new_data)
195 updated_data: updated_data,
197 used_history_in_new_object?: used_history_in_new_object?
201 def for_each_history_item(%{"orderedItems" => items} = history, _object, fun) do
204 |> Enum.reduce_while(
207 {:ok, item}, {:ok, acc} -> {:cont, {:ok, acc ++ [item]}}
208 e, _acc -> {:halt, e}
213 {:ok, items} -> {:ok, Map.put(history, "orderedItems", items)}
218 def for_each_history_item(history, _, _) do
222 def do_with_history(object, fun) do
223 with history <- object["formerRepresentations"],
224 object <- Map.drop(object, ["formerRepresentations"]),
225 {_, {:ok, object}} <- {:main_body, fun.(object)},
226 {_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do
229 Map.put(object, "formerRepresentations", history)
237 {:history_items, e} -> e