Browse Source

Merge branch 'features/poll-validation' into 'develop'

Poll and votes pipeline ingestion

Closes #1362 and #1852

See merge request pleroma/pleroma!2635
note-update
lain 3 years ago
parent
commit
34cbe9f44a
29 changed files with 1035 additions and 209 deletions
  1. +8
    -7
      lib/pleroma/object.ex
  2. +1
    -1
      lib/pleroma/object/containment.ex
  3. +29
    -10
      lib/pleroma/object/fetcher.ex
  4. +2
    -13
      lib/pleroma/web/activity_pub/activity_pub.ex
  5. +25
    -1
      lib/pleroma/web/activity_pub/builder.ex
  6. +54
    -3
      lib/pleroma/web/activity_pub/object_validator.ex
  7. +65
    -0
      lib/pleroma/web/activity_pub/object_validators/answer_validator.ex
  8. +58
    -2
      lib/pleroma/web/activity_pub/object_validators/common_validations.ex
  9. +133
    -0
      lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
  10. +1
    -27
      lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
  11. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/note_validator.ex
  12. +37
    -0
      lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex
  13. +127
    -0
      lib/pleroma/web/activity_pub/object_validators/question_validator.ex
  14. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex
  15. +39
    -3
      lib/pleroma/web/activity_pub/side_effects.ex
  16. +56
    -25
      lib/pleroma/web/activity_pub/transmogrifier.ex
  17. +20
    -14
      lib/pleroma/web/common_api/common_api.ex
  18. +0
    -11
      lib/pleroma/web/common_api/utils.ex
  19. +8
    -10
      lib/pleroma/web/mastodon_api/views/poll_view.ex
  20. +3
    -0
      test/emails/mailer_test.exs
  21. +0
    -1
      test/fixtures/mastodon-question-activity.json
  22. +99
    -0
      test/fixtures/tesla_mock/poll_attachment.json
  23. +7
    -0
      test/object/fetcher_test.exs
  24. +8
    -0
      test/support/http_request_mock.ex
  25. +1
    -1
      test/web/activity_pub/object_validators/delete_validation_test.exs
  26. +78
    -0
      test/web/activity_pub/transmogrifier/answer_handling_test.exs
  27. +123
    -0
      test/web/activity_pub/transmogrifier/question_handling_test.exs
  28. +22
    -78
      test/web/activity_pub/transmogrifier_test.exs
  29. +29
    -0
      test/web/mastodon_api/views/poll_view_test.exs

+ 8
- 7
lib/pleroma/object.ex View File

@@ -255,6 +255,10 @@ defmodule Pleroma.Object do
end
end

defp poll_is_multiple?(%Object{data: %{"anyOf" => [_ | _]}}), do: true

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 +285,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 +300,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


+ 1
- 1
lib/pleroma/object/containment.ex View 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



+ 29
- 10
lib/pleroma/object/fetcher.ex View 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}


+ 2
- 13
lib/pleroma/web/activity_pub/activity_pub.ex View 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),


+ 25
- 1
lib/pleroma/web/activity_pub/builder.ex View 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,


+ 54
- 3
lib/pleroma/web/activity_pub/object_validator.ex View 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()


+ 65
- 0
lib/pleroma/web/activity_pub/object_validators/answer_validator.ex View File

@@ -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_presence()
|> CommonValidations.validate_host_match()
end
end

+ 58
- 2
lib/pleroma/web/activity_pub/object_validators/common_validations.ex View 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
@@ -82,4 +82,60 @@ 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
if same_domain?(cng, fields) 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
if map_unique?(cng, fields) do
cng
else
fields
|> Enum.reduce(cng, fn field, cng ->
cng
|> add_error(field, "Fields #{inspect(fields)} aren't matching")
end)
end
end

defp map_unique?(cng, fields, func \\ & &1) do
Enum.reduce_while(fields, nil, fn field, acc ->
value =
cng
|> get_field(field)
|> func.()

case {value, acc} do
{value, nil} -> {:cont, value}
{value, value} -> {:cont, value}
_ -> {:halt, false}
end
end)
end

def same_domain?(cng, fields \\ [:actor, :object]) do
map_unique?(cng, fields, fn value -> URI.parse(value).host end)
end

# This figures out if a user is able to create, delete or modify something
# based on the domain and superuser status
def validate_modification_rights(cng) do
actor = User.get_cached_by_ap_id(get_field(cng, :actor))

if User.superuser?(actor) || same_domain?(cng) do
cng
else
cng
|> add_error(:actor, "is not allowed to modify object")
end
end
end

+ 133
- 0
lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex View File

@@ -0,0 +1,133 @@
# 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, meta \\ []) do
data = fix(data, meta)

%__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(meta)
|> validate_data(meta)
end

def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
end

defp fix_context(data, meta) do
if object = meta[:object_data] do
Map.put_new(data, "context", object["context"])
else
data
end
end

defp fix(data, meta) do
data
|> fix_context(meta)
end

def validate_data(cng, meta \\ []) do
cng
|> validate_required([:actor, :type, :object])
|> validate_inclusion(:type, ["Create"])
|> validate_actor_presence()
|> 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

