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

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,16 +85,6 @@ 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", "Question", "Answer"]
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
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),
{: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),

View file

@ -115,6 +115,21 @@ defmodule Pleroma.Web.ActivityPub.Builder do
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()}
def tombstone(actor, id) do
{:ok,

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
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.EmojiReactValidator
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),
{:ok, create_activity} <-
create_activity
|> CreateQuestionValidator.cast_and_validate(meta)
|> CreateGenericValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do
create_activity = stringify_keys(create_activity)
{:ok, create_activity, meta}
@ -188,7 +188,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
def cast_and_apply(%{"type" => "Answer"} = object) do
QuestionValidator.cast_and_apply(object)
AnswerValidator.cast_and_apply(object)
end
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
@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: [])
# 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, Types.ObjectID)
field(:actor, Types.ObjectID)
end
def cast_and_apply(data) do
data
|> cast_data
|> cast_data()
|> apply_action(:insert)
end

View file

@ -42,6 +42,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
end)
end
def validate_actor_is_active(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :actor)
cng
|> validate_change(field_name, fn field_name, actor ->
if %User{deactivated: false} = User.get_cached_by_ap_id(actor) do
[]
else
[{field_name, "can't find user (or deactivated)"}]
end
end)
end
def validate_object_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :object)
allowed_types = Keyword.get(options, :allowed_types, false)
@ -77,4 +90,29 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
if actor_cng.valid?, do: actor_cng, else: object_cng
end
def validate_host_match(cng, fields \\ [:id, :actor]) do
unique_hosts =
fields
|> Enum.map(fn field ->
%URI{host: host} =
cng
|> get_field(field)
|> URI.parse()
host
end)
|> Enum.uniq()
|> Enum.count()
if unique_hosts == 1 do
cng
else
fields
|> Enum.reduce(cng, fn field, cng ->
cng
|> add_error(field, "hosts of #{inspect(fields)} aren't matching")
end)
end
end
end

View file

@ -4,9 +4,8 @@
# 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
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
use Ecto.Schema
alias Pleroma.Object
@ -26,29 +25,53 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateQuestionValidator do
field(:object, Types.ObjectID)
end
def cast_data(data) do
%__MODULE__{}
|> changeset(data)
end
def cast_and_apply(data) do
data
|> cast_data
|> apply_action(:insert)
end
def cast_data(data) do
cast(%__MODULE__{}, data, __schema__(:fields))
def cast_and_validate(data, meta \\ []) do
data
|> cast_data
|> validate_data(meta)
end
def cast_and_validate(data, meta \\ []) do
cast_data(data)
|> validate_data(meta)
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
end
def validate_data(cng, meta \\ []) do
cng
|> validate_required([:actor, :type, :object])
|> validate_inclusion(:type, ["Create"])
|> validate_actor_presence()
|> validate_actor_is_active()
|> validate_any_presence([:to, :cc])
|> validate_actors_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

View file

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

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,24 @@ 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
# - 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,6 +284,18 @@ 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}

View file

@ -419,6 +419,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
@ -461,26 +484,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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)
{: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()
params = %{
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
with {:ok, created_activity} <- handle_create(data, user) do
reply_depth = (options[:depth] || 0) + 1
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
answer_activities =
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} =
ActivityPub.create(%{
to: answer_data["to"],
actor: user,
context: object.data["context"],
object: answer_data,
additional: %{"cc" => answer_data["cc"]}
})
{:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
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)
object = Object.get_cached_by_ap_id(object.data["id"])

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

View file

@ -282,7 +282,7 @@ 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
test "it rewrites Note votes to Answer and increments vote counters on Question activities" do
user = insert(:user)
{:ok, activity} =