logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

updater.ex (8942B)


  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. defmodule Pleroma.Object.Updater do
  5. require Pleroma.Constants
  6. alias Pleroma.Object
  7. alias Pleroma.Repo
  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. data =
  18. if Map.has_key?(updated_object, field) do
  19. Map.put(data, field, updated_object[field])
  20. else
  21. Map.drop(data, [field])
  22. end
  23. %{data: data, updated: updated}
  24. end
  25. )
  26. end
  27. def maybe_history(object) do
  28. with history <- Map.get(object, "formerRepresentations"),
  29. true <- is_map(history),
  30. "OrderedCollection" <- Map.get(history, "type"),
  31. true <- is_list(Map.get(history, "orderedItems")),
  32. true <- is_integer(Map.get(history, "totalItems")) do
  33. history
  34. else
  35. _ -> nil
  36. end
  37. end
  38. def history_for(object) do
  39. with history when not is_nil(history) <- maybe_history(object) do
  40. history
  41. else
  42. _ -> history_skeleton()
  43. end
  44. end
  45. defp history_skeleton do
  46. %{
  47. "type" => "OrderedCollection",
  48. "totalItems" => 0,
  49. "orderedItems" => []
  50. }
  51. end
  52. def maybe_update_history(
  53. updated_object,
  54. orig_object_data,
  55. opts
  56. ) do
  57. updated = opts[:updated]
  58. use_history_in_new_object? = opts[:use_history_in_new_object?]
  59. if not updated do
  60. %{updated_object: updated_object, used_history_in_new_object?: false}
  61. else
  62. # Put edit history
  63. # Note that we may have got the edit history by first fetching the object
  64. {new_history, used_history_in_new_object?} =
  65. with true <- use_history_in_new_object?,
  66. updated_history when not is_nil(updated_history) <- maybe_history(opts[:new_data]) do
  67. {updated_history, true}
  68. else
  69. _ ->
  70. history = history_for(orig_object_data)
  71. latest_history_item =
  72. orig_object_data
  73. |> Map.drop(["id", "formerRepresentations"])
  74. updated_history =
  75. history
  76. |> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
  77. |> Map.put("totalItems", history["totalItems"] + 1)
  78. {updated_history, false}
  79. end
  80. updated_object =
  81. updated_object
  82. |> Map.put("formerRepresentations", new_history)
  83. %{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?}
  84. end
  85. end
  86. defp maybe_update_poll(to_be_updated, updated_object) do
  87. choice_key = fn
  88. %{"anyOf" => [_ | _]} -> "anyOf"
  89. %{"oneOf" => [_ | _]} -> "oneOf"
  90. _ -> nil
  91. end
  92. with true <- to_be_updated["type"] == "Question",
  93. key when not is_nil(key) <- choice_key.(updated_object),
  94. true <- key == choice_key.(to_be_updated),
  95. orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])),
  96. new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])),
  97. true <- orig_choices == new_choices do
  98. # Choices are the same, but counts are different
  99. to_be_updated
  100. |> Map.put(key, updated_object[key])
  101. else
  102. # Choices (or vote type) have changed, do not allow this
  103. _ -> to_be_updated
  104. end
  105. end
  106. # This calculates the data to be sent as the object of an Update.
  107. # new_data's formerRepresentations is not considered.
  108. # formerRepresentations is added to the returned data.
  109. def make_update_object_data(original_data, new_data, date) do
  110. %{data: updated_data, updated: updated} =
  111. original_data
  112. |> update_content_fields(new_data)
  113. if not updated do
  114. updated_data
  115. else
  116. %{updated_object: updated_data} =
  117. updated_data
  118. |> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
  119. updated_data
  120. |> Map.put("updated", date)
  121. end
  122. end
  123. # This calculates the data of the new Object from an Update.
  124. # new_data's formerRepresentations is considered.
  125. def make_new_object_data_from_update_object(original_data, new_data) do
  126. update_is_reasonable =
  127. with {_, updated} when not is_nil(updated) <- {:cur_updated, new_data["updated"]},
  128. {_, {:ok, updated_time, _}} <- {:cur_updated, DateTime.from_iso8601(updated)},
  129. {_, last_updated} when not is_nil(last_updated) <-
  130. {:last_updated, original_data["updated"] || original_data["published"]},
  131. {_, {:ok, last_updated_time, _}} <-
  132. {:last_updated, DateTime.from_iso8601(last_updated)},
  133. :gt <- DateTime.compare(updated_time, last_updated_time) do
  134. :update_everything
  135. else
  136. # only allow poll updates
  137. {:cur_updated, _} -> :no_content_update
  138. :eq -> :no_content_update
  139. # allow all updates
  140. {:last_updated, _} -> :update_everything
  141. # allow no updates
  142. _ -> false
  143. end
  144. %{
  145. updated_object: updated_data,
  146. used_history_in_new_object?: used_history_in_new_object?,
  147. updated: updated
  148. } =
  149. if update_is_reasonable == :update_everything do
  150. %{data: updated_data, updated: updated} =
  151. original_data
  152. |> update_content_fields(new_data)
  153. updated_data
  154. |> maybe_update_history(original_data,
  155. updated: updated,
  156. use_history_in_new_object?: true,
  157. new_data: new_data
  158. )
  159. |> Map.put(:updated, updated)
  160. else
  161. %{
  162. updated_object: original_data,
  163. used_history_in_new_object?: false,
  164. updated: false
  165. }
  166. end
  167. updated_data =
  168. if update_is_reasonable != false do
  169. updated_data
  170. |> maybe_update_poll(new_data)
  171. else
  172. updated_data
  173. end
  174. %{
  175. updated_data: updated_data,
  176. updated: updated,
  177. used_history_in_new_object?: used_history_in_new_object?
  178. }
  179. end
  180. def for_each_history_item(%{"orderedItems" => items} = history, _object, fun) do
  181. new_items =
  182. Enum.map(items, fun)
  183. |> Enum.reduce_while(
  184. {:ok, []},
  185. fn
  186. {:ok, item}, {:ok, acc} -> {:cont, {:ok, acc ++ [item]}}
  187. e, _acc -> {:halt, e}
  188. end
  189. )
  190. case new_items do
  191. {:ok, items} -> {:ok, Map.put(history, "orderedItems", items)}
  192. e -> e
  193. end
  194. end
  195. def for_each_history_item(history, _, _) do
  196. {:ok, history}
  197. end
  198. def do_with_history(object, fun) do
  199. with history <- object["formerRepresentations"],
  200. object <- Map.drop(object, ["formerRepresentations"]),
  201. {_, {:ok, object}} <- {:main_body, fun.(object)},
  202. {_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do
  203. object =
  204. if history do
  205. Map.put(object, "formerRepresentations", history)
  206. else
  207. object
  208. end
  209. {:ok, object}
  210. else
  211. {:main_body, e} -> e
  212. {:history_items, e} -> e
  213. end
  214. end
  215. defp maybe_touch_changeset(changeset, true) do
  216. updated_at =
  217. NaiveDateTime.utc_now()
  218. |> NaiveDateTime.truncate(:second)
  219. Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
  220. end
  221. defp maybe_touch_changeset(changeset, _), do: changeset
  222. def do_update_and_invalidate_cache(orig_object, updated_object, touch_changeset? \\ false) do
  223. orig_object_ap_id = updated_object["id"]
  224. orig_object_data = orig_object.data
  225. %{
  226. updated_data: updated_object_data,
  227. updated: updated,
  228. used_history_in_new_object?: used_history_in_new_object?
  229. } = make_new_object_data_from_update_object(orig_object_data, updated_object)
  230. changeset =
  231. orig_object
  232. |> Repo.preload(:hashtags)
  233. |> Object.change(%{data: updated_object_data})
  234. |> maybe_touch_changeset(touch_changeset?)
  235. with {:ok, new_object} <- Repo.update(changeset),
  236. {:ok, _} <- Object.invalid_object_cache(new_object),
  237. {:ok, _} <- Object.set_cache(new_object),
  238. # The metadata/utils.ex uses the object id for the cache.
  239. {:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(new_object.id) do
  240. if used_history_in_new_object? do
  241. with create_activity when not is_nil(create_activity) <-
  242. Pleroma.Activity.get_create_by_object_ap_id(orig_object_ap_id),
  243. {:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(create_activity.id) do
  244. nil
  245. else
  246. _ -> nil
  247. end
  248. end
  249. {:ok, new_object, updated}
  250. end
  251. end
  252. end