SideEffects: port ones from ActivityPub.do_create and ActivityPub.insert
authorHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Thu, 18 Jun 2020 02:05:42 +0000 (04:05 +0200)
committerHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Wed, 15 Jul 2020 09:40:23 +0000 (11:40 +0200)
13 files changed:
lib/pleroma/object/containment.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/builder.ex
lib/pleroma/web/activity_pub/object_validator.ex
lib/pleroma/web/activity_pub/object_validators/answer_validator.ex
lib/pleroma/web/activity_pub/object_validators/common_validations.ex
lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex [moved from lib/pleroma/web/activity_pub/object_validators/create_question_validator.ex with 70% similarity]
lib/pleroma/web/activity_pub/object_validators/question_validator.ex
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/common_api/utils.ex
test/web/activity_pub/transmogrifier_test.exs

index 99608b8a5540c68e367158e89590bc0c123ffc1b..bc88e8a0ca777865dd25ecd162e6966eacf38871 100644 (file)
@@ -55,7 +55,7 @@ defmodule Pleroma.Object.Containment do
   defp compare_uris(_id_uri, _other_uri), do: :error
 
   @doc """
-  Checks that an imported AP object's actor matches the domain it came from.
+  Checks that an imported AP object's actor matches the host it came from.
   """
   def contain_origin(_id, %{"actor" => nil}), do: :error
 
index d8cc8d24f38f34692103261382ff2dddf9e605aa..9d13a06c439cbd1a8b196be65b7c90327bcb52d5 100644 (file)
@@ -66,7 +66,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp check_remote_limit(_), do: true
 
-  defp increase_note_count_if_public(actor, object) do
+  def increase_note_count_if_public(actor, object) do
     if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
   end
 
@@ -85,16 +85,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp increase_replies_count_if_reply(_create_data), do: :noop
 
-  defp increase_poll_votes_if_vote(%{
-         "object" => %{"inReplyTo" => reply_ap_id, "name" => name},
-         "type" => "Create",
-         "actor" => actor
-       }) do
-    Object.increase_vote_count(reply_ap_id, name, actor)
-  end
-
-  defp increase_poll_votes_if_vote(_create_data), do: :noop
-
   @object_types ["ChatMessage", "Question", "Answer"]
   @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
   def persist(%{"type" => type} = object, meta) when type in @object_types do
@@ -258,7 +248,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     with {:ok, activity} <- insert(create_data, local, fake),
          {:fake, false, activity} <- {:fake, fake, activity},
          _ <- increase_replies_count_if_reply(create_data),
