QuestionValidator: Create

This commit is contained in:
Haelwenn (lanodan) Monnier 2020-06-11 20:23:10 +02:00
parent 3f65f2ea79
commit 7bcd7a9595
No known key found for this signature in database
GPG Key ID: D5B7A8E43C997DEE
10 changed files with 297 additions and 11 deletions

View File

@ -95,7 +95,7 @@ defp increase_poll_votes_if_vote(%{
defp increase_poll_votes_if_vote(_create_data), do: :noop defp increase_poll_votes_if_vote(_create_data), do: :noop
@object_types ["ChatMessage"] @object_types ["ChatMessage", "Question"]
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
def persist(%{"type" => type} = object, meta) when type in @object_types do def persist(%{"type" => type} = object, meta) when type in @object_types do
with {:ok, object} <- Object.create(object) do with {:ok, object} <- Object.create(object) do

View File

@ -16,10 +16,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateQuestionValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
@ -105,17 +107,30 @@ def validate(%{"type" => "ChatMessage"} = object, meta) do
end end
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" => "EmojiReact"} = object, meta) do def validate(%{"type" => "EmojiReact"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> EmojiReactValidator.cast_and_validate() |> EmojiReactValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object |> Map.from_struct()) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
end end
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), with {:ok, object_data} <- cast_and_apply(object),
meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
{:ok, create_activity} <- {:ok, create_activity} <-
@ -127,12 +142,27 @@ def validate(%{"type" => "Create", "object" => object} = create_activity, meta)
end end
end end
def validate(
%{"type" => "Create", "object" => %{"type" => "Question"} = 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} <-
create_activity
|> CreateQuestionValidator.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 def validate(%{"type" => "Announce"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> AnnounceValidator.cast_and_validate() |> AnnounceValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object |> Map.from_struct()) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
end end
end end
@ -141,8 +171,13 @@ def cast_and_apply(%{"type" => "ChatMessage"} = object) do
ChatMessageValidator.cast_and_apply(object) ChatMessageValidator.cast_and_apply(object)
end end
def cast_and_apply(%{"type" => "Question"} = object) do
QuestionValidator.cast_and_apply(object)
end
def cast_and_apply(o), do: {:error, {:validator_not_set, o}} 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 def stringify_keys(%{__struct__: _} = object) do
object object
|> Map.from_struct() |> Map.from_struct()

View File

@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
def validate_recipients_presence(cng, fields \\ [:to, :cc]) do def validate_any_presence(cng, fields) do
non_empty = non_empty =
fields fields
|> Enum.map(fn field -> get_field(cng, field) end) |> Enum.map(fn field -> get_field(cng, field) end)
@ -24,7 +24,7 @@ def validate_recipients_presence(cng, fields \\ [:to, :cc]) do
fields fields
|> Enum.reduce(cng, fn field, cng -> |> Enum.reduce(cng, fn field, cng ->
cng cng
|> add_error(field, "no recipients in any field") |> add_error(field, "none of #{inspect(fields)} present")
end) end)
end end
end end

View File

@ -0,0 +1,94 @@
# 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
# - Can probably be a generic create validator
# - doesn't embed, will only get the object id
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateQuestionValidator do
use Ecto.Schema
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:actor, Types.ObjectID)
field(:type, :string)
field(:to, Types.Recipients, default: [])
field(:cc, Types.Recipients, default: [])
field(:object, Types.ObjectID)
end
def cast_and_apply(data) do
data
|> cast_data
|> apply_action(:insert)
end
def cast_data(data) do
cast(%__MODULE__{}, data, __schema__(:fields))
end
def cast_and_validate(data, meta \\ []) do
cast_data(data)
|> validate_data(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_recipients_match(meta)
|> validate_actors_match(meta)
|> validate_object_nonexistence()
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
object_actor = meta[:object_data]["actor"]
cng
|> validate_change(:actor, fn :actor, actor ->
if actor == object_actor do
[]
else
[{:actor, "Actor doesn't match with object actor"}]
end
end)
end
def validate_recipients_match(cng, meta) do
object_recipients = meta[:object_data]["to"] || []
cng
|> validate_change(:to, fn :to, recipients ->
activity_set = MapSet.new(recipients)
object_set = MapSet.new(object_recipients)
if MapSet.equal?(activity_set, object_set) do
[]
else
[{:to, "Recipients don't match with object recipients"}]
end
end)
end
end

View File

@ -34,7 +34,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
field(:replies_count, :integer, default: 0) field(:replies_count, :integer, default: 0)
field(:like_count, :integer, default: 0) field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0) field(:announcement_count, :integer, default: 0)
field(:inRepyTo, :string) field(:inReplyTo, :string)
field(:uri, ObjectValidators.Uri) field(:uri, ObjectValidators.Uri)
field(:likes, {:array, :string}, default: []) field(:likes, {:array, :string}, default: [])

View File

@ -0,0 +1,47 @@
# 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
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsRepliesValidator
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:name, :string)
embeds_one(:replies, QuestionOptionsRepliesValidator)
field(:type, :string)
end
def changeset(struct, data) do
struct
|> cast(data, [:name, :type])
|> cast_embed(:replies)
|> validate_inclusion(:type, ["Note"])
|> validate_required([:name, :type])
end
end
defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsRepliesValidator do
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:totalItems, :integer)
field(:type, :string)
end
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
|> validate_inclusion(:type, ["Collection"])
|> validate_required([:type])
end
end

