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
@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 @@ defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(
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 @@ defp increase_replies_count_if_reply(%{
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 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param
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 @@ def chat_message(actor, recipient, content, opts \\ []) 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 @@ def validate(
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 @@ def cast_and_apply(%{"type" => "Question"} = object) 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 @@ def validate_actor_presence(cng, options \\ []) 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 @@ def validate_object_or_user_presence(cng, options \\ []) 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 @@ def cast_data(data) 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 @@ def fix(data) 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 @@ def validate_data(data_cng) 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 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) 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 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) 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 @@ defp get_reported(objects) 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 @@ def handle_incoming(
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 @@ def vote(user, %{data: %{"type" => "Question"}} = object, choices) 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 @@ def conversation_id_to_context(id) 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 @@ test "it works for incoming listens" 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} =