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
11 def update_content_fields(orig_object_data, updated_object) do
12 Pleroma.Constants.status_updatable_fields()
14 %{data: orig_object_data, updated: false},
15 fn field, %{data: data, updated: updated} ->
18 (field != "updated" and
19 Map.get(updated_object, field) != Map.get(orig_object_data, field))
22 if Map.has_key?(updated_object, field) do
23 Map.put(data, field, updated_object[field])
25 Map.drop(data, [field])
28 %{data: data, updated: updated}
33 def maybe_history(object) do
34 with history <- Map.get(object, "formerRepresentations"),
35 true <- is_map(history),
36 "OrderedCollection" <- Map.get(history, "type"),
37 true <- is_list(Map.get(history, "orderedItems")),
38 true <- is_integer(Map.get(history, "totalItems")) do
45 def history_for(object) do
46 with history when not is_nil(history) <- maybe_history(object) do
49 _ -> history_skeleton()
53 defp history_skeleton do
55 "type" => "OrderedCollection",
61 def maybe_update_history(
66 updated = opts[:updated]
67 use_history_in_new_object? = opts[:use_history_in_new_object?]
70 %{updated_object: updated_object, used_history_in_new_object?: false}
73 # Note that we may have got the edit history by first fetching the object
74 {new_history, used_history_in_new_object?} =
75 with true <- use_history_in_new_object?,
76 updated_history when not is_nil(updated_history) <- maybe_history(opts[:new_data]) do
77 {updated_history, true}
80 history = history_for(orig_object_data)
84 |> Map.drop(["id", "formerRepresentations"])
88 |> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
89 |> Map.put("totalItems", history["totalItems"] + 1)
91 {updated_history, false}
96 |> Map.put("formerRepresentations", new_history)
98 %{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?}
102 defp maybe_update_poll(to_be_updated, updated_object) do
104 %{"anyOf" => [_ | _]} -> "anyOf"
105 %{"oneOf" => [_ | _]} -> "oneOf"
109 with true <- to_be_updated["type"] == "Question",
110 key when not is_nil(key) <- choice_key.(updated_object),
111 true <- key == choice_key.(to_be_updated),
112 orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])),
113 new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])),
114 true <- orig_choices == new_choices do
115 # Choices are the same, but counts are different
117 |> Map.put(key, updated_object[key])
119 # Choices (or vote type) have changed, do not allow this
124 # This calculates the data to be sent as the object of an Update.
125 # new_data's formerRepresentations is not considered.
126 # formerRepresentations is added to the returned data.
127 def make_update_object_data(original_data, new_data, date) do
128 %{data: updated_data, updated: updated} =
130 |> update_content_fields(new_data)
135 %{updated_object: updated_data} =
137 |> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
140 |> Map.put("updated", date)
144 # This calculates the data of the new Object from an Update.
145 # new_data's formerRepresentations is considered.
146 def make_new_object_data_from_update_object(original_data, new_data) do
147 update_is_reasonable =
148 with {_, updated} when not is_nil(updated) <- {:cur_updated, new_data["updated"]},
149 {_, {:ok, updated_time, _}} <- {:cur_updated, DateTime.from_iso8601(updated)},
150 {_, last_updated} when not is_nil(last_updated) <-
151 {:last_updated, original_data["updated"] || original_data["published"]},
152 {_, {:ok, last_updated_time, _}} <-
153 {:last_updated, DateTime.from_iso8601(last_updated)},
154 :gt <- DateTime.compare(updated_time, last_updated_time) do
157 # only allow poll updates
158 {:cur_updated, _} -> :no_content_update
159 :eq -> :no_content_update
161 {:last_updated, _} -> :update_everything
167 updated_object: updated_data,
168 used_history_in_new_object?: used_history_in_new_object?,
171 if update_is_reasonable == :update_everything do
172 %{data: updated_data, updated: updated} =
174 |> update_content_fields(new_data)
177 |> maybe_update_history(original_data,
179 use_history_in_new_object?: true,
182 |> Map.put(:updated, updated)
185 updated_object: original_data,
186 used_history_in_new_object?: false,
192 if update_is_reasonable != false do
194 |> maybe_update_poll(new_data)
200 updated_data: updated_data,
202 used_history_in_new_object?: used_history_in_new_object?
206 def for_each_history_item(%{"orderedItems" => items} = history, _object, fun) do
209 |> Enum.reduce_while(
212 {:ok, item}, {:ok, acc} -> {:cont, {:ok, acc ++ [item]}}
213 e, _acc -> {:halt, e}
218 {:ok, items} -> {:ok, Map.put(history, "orderedItems", items)}
223 def for_each_history_item(history, _, _) do
227 def do_with_history(object, fun) do
228 with history <- object["formerRepresentations"],
229 object <- Map.drop(object, ["formerRepresentations"]),
230 {_, {:ok, object}} <- {:main_body, fun.(object)},
231 {_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do
234 Map.put(object, "formerRepresentations", history)
242 {:history_items, e} -> e
246 defp maybe_touch_changeset(changeset, true) do
248 NaiveDateTime.utc_now()
249 |> NaiveDateTime.truncate(:second)
251 Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
254 defp maybe_touch_changeset(changeset, _), do: changeset
256 def do_update_and_invalidate_cache(orig_object, updated_object, touch_changeset? \\ false) do
257 orig_object_ap_id = updated_object["id"]
258 orig_object_data = orig_object.data
261 updated_data: updated_object_data,
263 used_history_in_new_object?: used_history_in_new_object?
264 } = make_new_object_data_from_update_object(orig_object_data, updated_object)
268 |> Repo.preload(:hashtags)
269 |> Object.change(%{data: updated_object_data})
270 |> maybe_touch_changeset(touch_changeset?)
272 with {:ok, new_object} <- Repo.update(changeset),
273 {:ok, _} <- Object.invalid_object_cache(new_object),
274 {:ok, _} <- Object.set_cache(new_object),
275 # The metadata/utils.ex uses the object id for the cache.
276 {:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(new_object.id) do
277 if used_history_in_new_object? do
278 with create_activity when not is_nil(create_activity) <-
279 Pleroma.Activity.get_create_by_object_ap_id(orig_object_ap_id),
280 {:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(create_activity.id) do
287 {:ok, new_object, updated}