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 @@ defmodule Pleroma.Object.Containment 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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 @@ defmodule Pleroma.Web.ActivityPub.Builder 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator 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 @@ defmodule Pleroma.Web.ActivityPub.SideEffects 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 @@ defmodule Pleroma.Web.ActivityPub.SideEffects 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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"]),
{: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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest 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} =