+ 1
- 27
lib/pleroma/web/activity_pub/object_validators/delete_validator.ex View File

@@ -7,7 +7,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do

alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.User

import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@@ -59,7 +58,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Delete"])
|> validate_actor_presence()
|> validate_deletion_rights()
|> validate_modification_rights()
|> validate_object_or_user_presence(allowed_types: @deletable_types)
|> add_deleted_activity_id()
end
@@ -68,31 +67,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
!same_domain?(cng)
end

defp same_domain?(cng) do
actor_uri =
cng
|> get_field(:actor)
|> URI.parse()

object_uri =
cng
|> get_field(:object)
|> URI.parse()

object_uri.host == actor_uri.host
end

def validate_deletion_rights(cng) do
actor = User.get_cached_by_ap_id(get_field(cng, :actor))

if User.superuser?(actor) || same_domain?(cng) do
cng
else
cng
|> add_error(:actor, "is not allowed to delete object")
end
end

def cast_and_validate(data) do
data
|> cast_data


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/note_validator.ex View 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: [])


+ 37
- 0
lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex View File

@@ -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

+ 127
- 0
lib/pleroma/web/activity_pub/object_validators/question_validator.ex View File

@@ -0,0 +1,127 @@
# 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["conversation"])

data
|> Map.put_new_lazy("published", &Utils.make_date/0)
|> Map.put_new("context", context)
|> Map.put_new("context_id", context_id)
end

defp fix_attribution(data) do
data
|> Map.put_new("actor", data["attributedTo"])
end

defp fix(data) do
data
|> fix_attribution()
|> 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, :context])
|> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence()
|> CommonValidations.validate_any_presence([:oneOf, :anyOf])
|> CommonValidations.validate_host_match()
end
end

+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex View 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


+ 39
- 3
lib/pleroma/web/activity_pub/side_effects.ex View 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)


+ 56
- 25
lib/pleroma/web/activity_pub/transmogrifier.ex View 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
@@ -614,6 +634,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end

def handle_incoming(
%{"type" => "Create", "object" => %{"type" => objtype}} = data,
_options
)
when objtype in ["Question", "Answer", "ChatMessage"] do
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
) do


+ 20
- 14
lib/pleroma/web/common_api/common_api.ex View 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)


+ 0
- 11
lib/pleroma/web/common_api/utils.ex View 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


+ 8
- 10
lib/pleroma/web/mastodon_api/views/poll_view.ex View 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} ->
render(__MODULE__, "show.json", Map.merge(params, %{multiple: true, options: options}))

%{"oneOf" => options} when is_list(options) ->
%{"oneOf" => [_ | _] = 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



+ 3
- 0
test/emails/mailer_test.exs View File

@@ -19,6 +19,7 @@ defmodule Pleroma.Emails.MailerTest do
test "not send email when mailer is disabled" do
Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false)
Mailer.deliver(@email)
:timer.sleep(100)

refute_email_sent(
from: {"Pleroma", "noreply@example.com"},
@@ -30,6 +31,7 @@ defmodule Pleroma.Emails.MailerTest do

test "send email" do
Mailer.deliver(@email)
:timer.sleep(100)

assert_email_sent(
from: {"Pleroma", "noreply@example.com"},
@@ -41,6 +43,7 @@ defmodule Pleroma.Emails.MailerTest do

test "perform" do
Mailer.perform(:deliver_async, @email, [])
:timer.sleep(100)

assert_email_sent(
from: {"Pleroma", "noreply@example.com"},


+ 0
- 1
test/fixtures/mastodon-question-activity.json View 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": {


+ 99
- 0
test/fixtures/tesla_mock/poll_attachment.json View File

@@ -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"
]
}

+ 7
- 0
test/object/fetcher_test.exs View 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


+ 8
- 0
test/support/http_request_mock.ex View 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",
_,


+ 1
- 1
test/web/activity_pub/object_validators/delete_validation_test.exs View File

@@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidationTest do

{:error, cng} = ObjectValidator.validate(invalid_other_actor, [])

assert {:actor, {"is not allowed to delete object", []}} in cng.errors
assert {:actor, {"is not allowed to modify object", []}} in cng.errors
end

test "it's valid if the actor of the object is a local superuser",


+ 78
- 0
test/web/activity_pub/transmogrifier/answer_handling_test.exs View File

@@ -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

+ 123
- 0
test/web/activity_pub/transmogrifier/question_handling_test.exs View File

@@ -0,0 +1,123 @@
# 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

test "accepts a Question with no content" do
data =
File.read!("test/fixtures/mastodon-question-activity.json")
|> Poison.decode!()
|> Kernel.put_in(["object", "content"], "")

assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
end
end

+ 22
- 78
test/web/activity_pub/transmogrifier_test.exs View File

@@ -225,23 +225,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",
@@ -271,38 +254,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!()
@@ -677,7 +628,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"
}
]
}
@@ -696,7 +648,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"
}
]
}
@@ -1269,30 +1222,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)
@@ -1540,8 +1469,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"
}
]
}
]
@@ -1558,14 +1492,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"
}
]
}
]


+ 29
- 0
test/web/mastodon_api/views/poll_view_test.exs View 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

Loading…
Cancel
Save