View File

@ -0,0 +1,89 @@
# 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.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
@primary_key false
@derive Jason.Encoder
# Extends from NoteValidator
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
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)
field(:actor, Types.ObjectID)
field(:attributedTo, Types.ObjectID)
field(:summary, :string)
field(:published, Types.DateTime)
# TODO: Write type
field(:emoji, :map, default: %{})
field(:sensitive, :boolean, default: false)
# TODO: Write type
field(:attachment, {:array, :map}, default: [])
field(:replies_count, :integer, default: 0)
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:inReplyTo, :string)
field(:uri, Types.Uri)
field(:likes, {:array, :string}, default: [])
field(:announcements, {:array, :string}, default: [])
# see if needed
field(:conversation, :string)
field(:context_id, :string)
field(:closed, Types.DateTime)
field(:voters, {:array, Types.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
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields) -- [:anyOf, :oneOf])
|> cast_embed(:anyOf)
|> cast_embed(:oneOf)
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Question"])
|> validate_required([:id, :actor, :type, :content, :context])
|> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_actor_presence()
|> CommonValidations.validate_any_presence([:oneOf, :anyOf])
end
end

View File

@ -268,9 +268,15 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
end end
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 # Nothing to do
def handle_object_creation(object) do def handle_object_creation(object, meta) do
{:ok, object} {:ok, object, meta}
end end
defp undo_like(nil, object), do: delete_object(object) defp undo_like(nil, object), do: delete_object(object)

View File

@ -457,7 +457,7 @@ def handle_incoming(
%{"type" => "Create", "object" => %{"type" => objtype} = object} = data, %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
options options
) )
when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do when objtype in ["Article", "Event", "Note", "Video", "Page", "Answer", "Audio"] do
actor = Containment.get_actor(data) actor = Containment.get_actor(data)
with nil <- Activity.get_create_by_object_ap_id(object["id"]), with nil <- Activity.get_create_by_object_ap_id(object["id"]),
@ -613,6 +613,21 @@ def handle_incoming(
|> handle_incoming(options) |> handle_incoming(options)
end end
def handle_incoming(
%{"type" => "Create", "object" => %{"type" => "Question"} = object} = data,
_options
) do
data =
data
|> Map.put("object", fix_object(object))
|> fix_addressing()
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
end
end
def handle_incoming( def handle_incoming(
%{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
_options _options

View File

@ -222,7 +222,7 @@ test "it works for incoming questions" do
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity) object = Object.normalize(activity, false)
assert Enum.all?(object.data["oneOf"], fn choice -> assert Enum.all?(object.data["oneOf"], fn choice ->
choice["name"] in [ choice["name"] in [