SideEffects: port ones from ActivityPub.do_create and ActivityPub.insert

This commit is contained in:
Haelwenn (lanodan) Monnier 2020-06-18 04:05:42 +02:00
parent 4f70fd4105
commit 82895a4012
No known key found for this signature in database
GPG Key ID: D5B7A8E43C997DEE
13 changed files with 188 additions and 73 deletions

View File

@ -55,7 +55,7 @@ defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do
defp compare_uris(_id_uri, _other_uri), do: :error defp compare_uris(_id_uri, _other_uri), do: :error
@doc """ @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 def contain_origin(_id, %{"actor" => nil}), do: :error

View File

@ -66,7 +66,7 @@ defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(
defp check_remote_limit(_), do: true 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} if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end end
@ -85,16 +85,6 @@ defp increase_replies_count_if_reply(%{
defp increase_replies_count_if_reply(_create_data), do: :noop defp increase_replies_count_if_reply(_create_data), do: :noop
defp increase_poll_votes_if_vote(%{
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
"type" => "Create",
"actor" => actor
}) do
Object.increase_vote_count(reply_ap_id, name, actor)
end
defp increase_poll_votes_if_vote(_create_data), do: :noop
@object_types ["ChatMessage", "Question", "Answer"] @object_types ["ChatMessage", "Question", "Answer"]
@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
@ -258,7 +248,6 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param
with {:ok, activity} <- insert(create_data, local, fake), with {:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity}, {:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data), _ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity}, {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity), {:ok, _actor} <- increase_note_count_if_public(actor, activity),
_ <- notify_and_stream(activity), _ <- notify_and_stream(activity),

View File

@ -115,6 +115,21 @@ def chat_message(actor, recipient, content, opts \\ []) do
end end
end end
def answer(user, object, name) do
{:ok,
%{
"type" => "Answer",
"actor" => user.ap_id,
"cc" => [object.data["actor"]],
"to" => [],
"name" => name,
"inReplyTo" => object.data["id"],
"context" => object.data["context"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"id" => Utils.generate_object_id()
}, []}
end
@spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()} @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
def tombstone(actor, id) do def tombstone(actor, id) do
{:ok, {:ok,

View File

@ -17,7 +17,7 @@ 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.CreateGenericValidator
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
@ -162,7 +162,7 @@ def validate(
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} <-
create_activity create_activity
|> CreateQuestionValidator.cast_and_validate(meta) |> CreateGenericValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
create_activity = stringify_keys(create_activity) create_activity = stringify_keys(create_activity)
{:ok, create_activity, meta} {:ok, create_activity, meta}
@ -188,7 +188,7 @@ def cast_and_apply(%{"type" => "Question"} = object) do
end end
def cast_and_apply(%{"type" => "Answer"} = object) do def cast_and_apply(%{"type" => "Answer"} = object) do
QuestionValidator.cast_and_apply(object) AnswerValidator.cast_and_apply(object)
end end
def cast_and_apply(o), do: {:error, {:validator_not_set, o}} def cast_and_apply(o), do: {:error, {:validator_not_set, o}}

View File

@ -13,22 +13,25 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do
@primary_key false @primary_key false
@derive Jason.Encoder @derive Jason.Encoder
# Extends from NoteValidator
embedded_schema do embedded_schema do
field(:id, Types.ObjectID, primary_key: true) field(:id, Types.ObjectID, primary_key: true)
field(:to, {:array, :string}, default: []) field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: []) field(:cc, {:array, :string}, default: [])
# is this actually needed?
field(:bto, {:array, :string}, default: []) field(:bto, {:array, :string}, default: [])
field(:bcc, {:array, :string}, default: []) field(:bcc, {:array, :string}, default: [])
field(:type, :string) field(:type, :string)
field(:name, :string) field(:name, :string)
field(:inReplyTo, :string) field(:inReplyTo, :string)
field(:attributedTo, Types.ObjectID) field(:attributedTo, Types.ObjectID)
field(:actor, Types.ObjectID)
end end
def cast_and_apply(data) do def cast_and_apply(data) do
data data
|> cast_data |> cast_data()
|> apply_action(:insert) |> apply_action(:insert)
end end

View File

@ -42,6 +42,19 @@ def validate_actor_presence(cng, options \\ []) do
end) end)
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 def validate_object_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :object) field_name = Keyword.get(options, :field_name, :object)
allowed_types = Keyword.get(options, :allowed_types, false) allowed_types = Keyword.get(options, :allowed_types, false)
@ -77,4 +90,29 @@ def validate_object_or_user_presence(cng, options \\ []) do
if actor_cng.valid?, do: actor_cng, else: object_cng if actor_cng.valid?, do: actor_cng, else: object_cng
end end
def validate_host_match(cng, fields \\ [:id, :actor]) do
unique_hosts =
fields
|> Enum.map(fn field ->
%URI{host: host} =
cng
|> get_field(field)
|> URI.parse()
host
end)
|> Enum.uniq()
|> Enum.count()
if unique_hosts == 1 do
cng
else
fields
|> Enum.reduce(cng, fn field, cng ->
cng
|> add_error(field, "hosts of #{inspect(fields)} aren't matching")
end)
end
end
end end

View File

@ -4,9 +4,8 @@
# Code based on CreateChatMessageValidator # Code based on CreateChatMessageValidator
# NOTES # NOTES
# - Can probably be a generic create validator
# - doesn't embed, will only get the object id # - doesn't embed, will only get the object id
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateQuestionValidator do defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Object alias Pleroma.Object
@ -26,29 +25,53 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateQuestionValidator do
field(:object, Types.ObjectID) field(:object, Types.ObjectID)
end end
def cast_data(data) do
%__MODULE__{}
|> changeset(data)
end
def cast_and_apply(data) do def cast_and_apply(data) do
data data
|> cast_data |> cast_data
|> apply_action(:insert) |> apply_action(:insert)
end end
def cast_data(data) do def cast_and_validate(data, meta \\ []) do
cast(%__MODULE__{}, data, __schema__(:fields)) data
|> cast_data
|> validate_data(meta)
end end
def cast_and_validate(data, meta \\ []) do def changeset(struct, data) do
cast_data(data) struct
|> validate_data(meta) |> cast(data, __schema__(:fields))
end end
def validate_data(cng, meta \\ []) do def validate_data(cng, meta \\ []) do
cng cng
|> validate_required([:actor, :type, :object]) |> validate_required([:actor, :type, :object])
|> validate_inclusion(:type, ["Create"]) |> validate_inclusion(:type, ["Create"])
|> validate_actor_presence() |> validate_actor_is_active()
|> validate_any_presence([:to, :cc]) |> validate_any_presence([:to, :cc])
|> validate_actors_match(meta) |> validate_actors_match(meta)
|> validate_object_nonexistence() |> 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 end
def validate_object_nonexistence(cng) do def validate_object_nonexistence(cng) do

View File

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.ObjectValidators.Types
@ -40,13 +41,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
field(:announcement_count, :integer, default: 0) field(:announcement_count, :integer, default: 0)
field(:inReplyTo, :string) field(:inReplyTo, :string)
field(:uri, Types.Uri) field(:uri, Types.Uri)
# short identifier for PleromaFE to group statuses by context
field(:context_id, :integer)
field(:likes, {:array, :string}, default: []) field(:likes, {:array, :string}, default: [])
field(:announcements, {:array, :string}, default: []) field(:announcements, {:array, :string}, default: [])
# see if needed
field(:context_id, :string)
field(:closed, Types.DateTime) field(:closed, Types.DateTime)
field(:voters, {:array, Types.ObjectID}, default: []) field(:voters, {:array, Types.ObjectID}, default: [])
embeds_many(:anyOf, QuestionOptionsValidator) embeds_many(:anyOf, QuestionOptionsValidator)
@ -70,7 +70,7 @@ def cast_data(data) do
|> changeset(data) |> changeset(data)
end end
def fix(data) do defp fix_closed(data) do
cond do cond do
is_binary(data["closed"]) -> data is_binary(data["closed"]) -> data
is_binary(data["endTime"]) -> Map.put(data, "closed", data["endTime"]) is_binary(data["endTime"]) -> Map.put(data, "closed", data["endTime"])
@ -78,6 +78,23 @@ def fix(data) do
end end
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 def changeset(struct, data) do
data = fix(data) data = fix(data)
@ -92,7 +109,8 @@ def validate_data(data_cng) do
|> validate_inclusion(:type, ["Question"]) |> validate_inclusion(:type, ["Question"])
|> validate_required([:id, :actor, :type, :content, :context]) |> validate_required([:id, :actor, :type, :content, :context])
|> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_is_active()
|> CommonValidations.validate_any_presence([:oneOf, :anyOf]) |> CommonValidations.validate_any_presence([:oneOf, :anyOf])
|> CommonValidations.validate_host_match()
end end
end end

View File

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
""" """
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics alias Pleroma.Activity.Ir.Topics
alias Pleroma.ActivityExpiration
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.Chat.MessageReference alias Pleroma.Chat.MessageReference
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
@ -19,6 +20,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Push alias Pleroma.Web.Push
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
alias Pleroma.Workers.BackgroundWorker
def handle(object, meta \\ []) def handle(object, meta \\ [])
@ -135,10 +137,24 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do
# Tasks this handles # Tasks this handles
# - Actually create object # - Actually create object
# - Rollback if we couldn't create it # - Rollback if we couldn't create it
# - Increase the user note count
# - Increase the reply count
# - Set up notifications # - Set up notifications
def handle(%{data: %{"type" => "Create"}} = activity, meta) do 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, 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 =
meta meta
@ -268,6 +284,18 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
end end
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 def handle_object_creation(%{"type" => "Question"} = object, meta) do
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
{:ok, object, meta} {:ok, object, meta}

View File

@ -419,6 +419,29 @@ defp get_reported(objects) do
end) end)
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 \\ []) def handle_incoming(data, options \\ [])
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
@ -461,26 +484,14 @@ def handle_incoming(
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"]),
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor), {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do
data <- Map.put(data, "actor", actor) |> fix_addressing() do data =
object = fix_object(object, options) data
|> Map.put("object", fix_object(object, options))
|> Map.put("actor", actor)
|> fix_addressing()
params = %{ with {:ok, created_activity} <- handle_create(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"
])
}
with {:ok, created_activity} <- ActivityPub.create(params) do
reply_depth = (options[:depth] || 0) + 1 reply_depth = (options[:depth] || 0) + 1
if Federator.allowed_thread_distance?(reply_depth) do if Federator.allowed_thread_distance?(reply_depth) do

View File

@ -308,18 +308,19 @@ def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
answer_activities = answer_activities =
Enum.map(choices, fn index -> Enum.map(choices, fn index ->
answer_data = make_answer_data(user, object, Enum.at(options, index)["name"]) {:ok, answer_object, _meta} =
Builder.answer(user, object, Enum.at(options, index)["name"])
{:ok, activity} = {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
ActivityPub.create(%{
to: answer_data["to"],
actor: user,
context: object.data["context"],
object: answer_data,
additional: %{"cc" => answer_data["cc"]}
})
activity {: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) end)
object = Object.get_cached_by_ap_id(object.data["id"]) object = Object.get_cached_by_ap_id(object.data["id"])

View File

@ -548,17 +548,6 @@ def conversation_id_to_context(id) do
end end
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 def validate_character_limit("" = _full_payload, [] = _attachments) do
{:error, dgettext("errors", "Cannot post an empty status without attachments")} {:error, dgettext("errors", "Cannot post an empty status without attachments")}
end end

View File

@ -282,7 +282,7 @@ test "it works for incoming listens" do
assert object.data["length"] == 180_000 assert object.data["length"] == 180_000
end end
test "it rewrites Note votes to Answers and increments vote counters on question activities" do test "it rewrites Note votes to Answer and increments vote counters on Question activities" do
user = insert(:user) user = insert(:user)
{:ok, activity} = {:ok, activity} =