Rebased from #103 Co-authored-by: Tusooa Zhu <tusooa@kazv.moe> Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk> Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/202
		
			
				
	
	
		
			240 lines
		
	
	
	
		
			7.1 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
			
		
		
	
	
			240 lines
		
	
	
	
		
			7.1 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
# Pleroma: A lightweight social networking server
 | 
						|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
 | 
						|
# SPDX-License-Identifier: AGPL-3.0-only
 | 
						|
 | 
						|
defmodule Pleroma.Object.Updater do
 | 
						|
  require Pleroma.Constants
 | 
						|
 | 
						|
  def update_content_fields(orig_object_data, updated_object) do
 | 
						|
    Pleroma.Constants.status_updatable_fields()
 | 
						|
    |> Enum.reduce(
 | 
						|
      %{data: orig_object_data, updated: false},
 | 
						|
      fn field, %{data: data, updated: updated} ->
 | 
						|
        updated =
 | 
						|
          updated or
 | 
						|
            (field != "updated" and
 | 
						|
               Map.get(updated_object, field) != Map.get(orig_object_data, field))
 | 
						|
 | 
						|
        data =
 | 
						|
          if Map.has_key?(updated_object, field) do
 | 
						|
            Map.put(data, field, updated_object[field])
 | 
						|
          else
 | 
						|
            Map.drop(data, [field])
 | 
						|
          end
 | 
						|
 | 
						|
        %{data: data, updated: updated}
 | 
						|
      end
 | 
						|
    )
 | 
						|
  end
 | 
						|
 | 
						|
  def maybe_history(object) do
 | 
						|
    with history <- Map.get(object, "formerRepresentations"),
 | 
						|
         true <- is_map(history),
 | 
						|
         "OrderedCollection" <- Map.get(history, "type"),
 | 
						|
         true <- is_list(Map.get(history, "orderedItems")),
 | 
						|
         true <- is_integer(Map.get(history, "totalItems")) do
 | 
						|
      history
 | 
						|
    else
 | 
						|
      _ -> nil
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  def history_for(object) do
 | 
						|
    with history when not is_nil(history) <- maybe_history(object) do
 | 
						|
      history
 | 
						|
    else
 | 
						|
      _ -> history_skeleton()
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp history_skeleton do
 | 
						|
    %{
 | 
						|
      "type" => "OrderedCollection",
 | 
						|
      "totalItems" => 0,
 | 
						|
      "orderedItems" => []
 | 
						|
    }
 | 
						|
  end
 | 
						|
 | 
						|
  def maybe_update_history(
 | 
						|
        updated_object,
 | 
						|
        orig_object_data,
 | 
						|
        opts
 | 
						|
      ) do
 | 
						|
    updated = opts[:updated]
 | 
						|
    use_history_in_new_object? = opts[:use_history_in_new_object?]
 | 
						|
 | 
						|
    if not updated do
 | 
						|
      %{updated_object: updated_object, used_history_in_new_object?: false}
 | 
						|
    else
 | 
						|
      # Put edit history
 | 
						|
      # Note that we may have got the edit history by first fetching the object
 | 
						|
      {new_history, used_history_in_new_object?} =
 | 
						|
        with true <- use_history_in_new_object?,
 | 
						|
             updated_history when not is_nil(updated_history) <- maybe_history(opts[:new_data]) do
 | 
						|
          {updated_history, true}
 | 
						|
        else
 | 
						|
          _ ->
 | 
						|
            history = history_for(orig_object_data)
 | 
						|
 | 
						|
            latest_history_item =
 | 
						|
              orig_object_data
 | 
						|
              |> Map.drop(["id", "formerRepresentations"])
 | 
						|
 | 
						|
            updated_history =
 | 
						|
              history
 | 
						|
              |> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
 | 
						|
              |> Map.put("totalItems", history["totalItems"] + 1)
 | 
						|
 | 
						|
            {updated_history, false}
 | 
						|
        end
 | 
						|
 | 
						|
      updated_object =
 | 
						|
        updated_object
 | 
						|
        |> Map.put("formerRepresentations", new_history)
 | 
						|
 | 
						|
      %{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?}
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp maybe_update_poll(to_be_updated, updated_object) do
 | 
						|
    choice_key = fn data ->
 | 
						|
      if Map.has_key?(data, "anyOf"), do: "anyOf", else: "oneOf"
 | 
						|
    end
 | 
						|
 | 
						|
    with true <- to_be_updated["type"] == "Question",
 | 
						|
         key <- choice_key.(updated_object),
 | 
						|
         true <- key == choice_key.(to_be_updated),
 | 
						|
         orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])),
 | 
						|
         new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])),
 | 
						|
         true <- orig_choices == new_choices do
 | 
						|
      # Choices are the same, but counts are different
 | 
						|
      to_be_updated
 | 
						|
      |> Map.put(key, updated_object[key])
 | 
						|
    else
 | 
						|
      # Choices (or vote type) have changed, do not allow this
 | 
						|
      _ -> to_be_updated
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  # This calculates the data to be sent as the object of an Update.
 | 
						|
  # new_data's formerRepresentations is not considered.
 | 
						|
  # formerRepresentations is added to the returned data.
 | 
						|
  def make_update_object_data(original_data, new_data, date) do
 | 
						|
    %{data: updated_data, updated: updated} =
 | 
						|
      original_data
 | 
						|
      |> update_content_fields(new_data)
 | 
						|
 | 
						|
    if not updated do
 | 
						|
      updated_data
 | 
						|
    else
 | 
						|
      %{updated_object: updated_data} =
 | 
						|
        updated_data
 | 
						|
        |> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
 | 
						|
 | 
						|
      updated_data
 | 
						|
      |> Map.put("updated", date)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  # This calculates the data of the new Object from an Update.
 | 
						|
  # new_data's formerRepresentations is considered.
 | 
						|
  def make_new_object_data_from_update_object(original_data, new_data) do
 | 
						|
    update_is_reasonable =
 | 
						|
      with {_, updated} when not is_nil(updated) <- {:cur_updated, new_data["updated"]},
 | 
						|
           {_, {:ok, updated_time, _}} <- {:cur_updated, DateTime.from_iso8601(updated)},
 | 
						|
           {_, last_updated} when not is_nil(last_updated) <-
 | 
						|
             {:last_updated, original_data["updated"] || original_data["published"]},
 | 
						|
           {_, {:ok, last_updated_time, _}} <-
 | 
						|
             {:last_updated, DateTime.from_iso8601(last_updated)},
 | 
						|
           :gt <- DateTime.compare(updated_time, last_updated_time) do
 | 
						|
        :update_everything
 | 
						|
      else
 | 
						|
        # only allow poll updates
 | 
						|
        {:cur_updated, _} -> :no_content_update
 | 
						|
        :eq -> :no_content_update
 | 
						|
        # allow all updates
 | 
						|
        {:last_updated, _} -> :update_everything
 | 
						|
        # allow no updates
 | 
						|
        _ -> false
 | 
						|
      end
 | 
						|
 | 
						|
    %{
 | 
						|
      updated_object: updated_data,
 | 
						|
      used_history_in_new_object?: used_history_in_new_object?,
 | 
						|
      updated: updated
 | 
						|
    } =
 | 
						|
      if update_is_reasonable == :update_everything do
 | 
						|
        %{data: updated_data, updated: updated} =
 | 
						|
          original_data
 | 
						|
          |> update_content_fields(new_data)
 | 
						|
 | 
						|
        updated_data
 | 
						|
        |> maybe_update_history(original_data,
 | 
						|
          updated: updated,
 | 
						|
          use_history_in_new_object?: true,
 | 
						|
          new_data: new_data
 | 
						|
        )
 | 
						|
        |> Map.put(:updated, updated)
 | 
						|
      else
 | 
						|
        %{
 | 
						|
          updated_object: original_data,
 | 
						|
          used_history_in_new_object?: false,
 | 
						|
          updated: false
 | 
						|
        }
 | 
						|
      end
 | 
						|
 | 
						|
    updated_data =
 | 
						|
      if update_is_reasonable != false do
 | 
						|
        updated_data
 | 
						|
        |> maybe_update_poll(new_data)
 | 
						|
      else
 | 
						|
        updated_data
 | 
						|
      end
 | 
						|
 | 
						|
    %{
 | 
						|
      updated_data: updated_data,
 | 
						|
      updated: updated,
 | 
						|
      used_history_in_new_object?: used_history_in_new_object?
 | 
						|
    }
 | 
						|
  end
 | 
						|
 | 
						|
  def for_each_history_item(%{"orderedItems" => items} = history, _object, fun) do
 | 
						|
    new_items =
 | 
						|
      Enum.map(items, fun)
 | 
						|
      |> Enum.reduce_while(
 | 
						|
        {:ok, []},
 | 
						|
        fn
 | 
						|
          {:ok, item}, {:ok, acc} -> {:cont, {:ok, acc ++ [item]}}
 | 
						|
          e, _acc -> {:halt, e}
 | 
						|
        end
 | 
						|
      )
 | 
						|
 | 
						|
    case new_items do
 | 
						|
      {:ok, items} -> {:ok, Map.put(history, "orderedItems", items)}
 | 
						|
      e -> e
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  def for_each_history_item(history, _, _) do
 | 
						|
    {:ok, history}
 | 
						|
  end
 | 
						|
 | 
						|
  def do_with_history(object, fun) do
 | 
						|
    with history <- object["formerRepresentations"],
 | 
						|
         object <- Map.drop(object, ["formerRepresentations"]),
 | 
						|
         {_, {:ok, object}} <- {:main_body, fun.(object)},
 | 
						|
         {_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do
 | 
						|
      object =
 | 
						|
        if history do
 | 
						|
          Map.put(object, "formerRepresentations", history)
 | 
						|
        else
 | 
						|
          object
 | 
						|
        end
 | 
						|
 | 
						|
      {:ok, object}
 | 
						|
    else
 | 
						|
      {:main_body, e} -> e
 | 
						|
      {:history_items, e} -> e
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |