Merge remote-tracking branch 'pleroma/develop' into features/poll-validation
authorHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Fri, 31 Jul 2020 11:57:21 +0000 (13:57 +0200)
committerHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Fri, 31 Jul 2020 11:57:21 +0000 (13:57 +0200)
26 files changed:
lib/pleroma/object.ex
lib/pleroma/object/containment.ex
lib/pleroma/object/fetcher.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 [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/common_validations.ex
lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/note_validator.ex
lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/question_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/url_object_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
lib/pleroma/web/mastodon_api/views/poll_view.ex
test/fixtures/mastodon-question-activity.json
test/fixtures/tesla_mock/poll_attachment.json [new file with mode: 0644]
test/object/fetcher_test.exs
test/support/http_request_mock.ex
test/web/activity_pub/transmogrifier/answer_handling_test.exs [new file with mode: 0644]
test/web/activity_pub/transmogrifier/question_handling_test.exs [new file with mode: 0644]
test/web/activity_pub/transmogrifier_test.exs
test/web/mastodon_api/views/poll_view_test.exs

index 546c4ea01693bc4fb4f0d461b22de60622a3f5b2..4dd929cfd61c6a78a3578073098fa519194e36f6 100644 (file)
@@ -255,6 +255,12 @@ defmodule Pleroma.Object do
     end
   end
 
+  defp poll_is_multiple?(%Object{data: %{"anyOf" => anyOf}}) do
+    !Enum.empty?(anyOf)
+  end
+
+  defp poll_is_multiple?(_), do: false
+
   def decrease_replies_count(ap_id) do
     Object
     |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
@@ -281,10 +287,10 @@ defmodule Pleroma.Object do
   def increase_vote_count(ap_id, name, actor) do
     with %Object{} = object <- Object.normalize(ap_id),
          "Question" <- object.data["type"] do
-      multiple = Map.has_key?(object.data, "anyOf")
+      key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf"
 
       options =
-        (object.data["anyOf"] || object.data["oneOf"] || [])
+        object.data[key]
         |> Enum.map(fn
           %{"name" => ^name} = option ->
             Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
@@ -296,11 +302,8 @@ defmodule Pleroma.Object do
       voters = [actor | object.data["voters"] || []] |> Enum.uniq()
 
       data =
-        if multiple do
-          Map.put(object.data, "anyOf", options)
-        else
-          Map.put(object.data, "oneOf", options)
-        end
+        object.data
+        |> Map.put(key, options)
         |> Map.put("voters", voters)
 
       object
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 e74c87269f2d5b5bc47e039818dae47a3c93b5c8..3ff25118d5160c82b3f4922b29593f8f8eca5f28 100644 (file)
@@ -9,6 +9,7 @@ defmodule Pleroma.Object.Fetcher do
   alias Pleroma.Repo
   alias Pleroma.Signature
   alias Pleroma.Web.ActivityPub.InternalFetchActor
+  alias Pleroma.Web.ActivityPub.ObjectValidator
   alias Pleroma.Web.ActivityPub.Transmogrifier
   alias Pleroma.Web.Federator
 
@@ -23,21 +24,39 @@ defmodule Pleroma.Object.Fetcher do
     Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
   end
 
-  defp maybe_reinject_internal_fields(data, %{data: %{} = old_data}) do
+  defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
     internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
 
-    Map.merge(data, internal_fields)
+    Map.merge(new_data, internal_fields)
   end
 
-  defp maybe_reinject_internal_fields(data, _), do: data
+  defp maybe_reinject_internal_fields(_, new_data), do: new_data
 
   @spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
-  defp reinject_object(struct, data) do
-    Logger.debug("Reinjecting object #{data["id"]}")
+  defp reinject_object(%Object{data: %{"type" => "Question"}} = object, new_data) do
+    Logger.debug("Reinjecting object #{new_data["id"]}")
 
-    with data <- Transmogrifier.fix_object(data),
-         data <- maybe_reinject_internal_fields(data, struct),
-         changeset <- Object.change(struct, %{data: data}),
+    with new_data <- Transmogrifier.fix_object(new_data),
+         data <- maybe_reinject_internal_fields(object, new_data),
+         {:ok, data, _} <- ObjectValidator.validate(data, %{}),
+         changeset <- Object.change(object, %{data: data}),
+         changeset <- touch_changeset(changeset),
+         {:ok, object} <- Repo.insert_or_update(changeset),
+         {:ok, object} <- Object.set_cache(object) do
+      {:ok, object}
+    else
+      e ->
+        Logger.error("Error while processing object: #{inspect(e)}")
+        {:error, e}
+    end
+  end
+
+  defp reinject_object(%Object{} = object, new_data) do
+    Logger.debug("Reinjecting object #{new_data["id"]}")
+
+    with new_data <- Transmogrifier.fix_object(new_data),
+         data <- maybe_reinject_internal_fields(object, new_data),
+         changeset <- Object.change(object, %{data: data}),
          changeset <- touch_changeset(changeset),
          {:ok, object} <- Repo.insert_or_update(changeset),
          {:ok, object} <- Object.set_cache(object) do
@@ -51,8 +70,8 @@ defmodule Pleroma.Object.Fetcher do
 
   def refetch_object(%Object{data: %{"id" => id}} = object) do
     with {:local, false} <- {:local, Object.local?(object)},
-         {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
-         {:ok, object} <- reinject_object(object, data) do
+         {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
+         {:ok, object} <- reinject_object(object, new_data) do
       {:ok, object}
     else
       {:local, true} -> {:ok, object}
index a4db1d87c42334432396e10ccc2cf6d41176a363..fe62673dce2515fa9f646dd1a7d8d5a91c2e61f4 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,17 +85,7 @@ 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"]
+  @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
     with {:ok, object} <- Object.create(object) 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..1b4c421b880a38160062e8d4b13367c20e5be6b3 100644 (file)
@@ -80,6 +80,13 @@ defmodule Pleroma.Web.ActivityPub.Builder do
   end
 
   def create(actor, object, recipients) do
+    context =
+      if is_map(object) do
+        object["context"]
+      else
+        nil
+      end
+
     {:ok,
      %{
        "id" => Utils.generate_activity_id(),
@@ -88,7 +95,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
        "object" => object,
        "type" => "Create",
        "published" => DateTime.utc_now() |> DateTime.to_iso8601()
-     }, []}
+     }
+     |> Pleroma.Maps.put_if_present("context", context), []}
   end
 
   def chat_message(actor, recipient, content, opts \\ []) do
@@ -115,6 +123,22 @@ defmodule Pleroma.Web.ActivityPub.Builder do
     end
   end
 
+  def answer(user, object, name) do
+    {:ok,
+     %{
+       "type" => "Answer",
+       "actor" => user.ap_id,
+       "attributedTo" => 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 0dcc7be4dbd778a1eebace51c8848e4f86cb76cd..e1114a44d952f30d23d46d9273d4a57194e03012 100644 (file)
@@ -14,13 +14,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
   alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
 
@@ -112,17 +115,40 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
     end
   end
 
+  def validate(%{"type" => "Question"} = object, meta) do
+    with {:ok, object} <-
+           object
+           |> QuestionValidator.cast_and_validate()
+           |> Ecto.Changeset.apply_action(:insert) do
+      object = stringify_keys(object)
+      {:ok, object, meta}
+    end
+  end
+
+  def validate(%{"type" => "Answer"} = object, meta) do
+    with {:ok, object} <-
+           object
+           |> AnswerValidator.cast_and_validate()
+           |> Ecto.Changeset.apply_action(:insert) do
+      object = stringify_keys(object)
+      {:ok, object, meta}
+    end
+  end
+
   def validate(%{"type" => "EmojiReact"} = object, meta) do
     with {:ok, object} <-
            object
            |> EmojiReactValidator.cast_and_validate()
            |> Ecto.Changeset.apply_action(:insert) do
-      object = stringify_keys(object |> Map.from_struct())
+      object = stringify_keys(object)
       {:ok, object, meta}
     end
   end
 
-  def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
+  def validate(
+        %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object} = create_activity,
+        meta
+      ) do
     with {:ok, object_data} <- cast_and_apply(object),
          meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
          {:ok, create_activity} <-
@@ -134,12 +160,28 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
     end
   end
 
+  def validate(
+        %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
+        meta
+      )
+      when objtype in ["Question", "Answer"] do
+    with {:ok, object_data} <- cast_and_apply(object),
+         meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
+         {:ok, create_activity} <-
+           create_activity
+           |> CreateGenericValidator.cast_and_validate(meta)
+           |> Ecto.Changeset.apply_action(:insert) do
+      create_activity = stringify_keys(create_activity)
+      {:ok, create_activity, meta}
+    end
+  end
+
   def validate(%{"type" => "Announce"} = object, meta) do
     with {:ok, object} <-
            object
            |> AnnounceValidator.cast_and_validate()
            |> Ecto.Changeset.apply_action(:insert) do
-      object = stringify_keys(object |> Map.from_struct())
+      object = stringify_keys(object)
       {:ok, object, meta}
     end
   end
@@ -148,8 +190,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
     ChatMessageValidator.cast_and_apply(object)
   end
 
+  def cast_and_apply(%{"type" => "Question"} = object) do
+    QuestionValidator.cast_and_apply(object)
+  end
+
+  def cast_and_apply(%{"type" => "Answer"} = object) do
+    AnswerValidator.cast_and_apply(object)
+  end
+
   def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
 
+  # is_struct/1 isn't present in Elixir 1.8.x
   def stringify_keys(%{__struct__: _} = object) do
     object
     |> Map.from_struct()
diff --git a/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex b/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex
new file mode 100644 (file)
index 0000000..ebddd50
--- /dev/null
@@ -0,0 +1,65 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do
+  use Ecto.Schema
+
+  alias Pleroma.EctoType.ActivityPub.ObjectValidators
+  alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+  import Ecto.Changeset
+
+  @primary_key false
+  @derive Jason.Encoder
+
+  embedded_schema do
+    field(:id, ObjectValidators.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, ObjectValidators.ObjectID)
+
+    # TODO: Remove actor on objects
+    field(:actor, ObjectValidators.ObjectID)
+  end
+
+  def cast_and_apply(data) do
+    data
+    |> cast_data()
+    |> apply_action(:insert)
+  end
+
+  def cast_and_validate(data) do
+    data
+    |> cast_data()
+    |> validate_data()
+  end
+
+  def cast_data(data) do
+    %__MODULE__{}
+    |> changeset(data)
+  end
+
+  def changeset(struct, data) do
+    struct
+    |> cast(data, __schema__(:fields))
+  end
+
+  def validate_data(data_cng) do
+    data_cng
+    |> validate_inclusion(:type, ["Answer"])
+    |> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor])
+    |> CommonValidations.validate_any_presence([:cc, :to])
+    |> CommonValidations.validate_fields_match([:actor, :attributedTo])
+    |> CommonValidations.validate_actor_is_active()
+    |> CommonValidations.validate_host_match()
+  end
+end
index aeef31945dab440ec462952a35cac3be8126bbe6..e981dacaa8e166585f1b095dce992d3eae3e7e95 100644 (file)
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
   alias Pleroma.Object
   alias Pleroma.User
 
-  def validate_recipients_presence(cng, fields \\ [:to, :cc]) do
+  def validate_any_presence(cng, fields) do
     non_empty =
       fields
       |> Enum.map(fn field -> get_field(cng, field) end)
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
       fields
       |> Enum.reduce(cng, fn field, cng ->
         cng
-        |> add_error(field, "no recipients in any field")
+        |> add_error(field, "none of #{inspect(fields)} present")
       end)
     end
   end
@@ -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,47 @@ 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
+
+  def validate_fields_match(cng, fields) do
+    unique_fields =
+      fields
+      |> Enum.map(fn field -> get_field(cng, field) end)
+      |> Enum.uniq()
+      |> Enum.count()
+
+    if unique_fields == 1 do
+      cng
+    else
+      fields
+      |> Enum.reduce(cng, fn field, cng ->
+        cng
+        |> add_error(field, "Fields #{inspect(fields)} aren't matching")
+      end)
+    end
+  end
 end
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
new file mode 100644 (file)
index 0000000..ff88933
--- /dev/null
@@ -0,0 +1,118 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+# Code based on CreateChatMessageValidator
+# NOTES
+# - doesn't embed, will only get the object id
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
+  use Ecto.Schema
+
+  alias Pleroma.EctoType.ActivityPub.ObjectValidators
+  alias Pleroma.Object
+
+  import Ecto.Changeset
+  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+  @primary_key false
+
+  embedded_schema do
+    field(:id, ObjectValidators.ObjectID, primary_key: true)
+    field(:actor, ObjectValidators.ObjectID)
+    field(:type, :string)
+    field(:to, ObjectValidators.Recipients, default: [])
+    field(:cc, ObjectValidators.Recipients, default: [])
+    field(:object, ObjectValidators.ObjectID)
+    field(:expires_at, ObjectValidators.DateTime)
+
+    # Should be moved to object, done for CommonAPI.Utils.make_context
+    field(:context, :string)
+  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_and_validate(data, meta \\ []) do
+    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_is_active()
+    |> validate_any_presence([:to, :cc])
+    |> validate_actors_match(meta)
+    |> validate_context_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
+    cng
+    |> validate_change(:object, fn :object, object_id ->
+      if Object.get_cached_by_ap_id(object_id) do
+        [{:object, "The object to create already exists"}]
+      else
+        []
+      end
+    end)
+  end
+
+  def validate_actors_match(cng, meta) do
+    attributed_to = meta[:object_data]["attributedTo"] || meta[:object_data]["actor"]
+
+    cng
+    |> validate_change(:actor, fn :actor, actor ->
+      if actor == attributed_to do
+        []
+      else
+        [{:actor, "Actor doesn't match with object attributedTo"}]
+      end
+    end)
+  end
+
+  def validate_context_match(cng, %{object_data: %{"context" => object_context}}) do
+    cng
+    |> validate_change(:context, fn :context, context ->
+      if context == object_context do
+        []
+      else
+        [{:context, "context field not matching between Create and object (#{object_context})"}]
+      end
+    end)
+  end
+
+  def validate_context_match(cng, _), do: cng
+end
index 56b93dde8298cc037022af5bdfd92c2368eb080d..a65fe23549ab2aec3330c859eb5a3618315f32a3 100644 (file)
@@ -34,7 +34,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
     field(:replies_count, :integer, default: 0)
     field(:like_count, :integer, default: 0)
     field(:announcement_count, :integer, default: 0)
-    field(:inRepyTo, :string)
+    field(:inReplyTo, :string)
     field(:uri, ObjectValidators.Uri)
 
     field(:likes, {:array, :string}, default: [])
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex
new file mode 100644 (file)
index 0000000..478b3b5
--- /dev/null
@@ -0,0 +1,37 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator do
+  use Ecto.Schema
+
+  import Ecto.Changeset
+
+  @primary_key false
+
+  embedded_schema do
+    field(:name, :string)
+
+    embeds_one :replies, Replies, primary_key: false do
+      field(:totalItems, :integer)
+      field(:type, :string)
+    end
+
+    field(:type, :string)
+  end
+
+  def changeset(struct, data) do
+    struct
+    |> cast(data, [:name, :type])
+    |> cast_embed(:replies, with: &replies_changeset/2)
+    |> validate_inclusion(:type, ["Note"])
+    |> validate_required([:name, :type])
+  end
+
+  def replies_changeset(struct, data) do
+    struct
+    |> cast(data, [:totalItems, :type])
+    |> validate_inclusion(:type, ["Collection"])
+    |> validate_required([:type])
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
new file mode 100644 (file)
index 0000000..466b3e6
--- /dev/null
@@ -0,0 +1,121 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
+  use Ecto.Schema
+
+  alias Pleroma.EctoType.ActivityPub.ObjectValidators
+  alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+  alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator
+  alias Pleroma.Web.ActivityPub.Utils
+
+  import Ecto.Changeset
+
+  @primary_key false
+  @derive Jason.Encoder
+
+  # Extends from NoteValidator
+  embedded_schema do
+    field(:id, ObjectValidators.ObjectID, primary_key: true)
+    field(:to, {:array, :string}, default: [])
+    field(:cc, {:array, :string}, default: [])
+    field(:bto, {:array, :string}, default: [])
+    field(:bcc, {:array, :string}, default: [])
+    # TODO: Write type
+    field(:tag, {:array, :map}, default: [])
+    field(:type, :string)
+    field(:content, :string)
+    field(:context, :string)
+
+    # TODO: Remove actor on objects
+    field(:actor, ObjectValidators.ObjectID)
+
+    field(:attributedTo, ObjectValidators.ObjectID)
+    field(:summary, :string)
+    field(:published, ObjectValidators.DateTime)
+    # TODO: Write type
+    field(:emoji, :map, default: %{})
+    field(:sensitive, :boolean, default: false)
+    embeds_many(:attachment, AttachmentValidator)
+    field(:replies_count, :integer, default: 0)
+    field(:like_count, :integer, default: 0)
+    field(:announcement_count, :integer, default: 0)
+    field(:inReplyTo, :string)
+    field(:uri, ObjectValidators.Uri)
+    # short identifier for PleromaFE to group statuses by context
+    field(:context_id, :integer)
+
+    field(:likes, {:array, :string}, default: [])
+    field(:announcements, {:array, :string}, default: [])
+
+    field(:closed, ObjectValidators.DateTime)
+    field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
+    embeds_many(:anyOf, QuestionOptionsValidator)
+    embeds_many(:oneOf, QuestionOptionsValidator)
+  end
+
+  def cast_and_apply(data) do
+    data
+    |> cast_data
+    |> apply_action(:insert)
+  end
+
+  def cast_and_validate(data) do
+    data
+    |> cast_data()
+    |> validate_data()
+  end
+
+  def cast_data(data) do
+    %__MODULE__{}
+    |> changeset(data)
+  end
+
+  defp fix_closed(data) do
+    cond do
+      is_binary(data["closed"]) -> data
+      is_binary(data["endTime"]) -> Map.put(data, "closed", data["endTime"])
+      true -> Map.drop(data, ["closed"])
+    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)
+
+    struct
+    |> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment])
+    |> cast_embed(:attachment)
+    |> cast_embed(:anyOf)
+    |> cast_embed(:oneOf)
+  end
+
+  def validate_data(data_cng) do
+    data_cng
+    |> validate_inclusion(:type, ["Question"])
+    |> validate_required([:id, :actor, :attributedTo, :type, :content, :context])
+    |> CommonValidations.validate_any_presence([:cc, :to])
+    |> CommonValidations.validate_fields_match([:actor, :attributedTo])
+    |> CommonValidations.validate_actor_is_active()
+    |> CommonValidations.validate_any_presence([:oneOf, :anyOf])
+    |> CommonValidations.validate_host_match()
+  end
+end
index f64fac46daffa28ef292960f8106d120977bb750..881030f386f5f0ce3b291e4047e7cfb17804bf26 100644 (file)
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do
   embedded_schema do
     field(:type, :string)
     field(:href, ObjectValidators.Uri)
-    field(:mediaType, :string)
+    field(:mediaType, :string, default: "application/octet-stream")
   end
 
   def changeset(struct, data) do
index 1d2c296a5c001a99d02c8ae72256d61029aa1dd4..5104d38eeafbcbda067abfdf5339eee93c75112b 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,26 @@ 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
+  # - Increase replies count
+  # - Set up ActivityExpiration
   # - 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,9 +286,27 @@ 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}
+    end
+  end
+
   # Nothing to do
-  def handle_object_creation(object) do
-    {:ok, object}
+  def handle_object_creation(object, meta) do
+    {:ok, object, meta}
   end
 
   defp undo_like(nil, object), do: delete_object(object)
index 35aa05eb5a1025bfd725dd5d11f7acd739d19c5f..f85a266796f89ad2482602040109a2191fe83172 100644 (file)
@@ -157,7 +157,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   end
 
   def fix_actor(%{"attributedTo" => actor} = object) do
-    Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
+    actor = Containment.get_actor(%{"actor" => actor})
+
+    # TODO: Remove actor field for Objects
+    object
+    |> Map.put("actor", actor)
+    |> Map.put("attributedTo", actor)
   end
 
   def fix_in_reply_to(object, options \\ [])
@@ -240,13 +245,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
 
         if href do
           attachment_url =
