in dev, allow dev FE
[akkoma] / 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 def update_content_fields(orig_object_data, updated_object) do
9 Pleroma.Constants.status_updatable_fields()
10 |> Enum.reduce(
11 %{data: orig_object_data, updated: false},
12 fn field, %{data: data, updated: updated} ->
13 updated =
14 updated or
15 (field != "updated" and
16 Map.get(updated_object, field) != Map.get(orig_object_data, field))
17
18 data =
19 if Map.has_key?(updated_object, field) do
20 Map.put(data, field, updated_object[field])
21 else
22 Map.drop(data, [field])
23 end
24
25 %{data: data, updated: updated}
26 end
27 )
28 end
29
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
36 history
37 else
38 _ -> nil
39 end
40 end
41
42 def history_for(object) do
43 with history when not is_nil(history) <- maybe_history(object) do
44 history
45 else
46 _ -> history_skeleton()
47 end
48 end
49
50 defp history_skeleton do
51 %{
52 "type" => "OrderedCollection",
53 "totalItems" => 0,
54 "orderedItems" => []
55 }
56 end
57
58 def maybe_update_history(
59 updated_object,
60 orig_object_data,
61 opts
62 ) do
63 updated = opts[:updated]
64 use_history_in_new_object? = opts[:use_history_in_new_object?]
65
66 if not updated do
67 %{updated_object: updated_object, used_history_in_new_object?: false}
68 else
69 # Put edit history
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}
75 else
76 _ ->
77 history = history_for(orig_object_data)
78
79 latest_history_item =
80 orig_object_data
81 |> Map.drop(["id", "formerRepresentations"])
82
83 updated_history =
84 history
85 |> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
86 |> Map.put("totalItems", history["totalItems"] + 1)
87
88 {updated_history, false}
89 end
90
91 updated_object =
92 updated_object
93 |> Map.put("formerRepresentations", new_history)
94
95 %{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?}
96 end
97 end
98
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"
102 end
103
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
111 to_be_updated
112 |> Map.put(key, updated_object[key])
113 else
114 # Choices (or vote type) have changed, do not allow this
115 _ -> to_be_updated
116 end
117 end
118
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} =
124 original_data
125 |> update_content_fields(new_data)
126
127 if not updated do
128 updated_data
129 else
130 %{updated_object: updated_data} =
131 updated_data
132 |> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
133
134 updated_data
135 |> Map.put("updated", date)
136 end
137 end
138
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
150 :update_everything
151 else
152 # only allow poll updates
153 {:cur_updated, _} -> :no_content_update
154 :eq -> :no_content_update
155 # allow all updates
156 {:last_updated, _} -> :update_everything
157 # allow no updates
158 _ -> false
159 end
160
161 %{
162 updated_object: updated_data,
163 used_history_in_new_object?: used_history_in_new_object?,
164 updated: updated
165 } =
166 if update_is_reasonable == :update_everything do
167 %{data: updated_data, updated: updated} =
168 original_data
169 |> update_content_fields(new_data)
170
171 updated_data
172 |> maybe_update_history(original_data,
173 updated: updated,
174 use_history_in_new_object?: true,
175 new_data: new_data
176 )
177 |> Map.put(:updated, updated)
178 else
179 %{
180 updated_object: original_data,
181 used_history_in_new_object?: false,
182 updated: false
183 }
184 end
185
186 updated_data =
187 if update_is_reasonable != false do
188 updated_data
189 |> maybe_update_poll(new_data)
190 else
191 updated_data
192 end
193
194 %{
195 updated_data: updated_data,
196 updated: updated,
197 used_history_in_new_object?: used_history_in_new_object?
198 }
199 end
200
201 def for_each_history_item(%{"orderedItems" => items} = history, _object, fun) do
202 new_items =
203 Enum.map(items, fun)
204 |> Enum.reduce_while(
205 {:ok, []},
206 fn
207 {:ok, item}, {:ok, acc} -> {:cont, {:ok, acc ++ [item]}}
208 e, _acc -> {:halt, e}
209 end
210 )
211
212 case new_items do
213 {:ok, items} -> {:ok, Map.put(history, "orderedItems", items)}
214 e -> e
215 end
216 end
217
218 def for_each_history_item(history, _, _) do
219 {:ok, history}
220 end
221
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
227 object =
228 if history do
229 Map.put(object, "formerRepresentations", history)
230 else
231 object
232 end
233
234 {:ok, object}
235 else
236 {:main_body, e} -> e
237 {:history_items, e} -> e
238 end
239 end
240 end