-         _ <- increase_poll_votes_if_vote(create_data),
          {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
          {:ok, _actor} <- increase_note_count_if_public(actor, activity),
          _ <- notify_and_stream(activity),
index d5f3610ede258eedc2f64ec401c879a7ddf0f1d9..e973819546e0d10589273effa7b1ab437b0c6aac 100644 (file)
@@ -115,6 +115,21 @@ defmodule Pleroma.Web.ActivityPub.Builder do
     end
   end
 
+  def answer(user, object, name) do
+    {:ok,
+     %{
+       "type" => "Answer",
+       "actor" => user.ap_id,
+       "cc" => [object.data["actor"]],
+       "to" => [],
+       "name" => name,
+       "inReplyTo" => object.data["id"],
+       "context" => object.data["context"],
+       "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
+       "id" => Utils.generate_object_id()
+     }, []}
+  end
+
   @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
   def tombstone(actor, id) do
     {:ok,
index c89311187f17836178a151f65b0f10b0c71acecb..a24aaf00c60fd768a6afefcaf13826555c78ecc1 100644 (file)
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
   alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
-  alias Pleroma.Web.ActivityPub.ObjectValidators.CreateQuestionValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
@@ -162,7 +162,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
          meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
          {:ok, create_activity} <-
            create_activity
-           |> CreateQuestionValidator.cast_and_validate(meta)
+           |> CreateGenericValidator.cast_and_validate(meta)
            |> Ecto.Changeset.apply_action(:insert) do
       create_activity = stringify_keys(create_activity)
       {:ok, create_activity, meta}
@@ -188,7 +188,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
   end
 
   def cast_and_apply(%{"type" => "Answer"} = object) do
-    QuestionValidator.cast_and_apply(object)
+    AnswerValidator.cast_and_apply(object)
   end
 
   def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
index 0b51eccfa80131e1977acb34c3029d3d88fbe31b..8d4c925206711a108a3c9fd054c1a415d1e5ed65 100644 (file)
@@ -13,22 +13,25 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do
   @primary_key false
   @derive Jason.Encoder
 
-  # Extends from NoteValidator
   embedded_schema do
     field(:id, Types.ObjectID, primary_key: true)
     field(:to, {:array, :string}, default: [])
     field(:cc, {:array, :string}, default: [])
+
+    # is this actually needed?
     field(:bto, {:array, :string}, default: [])
     field(:bcc, {:array, :string}, default: [])
+
     field(:type, :string)
     field(:name, :string)
     field(:inReplyTo, :string)
     field(:attributedTo, Types.ObjectID)
+    field(:actor, Types.ObjectID)
   end
 
   def cast_and_apply(data) do
     data
-    |> cast_data
+    |> cast_data()
     |> apply_action(:insert)
   end
 
index e746b93607e9c7904102a2fc1e6b7bc1e67d320e..140555a45e6ab8706f76058eeaeddbd2f636dd86 100644 (file)
@@ -42,6 +42,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
     end)
   end
 
+  def validate_actor_is_active(cng, options \\ []) do
+    field_name = Keyword.get(options, :field_name, :actor)
+
+    cng
+    |> validate_change(field_name, fn field_name, actor ->
+      if %User{deactivated: false} = User.get_cached_by_ap_id(actor) do
+        []
+      else
+        [{field_name, "can't find user (or deactivated)"}]
+      end
+    end)
+  end
+
   def validate_object_presence(cng, options \\ []) do
     field_name = Keyword.get(options, :field_name, :object)
     allowed_types = Keyword.get(options, :allowed_types, false)
@@ -77,4 +90,29 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
 
     if actor_cng.valid?, do: actor_cng, else: object_cng
   end
+
+  def validate_host_match(cng, fields \\ [:id, :actor]) do
+    unique_hosts =
+      fields
+      |> Enum.map(fn field ->
+        %URI{host: host} =
+          cng
+          |> get_field(field)
+          |> URI.parse()
+
+        host
+      end)
+      |> Enum.uniq()
+      |> Enum.count()
+
+    if unique_hosts == 1 do
+      cng
+    else
+      fields
+      |> Enum.reduce(cng, fn field, cng ->
+        cng
+        |> add_error(field, "hosts of #{inspect(fields)} aren't matching")
+      end)
+    end
+  end
 end
similarity index 70%
rename from lib/pleroma/web/activity_pub/object_validators/create_question_validator.ex
rename to lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
index 6d3f71566db048a01fa6d10e8278a415c7efb3d7..4ad4ca0de5da4f07ee44521eda2ec45e497b963a 100644 (file)
@@ -4,9 +4,8 @@
 
 # Code based on CreateChatMessageValidator
 # NOTES
-# - Can probably be a generic create validator
 # - doesn't embed, will only get the object id
-defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateQuestionValidator do
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
   use Ecto.Schema
 
   alias Pleroma.Object
@@ -26,29 +25,53 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateQuestionValidator do
     field(:object, Types.ObjectID)
   end
 
+  def cast_data(data) do
+    %__MODULE__{}
+    |> changeset(data)
+  end
+
   def cast_and_apply(data) do
     data
     |> cast_data
     |> apply_action(:insert)
   end
 
-  def cast_data(data) do
-    cast(%__MODULE__{}, data, __schema__(:fields))
-  end
-
   def cast_and_validate(data, meta \\ []) do
-    cast_data(data)
+    data
+    |> cast_data
     |> validate_data(meta)
   end
 
+  def changeset(struct, data) do
+    struct
+    |> cast(data, __schema__(:fields))
+  end
+
   def validate_data(cng, meta \\ []) do
     cng
     |> validate_required([:actor, :type, :object])
     |> validate_inclusion(:type, ["Create"])
-    |> validate_actor_presence()
+    |> validate_actor_is_active()
     |> validate_any_presence([:to, :cc])
     |> validate_actors_match(meta)
     |> validate_object_nonexistence()
+    |> validate_object_containment()
+  end
+
+  def validate_object_containment(cng) do
+    actor = get_field(cng, :actor)
+
+    cng
+    |> validate_change(:object, fn :object, object_id ->
+      %URI{host: object_id_host} = URI.parse(object_id)
+      %URI{host: actor_host} = URI.parse(actor)
+
+      if object_id_host == actor_host do
+        []
+      else
+        [{:object, "The host of the object id doesn't match with the host of the actor"}]
+      end
+    end)
   end
 
   def validate_object_nonexistence(cng) do
index 211f520c45829bf0c9666b8e9aeddfe4a4932965..f7f3b135423e97a894b91231e50c9b05c35d9dd9 100644 (file)
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
   use Ecto.Schema
 
+  alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
   alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.Types
@@ -40,13 +41,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
     field(:announcement_count, :integer, default: 0)
     field(:inReplyTo, :string)
     field(:uri, Types.Uri)
+    # short identifier for PleromaFE to group statuses by context
+    field(:context_id, :integer)
 
     field(:likes, {:array, :string}, default: [])
     field(:announcements, {:array, :string}, default: [])
 
-    # see if needed
-    field(:context_id, :string)
-
     field(:closed, Types.DateTime)
     field(:voters, {:array, Types.ObjectID}, default: [])
     embeds_many(:anyOf, QuestionOptionsValidator)
@@ -70,7 +70,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
     |> changeset(data)
   end
 
-  def fix(data) do
+  defp fix_closed(data) do
     cond do
       is_binary(data["closed"]) -> data
       is_binary(data["endTime"]) -> Map.put(data, "closed", data["endTime"])
@@ -78,6 +78,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
     end
   end
 
+  # based on Pleroma.Web.ActivityPub.Utils.lazy_put_objects_defaults
+  defp fix_defaults(data) do
+    %{data: %{"id" => context}, id: context_id} = Utils.create_context(data["context"])
+
+    data
+    |> Map.put_new_lazy("id", &Utils.generate_object_id/0)
+    |> Map.put_new_lazy("published", &Utils.make_date/0)
+    |> Map.put_new("context", context)
+    |> Map.put_new("context_id", context_id)
+  end
+
+  defp fix(data) do
+    data
+    |> fix_closed()
+    |> fix_defaults()
+  end
+
   def changeset(struct, data) do
     data = fix(data)
 
@@ -92,7 +109,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
     |> validate_inclusion(:type, ["Question"])
     |> validate_required([:id, :actor, :type, :content, :context])
     |> CommonValidations.validate_any_presence([:cc, :to])
-    |> CommonValidations.validate_actor_presence()
+    |> CommonValidations.validate_actor_is_active()
     |> CommonValidations.validate_any_presence([:oneOf, :anyOf])
+    |> CommonValidations.validate_host_match()
   end
 end
index a78ec411fb5fb136d53bb59db410d4580742b66a..c17197becb9c4e73a2b0f3371d7a1597ba2c9875 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   """
   alias Pleroma.Activity
   alias Pleroma.Activity.Ir.Topics
+  alias Pleroma.ActivityExpiration
   alias Pleroma.Chat
   alias Pleroma.Chat.MessageReference
   alias Pleroma.FollowingRelationship
@@ -19,6 +20,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.Push
   alias Pleroma.Web.Streamer
+  alias Pleroma.Workers.BackgroundWorker
 
   def handle(object, meta \\ [])
 
@@ -135,10 +137,24 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   # Tasks this handles
   # - Actually create object
   # - Rollback if we couldn't create it
+  # - Increase the user note count
+  # - Increase the reply count
   # - Set up notifications
   def handle(%{data: %{"type" => "Create"}} = activity, meta) do
-    with {:ok, _object, meta} <- handle_object_creation(meta[:object_data], meta) do
+    with {:ok, object, meta} <- handle_object_creation(meta[:object_data], meta),
+         %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
       {:ok, notifications} = Notification.create_notifications(activity, do_send: false)
+      {:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
+
+      if in_reply_to = object.data["inReplyTo"] do
+        Object.increase_replies_count(in_reply_to)
+      end
+
+      if expires_at = activity.data["expires_at"] do
+        ActivityExpiration.create(activity, expires_at)
+      end
+
+      BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
 
       meta =
         meta
@@ -268,6 +284,18 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
     end
   end
 
+  def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do
+    with {:ok, object, meta} <- Pipeline.common_pipeline(object_map, meta) do
+      Object.increase_vote_count(
+        object.data["inReplyTo"],
+        object.data["name"],
+        object.data["actor"]
+      )
+
+      {:ok, object, meta}
+    end
+  end
+
   def handle_object_creation(%{"type" => "Question"} = object, meta) do
     with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
       {:ok, object, meta}
index 9900602e4aeb0eaf281a28b5a8da63a3b46cf943..26325d5de02c6793e94d273f09751427eb2cfd29 100644 (file)
@@ -419,6 +419,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     end)
   end
 
+  # Compatibility wrapper for Mastodon votes
+  defp handle_create(%{"object" => %{"type" => "Answer"}} = data, _user) do
+    handle_incoming(data)
+  end
+
+  defp handle_create(%{"object" => object} = data, user) do
+    %{
+      to: data["to"],
+      object: object,
+      actor: user,
+      context: object["context"],
+      local: false,
+      published: data["published"],
+      additional:
+        Map.take(data, [
+          "cc",
+          "directMessage",
+          "id"
+        ])
+    }
+    |> ActivityPub.create()
+  end
+
   def handle_incoming(data, options \\ [])
 
   # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
@@ -461,26 +484,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     actor = Containment.get_actor(data)
 
     with nil <- Activity.get_create_by_object_ap_id(object["id"]),
-         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor),
-         data <- Map.put(data, "actor", actor) |> fix_addressing() do
-      object = fix_object(object, options)
-
-      params = %{
-        to: data["to"],
-        object: object,
-        actor: user,
-        context: object["context"],
-        local: false,
-        published: data["published"],
-        additional:
-          Map.take(data, [
-            "cc",
-            "directMessage",
-            "id"
-          ])
-      }
+         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do
+      data =
+        data
+        |> Map.put("object", fix_object(object, options))
+        |> Map.put("actor", actor)
+        |> fix_addressing()
 
-      with {:ok, created_activity} <- ActivityPub.create(params) do
+      with {:ok, created_activity} <- handle_create(data, user) do
         reply_depth = (options[:depth] || 0) + 1
 
         if Federator.allowed_thread_distance?(reply_depth) do
index 692ceab1e05e5433dc0f11269bceb0fd9a81ae73..c08e0ffebe78df8dbcee4eb4c2eadd9338381d63 100644 (file)
@@ -308,18 +308,19 @@ defmodule Pleroma.Web.CommonAPI do
          {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
       answer_activities =
         Enum.map(choices, fn index ->
-          answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
-
-          {:ok, activity} =
-            ActivityPub.create(%{
-              to: answer_data["to"],
-              actor: user,
-              context: object.data["context"],
-              object: answer_data,
-              additional: %{"cc" => answer_data["cc"]}
-            })
-
-          activity
+          {:ok, answer_object, _meta} =
+            Builder.answer(user, object, Enum.at(options, index)["name"])
+
+          {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
+
+          {:ok, activity, _meta} =
+            activity_data
+            |> Map.put("cc", answer_object["cc"])
+            |> Map.put("context", answer_object["context"])
+            |> Pipeline.common_pipeline(local: true)
+
+          # TODO: Do preload of Pleroma.Object in Pipeline
+          Activity.normalize(activity.data)
         end)
 
       object = Object.get_cached_by_ap_id(object.data["id"])
index 9c38b73eb11bf2b28293dd6399911044c316b863..9d7b24eb295cfbc43f3d409e0746e26c047efaa8 100644 (file)
@@ -548,17 +548,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
     end
   end
 
-  def make_answer_data(%User{ap_id: ap_id}, object, name) do
-    %{
-      "type" => "Answer",
-      "actor" => ap_id,
-      "cc" => [object.data["actor"]],
-      "to" => [],
-      "name" => name,
-      "inReplyTo" => object.data["id"]
-    }
-  end
-
   def validate_character_limit("" = _full_payload, [] = _attachments) do
     {:error, dgettext("errors", "Cannot post an empty status without attachments")}
   end
index 4184b93cecd9be291a2a4941d25c75f927b6114f..62b5b06aacd69e1349e6c4e9f244dd523daba7d6 100644 (file)
@@ -282,7 +282,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert object.data["length"] == 180_000
     end
 
-    test "it rewrites Note votes to Answers and increments vote counters on question activities" do
+    test "it rewrites Note votes to Answer and increments vote counters on Question activities" do
       user = insert(:user)
 
       {:ok, activity} =