-            %{"href" => href}
+            %{
+              "href" => href,
+              "type" => Map.get(url || %{}, "type", "Link")
+            }
             |> Maps.put_if_present("mediaType", media_type)
-            |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
 
-          %{"url" => [attachment_url]}
+          %{
+            "url" => [attachment_url],
+            "type" => data["type"] || "Document"
+          }
           |> Maps.put_if_present("mediaType", media_type)
-          |> Maps.put_if_present("type", data["type"])
           |> Maps.put_if_present("name", data["name"])
         else
           nil
@@ -419,6 +428,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
@@ -457,30 +489,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
         %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
         options
       )
-      when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
+      when objtype in ["Article", "Event", "Note", "Video", "Page", "Audio"] 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
@@ -613,6 +633,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     |> handle_incoming(options)
   end
 
+  def handle_incoming(
+        %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
+        _options
+      )
+      when objtype in ["Question", "Answer"] do
+    data =
+      data
+      |> Map.put("object", fix_object(object))
+      |> fix_addressing()
+
+    data = Map.put_new(data, "context", data["object"]["context"])
+
+    with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
+         {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
+      {:ok, activity}
+    end
+  end
+
   def handle_incoming(
         %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
         _options
index 4d5b0decf77c3a3b3aec03b9a1f046a81de35ed9..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"])
@@ -340,8 +341,13 @@ defmodule Pleroma.Web.CommonAPI do
     end
   end
 
-  defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
-  defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
+  defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
+       when is_list(any_of) and any_of != [],
+       do: {any_of, Enum.count(any_of)}
+
+  defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
+       when is_list(one_of) and one_of != [],
+       do: {one_of, 1}
 
   defp normalize_and_validate_choices(choices, object) do
     choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
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 59a5deb28bf360c8b533bac7102a81527aea86bd..ce595ae8a8825ba0684c64b9cba800932cb17491 100644 (file)
@@ -28,10 +28,10 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
 
   def render("show.json", %{object: object} = params) do
     case object.data do
-      %{"anyOf" => options} when is_list(options) ->
+      %{"anyOf" => options} when is_list(options) and options != [] ->
         render(__MODULE__, "show.json", Map.merge(params, %{multiple: true, options: options}))
 
-      %{"oneOf" => options} when is_list(options) ->
+      %{"oneOf" => options} when is_list(options) and options != [] ->
         render(__MODULE__, "show.json", Map.merge(params, %{multiple: false, options: options}))
 
       _ ->
@@ -40,15 +40,13 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
   end
 
   defp end_time_and_expired(object) do
-    case object.data["closed"] || object.data["endTime"] do
-      end_time when is_binary(end_time) ->
-        end_time = NaiveDateTime.from_iso8601!(end_time)
-        expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt
+    if object.data["closed"] do
+      end_time = NaiveDateTime.from_iso8601!(object.data["closed"])
+      expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt
 
-        {Utils.to_masto_date(end_time), expired}
-
-      _ ->
-        {nil, false}
+      {Utils.to_masto_date(end_time), expired}
+    else
+      {nil, false}
     end
   end
 
index ac329c7d5f4754b6dade73272ac5b56898ebcec9..3648b9f90bd44d60b410ce6657c09f3640c92e04 100644 (file)
@@ -49,7 +49,6 @@
       "en": "<p>Why is Tenshi eating a corndog so cute?</p>"
     },
     "endTime": "2019-05-11T09:03:36Z",
-    "closed": "2019-05-11T09:03:36Z",
     "attachment": [],
     "tag": [],
     "replies": {
diff --git a/test/fixtures/tesla_mock/poll_attachment.json b/test/fixtures/tesla_mock/poll_attachment.json
new file mode 100644 (file)
index 0000000..92e822d
--- /dev/null
@@ -0,0 +1,99 @@
+{
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://patch.cx/schemas/litepub-0.1.jsonld",
+    {
+      "@language": "und"
+    }
+  ],
+  "actor": "https://patch.cx/users/rin",
+  "anyOf": [],
+  "attachment": [
+    {
+      "mediaType": "image/jpeg",
+      "name": "screenshot_mpv:Totoro@01:18:44.345.jpg",
+      "type": "Document",
+      "url": "https://shitposter.club/media/3bb4c4d402f8fdcc7f80963c3d7cf6f10f936897fd374922ade33199d2f86d87.jpg?name=screenshot_mpv%3ATotoro%4001%3A18%3A44.345.jpg"
+    }
+  ],
+  "attributedTo": "https://patch.cx/users/rin",
+  "cc": [
+    "https://patch.cx/users/rin/followers"
+  ],
+  "closed": "2020-06-19T23:22:02.754678Z",
+  "content": "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"9vwjTNzEWEM1TfkBGq\" href=\"https://mastodon.sdf.org/users/rinpatch\" rel=\"ugc\">@<span>rinpatch</span></a></span>",
+  "closed": "2019-09-19T00:32:36.785333",
+  "content": "can you vote on this poll?",
+  "id": "https://patch.cx/objects/tesla_mock/poll_attachment",
+  "oneOf": [
+    {
+      "name": "a",
+      "replies": {
+        "totalItems": 0,
+        "type": "Collection"
+      },
+      "type": "Note"
+    },
+    {
+      "name": "A",
+      "replies": {
+        "totalItems": 0,
+        "type": "Collection"
+      },
+      "type": "Note"
+    },
+    {
+      "name": "Aa",
+      "replies": {
+        "totalItems": 0,
+        "type": "Collection"
+      },
+      "type": "Note"
+    },
+    {
+      "name": "AA",
+      "replies": {
+        "totalItems": 0,
+        "type": "Collection"
+      },
+      "type": "Note"
+    },
+    {
+      "name": "AAa",
+      "replies": {
+        "totalItems": 1,
+        "type": "Collection"
+      },
+      "type": "Note"
+    },
+    {
+      "name": "AAA",
+      "replies": {
+        "totalItems": 3,
+        "type": "Collection"
+      },
+      "type": "Note"
+    }
+  ],
+  "published": "2020-06-19T23:12:02.786113Z",
+  "sensitive": false,
+  "summary": "",
+  "tag": [
+    {
+      "href": "https://mastodon.sdf.org/users/rinpatch",
+      "name": "@rinpatch@mastodon.sdf.org",
+      "type": "Mention"
+    }
+  ],
+  "to": [
+    "https://www.w3.org/ns/activitystreams#Public",
+    "https://mastodon.sdf.org/users/rinpatch"
+  ],
+  "type": "Question",
+  "voters": [
+    "https://shitposter.club/users/moonman",
+    "https://skippers-bin.com/users/7v1w1r8ce6",
+    "https://mastodon.sdf.org/users/rinpatch",
+    "https://mastodon.social/users/emelie"
+  ]
+}
index d9098ea1b3069793fcdb1847b0496fc34fb1374b..16cfa7f5cb70e76b91ce605cf5cf0cd1897ade79 100644 (file)
@@ -177,6 +177,13 @@ defmodule Pleroma.Object.FetcherTest do
                  "https://mastodon.example.org/users/userisgone404"
                )
     end
