b1e4870bac4c7900966a57d7dd6774606c0f6109
[anni] / lib / pleroma / object / updater.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Object.Updater do
6   require Pleroma.Constants
7
8   alias Pleroma.Object
9   alias Pleroma.Repo
10
11   def update_content_fields(orig_object_data, updated_object) do
12     Pleroma.Constants.status_updatable_fields()
13     |> Enum.reduce(
14       %{data: orig_object_data, updated: false},
15       fn field, %{data: data, updated: updated} ->
16         updated =
17           updated or
18             (field != "updated" and
19                Map.get(updated_object, field) != Map.get(orig_object_data, field))
20
21         data =
22           if Map.has_key?(updated_object, field) do
23             Map.put(data, field, updated_object[field])
24           else
25             Map.drop(data, [field])
26           end
27
28         %{data: data, updated: updated}
29       end
30     )
31   end
32
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
39       history
40     else
41       _ -> nil
42     end
43   end
44
45   def history_for(object) do
46     with history when not is_nil(history) <- maybe_history(object) do
47       history
48     else
49       _ -> history_skeleton()
50     end
51   end
52
53   defp history_skeleton do
54     %{
55       "type" => "OrderedCollection",
56       "totalItems" => 0,
57       "orderedItems" => []
58     }
59   end
60
61   def maybe_update_history(
62         updated_object,
63         orig_object_data,
64         opts
65       ) do
66     updated = opts[:updated]
67     use_history_in_new_object? = opts[:use_history_in_new_object?]
68
69     if not updated do
70       %{updated_object: updated_object, used_history_in_new_object?: false}
71     else
72       # Put edit history
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}
78         else
79           _ ->
80             history = history_for(orig_object_data)
81
82             latest_history_item =
83               orig_object_data
84               |> Map.drop(["id", "formerRepresentations"])
85
86             updated_history =
87               history
88               |> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
89               |> Map.put("totalItems", history["totalItems"] + 1)
90
91             {updated_history, false}
92         end
93
94       updated_object =
95         updated_object
96         |> Map.put("formerRepresentations", new_history)
97
98       %{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?}
99     end
100   end
101
102   defp maybe_update_poll(to_be_updated, updated_object) do
103     choice_key = fn
104       %{"anyOf" => [_ | _]} -> "anyOf"
105       %{"oneOf" => [_ | _]} -> "oneOf"
106       _ -> nil
107     end
108
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
116       to_be_updated
117       |> Map.put(key, updated_object[key])
118     else
119       # Choices (or vote type) have changed, do not allow this
120       _ -> to_be_updated
121     end
122   end
123
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} =
129       original_data
130       |> update_content_fields(new_data)
131
132     if not updated do
133       updated_data
134     else
135       %{updated_object: updated_data} =
136         updated_data
137         |> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
138
139       updated_data
140       |> Map.put("updated", date)
141     end
142   end
143
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
155         :update_everything
156       else
157         # only allow poll updates
158         {:cur_updated, _} -> :no_content_update
159         :eq -> :no_content_update
160         # allow all updates
161         {:last_updated, _} -> :update_everything
162         # allow no updates
163         _ -> false
164       end
165
166     %{
167       updated_object: updated_data,
168       used_history_in_new_object?: used_history_in_new_object?,
169       updated: updated
170     } =
171       if update_is_reasonable == :update_everything do
172         %{data: updated_data, updated: updated} =
173           original_data
174           |> update_content_fields(new_data)
175
176         updated_data
177         |> maybe_update_history(original_data,
178           updated: updated,
179           use_history_in_new_object?: true,
180           new_data: new_data
181         )
182         |> Map.put(:updated, updated)
183       else
184         %{
185           updated_object: original_data,
186           used_history_in_new_object?: false,
187           updated: false
188         }
189       end
190
191     updated_data =
192       if update_is_reasonable != false do
193         updated_data
194         |> maybe_update_poll(new_data)
195       else
196         updated_data
197       end
198
199     %{
200       updated_data: updated_data,
201       updated: updated,
202       used_history_in_new_object?: used_history_in_new_object?
203     }
204   end
205
206   def for_each_history_item(%{"orderedItems" => items} = history, _object, fun) do
207     new_items =
208       Enum.map(items, fun)
209       |> Enum.reduce_while(
210         {:ok, []},
211         fn
212           {:ok, item}, {:ok, acc} -> {:cont, {:ok, acc ++ [item]}}
213           e, _acc -> {:halt, e}
214         end
215       )
216
217     case new_items do
218       {:ok, items} -> {:ok, Map.put(history, "orderedItems", items)}
219       e -> e
220     end
221   end
222
223   def for_each_history_item(history, _, _) do
224     {:ok, history}
225   end
226
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
232       object =
233         if history do
234           Map.put(object, "formerRepresentations", history)
235         else
236           object
237         end
238
239       {:ok, object}
240     else
241       {:main_body, e} -> e
242       {:history_items, e} -> e
243     end
244   end
245
246   defp maybe_touch_changeset(changeset, true) do
247     updated_at =
248       NaiveDateTime.utc_now()
249       |> NaiveDateTime.truncate(:second)
250
251     Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
252   end
253
254   defp maybe_touch_changeset(changeset, _), do: changeset
255
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
259
260     %{
261       updated_data: updated_object_data,
262       updated: updated,
263       used_history_in_new_object?: used_history_in_new_object?
264     } = make_new_object_data_from_update_object(orig_object_data, updated_object)
265
266     changeset =
267       orig_object
268       |> Repo.preload(:hashtags)
269       |> Object.change(%{data: updated_object_data})
270       |> maybe_touch_changeset(touch_changeset?)
271
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
281           nil
282         else
283           _ -> nil
284         end
285       end
286
287       {:ok, new_object, updated}
288     end
289   end
290 end