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
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
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
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),
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,
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
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}
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}}
@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
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)
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
# 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
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))
- end
-
def cast_and_validate(data, meta \\ []) do
- cast_data(data)
+ data
+ |> cast_data
|> validate_data(meta)
end
+ 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
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
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)
|> 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"])
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)
|> 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
"""
alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics
+ alias Pleroma.ActivityExpiration
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.FollowingRelationship
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
+ alias Pleroma.Workers.BackgroundWorker
def handle(object, meta \\ [])
# 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
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}
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
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)
-
- params = %{
- to: data["to"],
- object: object,
- actor: user,
- context: object["context"],
- local: false,
- published: data["published"],
- additional:
- Map.take(data, [
- "cc",
- "directMessage",
- "id"
- ])
- }
+ {: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()
- 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
{: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, activity} =
- ActivityPub.create(%{
- to: answer_data["to"],
- actor: user,
- context: object.data["context"],
- object: answer_data,
- additional: %{"cc" => answer_data["cc"]}
- })
-
- activity
+ {:ok, answer_object, _meta} =
+ Builder.answer(user, object, Enum.at(options, index)["name"])
+
+ {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
+
+ {: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"])
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
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} =