+
+    test "it can fetch pleroma polls with attachments" do
+      {:ok, object} =
+        Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment")
+
+      assert object
+    end
   end
 
   describe "pruning" do
index 19a2026544963fa5eece3c5b35d69125efbfbda4..eeeba7880da95927b5dac7c8f8450fff35077733 100644 (file)
@@ -82,6 +82,14 @@ defmodule HttpRequestMock do
      }}
   end
 
+  def get("https://patch.cx/objects/tesla_mock/poll_attachment", _, _, _) do
+    {:ok,
+     %Tesla.Env{
+       status: 200,
+       body: File.read!("test/fixtures/tesla_mock/poll_attachment.json")
+     }}
+  end
+
   def get(
         "https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/emelie",
         _,
diff --git a/test/web/activity_pub/transmogrifier/answer_handling_test.exs b/test/web/activity_pub/transmogrifier/answer_handling_test.exs
new file mode 100644 (file)
index 0000000..0f6605c
--- /dev/null
@@ -0,0 +1,78 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Activity
+  alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.Transmogrifier
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  setup_all do
+    Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+    :ok
+  end
+
+  test "incoming, rewrites Note to Answer and increments vote counters" do
+    user = insert(:user)
+
+    {:ok, activity} =
+      CommonAPI.post(user, %{
+        status: "suya...",
+        poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
+      })
+
+    object = Object.normalize(activity)
+
+    data =
+      File.read!("test/fixtures/mastodon-vote.json")
+      |> Poison.decode!()
+      |> Kernel.put_in(["to"], user.ap_id)
+      |> Kernel.put_in(["object", "inReplyTo"], object.data["id"])
+      |> Kernel.put_in(["object", "to"], user.ap_id)
+
+    {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
+    answer_object = Object.normalize(activity)
+    assert answer_object.data["type"] == "Answer"
+    assert answer_object.data["inReplyTo"] == object.data["id"]
+
+    new_object = Object.get_by_ap_id(object.data["id"])
+    assert new_object.data["replies_count"] == object.data["replies_count"]
+
+    assert Enum.any?(
+             new_object.data["oneOf"],
+             fn
+               %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true
+               _ -> false
+             end
+           )
+  end
+
+  test "outgoing, rewrites Answer to Note" do
+    user = insert(:user)
+
+    {:ok, poll_activity} =
+      CommonAPI.post(user, %{
+        status: "suya...",
+        poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
+      })
+
+    poll_object = Object.normalize(poll_activity)
+    # TODO: Replace with CommonAPI vote creation when implemented
+    data =
+      File.read!("test/fixtures/mastodon-vote.json")
+      |> Poison.decode!()
+      |> Kernel.put_in(["to"], user.ap_id)
+      |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"])
+      |> Kernel.put_in(["object", "to"], user.ap_id)
+
+    {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
+    {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+
+    assert data["object"]["type"] == "Note"
+  end
+end
diff --git a/test/web/activity_pub/transmogrifier/question_handling_test.exs b/test/web/activity_pub/transmogrifier/question_handling_test.exs
new file mode 100644 (file)
index 0000000..12516c4
--- /dev/null
@@ -0,0 +1,114 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Activity
+  alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.Transmogrifier
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  setup_all do
+    Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+    :ok
+  end
+
+  test "Mastodon Question activity" do
+    data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!()
+
+    {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
+
+    object = Object.normalize(activity, false)
+
+    assert object.data["closed"] == "2019-05-11T09:03:36Z"
+
+    assert object.data["context"] == activity.data["context"]
+
+    assert object.data["context"] ==
+             "tag:mastodon.sdf.org,2019-05-10:objectId=15095122:objectType=Conversation"
+
+    assert object.data["context_id"]
+
+    assert object.data["anyOf"] == []
+
+    assert Enum.sort(object.data["oneOf"]) ==
+             Enum.sort([
+               %{
+                 "name" => "25 char limit is dumb",
+                 "replies" => %{"totalItems" => 0, "type" => "Collection"},
+                 "type" => "Note"
+               },
+               %{
+                 "name" => "Dunno",
+                 "replies" => %{"totalItems" => 0, "type" => "Collection"},
+                 "type" => "Note"
+               },
+               %{
+                 "name" => "Everyone knows that!",
+                 "replies" => %{"totalItems" => 1, "type" => "Collection"},
+                 "type" => "Note"
+               },
+               %{
+                 "name" => "I can't even fit a funny",
+                 "replies" => %{"totalItems" => 1, "type" => "Collection"},
+                 "type" => "Note"
+               }
+             ])
+
+    user = insert(:user)
+
+    {:ok, reply_activity} = CommonAPI.post(user, %{status: "hewwo", in_reply_to_id: activity.id})
+
+    reply_object = Object.normalize(reply_activity, false)
+
+    assert reply_object.data["context"] == object.data["context"]
+    assert reply_object.data["context_id"] == object.data["context_id"]
+  end
+
+  test "Mastodon Question activity with HTML tags in plaintext" do
+    options = [
+      %{
+        "type" => "Note",
+        "name" => "<input type=\"date\">",
+        "replies" => %{"totalItems" => 0, "type" => "Collection"}
+      },
+      %{
+        "type" => "Note",
+        "name" => "<input type=\"date\"/>",
+        "replies" => %{"totalItems" => 0, "type" => "Collection"}
+      },
+      %{
+        "type" => "Note",
+        "name" => "<input type=\"date\" />",
+        "replies" => %{"totalItems" => 1, "type" => "Collection"}
+      },
+      %{
+        "type" => "Note",
+        "name" => "<input type=\"date\"></input>",
+        "replies" => %{"totalItems" => 1, "type" => "Collection"}
+      }
+    ]
+
+    data =
+      File.read!("test/fixtures/mastodon-question-activity.json")
+      |> Poison.decode!()
+      |> Kernel.put_in(["object", "oneOf"], options)
+
+    {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
+    object = Object.normalize(activity, false)
+
+    assert Enum.sort(object.data["oneOf"]) == Enum.sort(options)
+  end
+
+  test "returns an error if received a second time" do
+    data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!()
+
+    assert {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
+
+    assert {:error, {:validate_object, {:error, _}}} = Transmogrifier.handle_incoming(data)
+  end
+end
index 7d33feaf28176a4fff62159854ae960671f1fddc..92ab0f28f0247d2c9605bc6c75940f2da1577e45 100644 (file)
@@ -217,23 +217,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert Enum.at(object.data["tag"], 2) == "moo"
     end
 
-    test "it works for incoming questions" do
-      data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!()
-
-      {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
-
-      object = Object.normalize(activity)
-
-      assert Enum.all?(object.data["oneOf"], fn choice ->
-               choice["name"] in [
-                 "Dunno",
-                 "Everyone knows that!",
-                 "25 char limit is dumb",
-                 "I can't even fit a funny"
-               ]
-             end)
-    end
-
     test "it works for incoming listens" do
       data = %{
         "@context" => "https://www.w3.org/ns/activitystreams",
@@ -263,38 +246,6 @@ 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
-      user = insert(:user)
-
-      {:ok, activity} =
-        CommonAPI.post(user, %{
-          status: "suya...",
-          poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
-        })
-
-      object = Object.normalize(activity)
-
-      data =
-        File.read!("test/fixtures/mastodon-vote.json")
-        |> Poison.decode!()
-        |> Kernel.put_in(["to"], user.ap_id)
-        |> Kernel.put_in(["object", "inReplyTo"], object.data["id"])
-        |> Kernel.put_in(["object", "to"], user.ap_id)
-
-      {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
-      answer_object = Object.normalize(activity)
-      assert answer_object.data["type"] == "Answer"
-      object = Object.get_by_ap_id(object.data["id"])
-
-      assert Enum.any?(
-               object.data["oneOf"],
-               fn
-                 %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true
-                 _ -> false
-               end
-             )
-    end
-
     test "it works for incoming notices with contentMap" do
       data =
         File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!()
@@ -669,7 +620,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
                    %{
                      "href" =>
                        "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
-                     "mediaType" => "video/mp4"
+                     "mediaType" => "video/mp4",
+                     "type" => "Link"
                    }
                  ]
                }
@@ -688,7 +640,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
                    %{
                      "href" =>
                        "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4",
-                     "mediaType" => "video/mp4"
+                     "mediaType" => "video/mp4",
+                     "type" => "Link"
                    }
                  ]
                }
@@ -1261,30 +1214,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
     end
   end
 
-  test "Rewrites Answers to Notes" do
-    user = insert(:user)
-
-    {:ok, poll_activity} =
-      CommonAPI.post(user, %{
-        status: "suya...",
-        poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
-      })
-
-    poll_object = Object.normalize(poll_activity)
-    # TODO: Replace with CommonAPI vote creation when implemented
-    data =
-      File.read!("test/fixtures/mastodon-vote.json")
-      |> Poison.decode!()
-      |> Kernel.put_in(["to"], user.ap_id)
-      |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"])
-      |> Kernel.put_in(["object", "to"], user.ap_id)
-
-    {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
-    {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
-
-    assert data["object"]["type"] == "Note"
-  end
-
   describe "fix_explicit_addressing" do
     setup do
       user = insert(:user)
@@ -1532,8 +1461,13 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
                "attachment" => [
                  %{
                    "mediaType" => "video/mp4",
+                   "type" => "Document",
                    "url" => [
-                     %{"href" => "https://peertube.moe/stat-480.mp4", "mediaType" => "video/mp4"}
+                     %{
+                       "href" => "https://peertube.moe/stat-480.mp4",
+                       "mediaType" => "video/mp4",
+                       "type" => "Link"
+                     }
                    ]
                  }
                ]
@@ -1550,14 +1484,24 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
                "attachment" => [
                  %{
                    "mediaType" => "video/mp4",
+                   "type" => "Document",
                    "url" => [
-                     %{"href" => "https://pe.er/stat-480.mp4", "mediaType" => "video/mp4"}
+                     %{
+                       "href" => "https://pe.er/stat-480.mp4",
+                       "mediaType" => "video/mp4",
+                       "type" => "Link"
+                     }
                    ]
                  },
                  %{
                    "mediaType" => "video/mp4",
+                   "type" => "Document",
                    "url" => [
-                     %{"href" => "https://pe.er/stat-480.mp4", "mediaType" => "video/mp4"}
+                     %{
+                       "href" => "https://pe.er/stat-480.mp4",
+                       "mediaType" => "video/mp4",
+                       "type" => "Link"
+                     }
                    ]
                  }
                ]
index 76672f36c79bdf1f928b791d0b70ecfc049a9cbc..b7e2f17eff9faf85af39ba109f1ff0bc964d493e 100644 (file)
@@ -135,4 +135,33 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
     assert result[:expires_at] == nil
     assert result[:expired] == false
   end
+
+  test "doesn't strips HTML tags" do
+    user = insert(:user)
+
+    {:ok, activity} =
+      CommonAPI.post(user, %{
+        status: "What's with the smug face?",
+        poll: %{
+          options: [
+            "<input type=\"date\">",
+            "<input type=\"date\" >",
+            "<input type=\"date\"/>",
+            "<input type=\"date\"></input>"
+          ],
+          expires_in: 20
+        }
+      })
+
+    object = Object.normalize(activity)
+
+    assert %{
+             options: [
+               %{title: "<input type=\"date\">", votes_count: 0},
+               %{title: "<input type=\"date\" >", votes_count: 0},
+               %{title: "<input type=\"date\"/>", votes_count: 0},
+               %{title: "<input type=\"date\"></input>", votes_count: 0}
+             ]
+           } = PollView.render("show.json", %{object: object})
+  end
 end