diff options
Diffstat (limited to 'lib/pleroma/web/activity_pub')
-rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 20 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/builder.ex | 26 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/object_validator.ex | 51 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex | 98 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/object_validators/common_validations.ex | 6 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex | 91 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex (renamed from lib/pleroma/web/activity_pub/object_validators/create_validator.ex) | 0 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/object_validators/types/recipients.ex | 36 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex | 25 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/side_effects.ex | 38 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/transmogrifier.ex | 10 |
11 files changed, 397 insertions, 4 deletions
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 1c21d78af..697ace488 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -126,7 +126,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def increase_poll_votes_if_vote(_create_data), do: :noop + @object_types ["ChatMessage"] @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} + def persist(%{"type" => type} = object, meta) when type in @object_types do + with {:ok, object} <- Object.create(object) do + {:ok, object, meta} + end + end + def persist(object, meta) do with local <- Keyword.fetch!(meta, :local), {recipients, _, _} <- get_recipients(object), @@ -1218,6 +1225,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end + defp exclude_chat_messages(query, %{"include_chat_messages" => true}), do: query + + defp exclude_chat_messages(query, _) do + if has_named_binding?(query, :object) do + from([activity, object: o] in query, + where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage") + ) + else + query + end + end + defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do from(activity in query, where: activity.id != ^id) end @@ -1323,6 +1342,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_instance(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) + |> exclude_chat_messages(opts) |> exclude_visibility(opts) end diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 429a510b8..7f9c071b3 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -5,11 +5,37 @@ defmodule Pleroma.Web.ActivityPub.Builder do This module encodes our addressing policies and general shape of our objects. """ + alias Pleroma.Emoji alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + def create(actor, object, recipients) do + {:ok, + %{ + "id" => Utils.generate_activity_id(), + "actor" => actor.ap_id, + "to" => recipients, + "object" => object, + "type" => "Create", + "published" => DateTime.utc_now() |> DateTime.to_iso8601() + }, []} + end + + def chat_message(actor, recipient, content) do + {:ok, + %{ + "id" => Utils.generate_object_id(), + "actor" => actor.ap_id, + "type" => "ChatMessage", + "to" => [recipient], + "content" => content, + "published" => DateTime.utc_now() |> DateTime.to_iso8601(), + "emoji" => Emoji.Formatter.get_emoji_map(content) + }, []} + end + @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()} def like(actor, object) do object_actor = User.get_cached_by_ap_id(object.data["actor"]) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index dc4bce059..20c7cceb6 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -11,26 +11,71 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Object alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.Types @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} def validate(object, meta) def validate(%{"type" => "Like"} = object, meta) do with {:ok, object} <- - object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do - object = stringify_keys(object |> Map.from_struct()) + object + |> LikeValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) {:ok, object, meta} end end + def validate(%{"type" => "ChatMessage"} = object, meta) do + with {:ok, object} <- + object + |> ChatMessageValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + + def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do + with {:ok, object_data} <- cast_and_apply(object), + meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), + {:ok, create_activity} <- + create_activity + |> CreateChatMessageValidator.cast_and_validate(meta) + |> Ecto.Changeset.apply_action(:insert) do + create_activity = stringify_keys(create_activity) + {:ok, create_activity, meta} + end + end + + def cast_and_apply(%{"type" => "ChatMessage"} = object) do + ChatMessageValidator.cast_and_apply(object) + end + + def cast_and_apply(o), do: {:error, {:validator_not_set, o}} + + def stringify_keys(%{__struct__: _} = object) do + object + |> Map.from_struct() + |> stringify_keys + end + def stringify_keys(object) do object |> Map.new(fn {key, val} -> {to_string(key), val} end) end + def fetch_actor(object) do + with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do + User.get_or_fetch_by_ap_id(actor) + end + end + def fetch_actor_and_object(object) do - User.get_or_fetch_by_ap_id(object["actor"]) + fetch_actor(object) Object.normalize(object["object"]) :ok end diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex new file mode 100644 index 000000000..e87c1ac2e --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -0,0 +1,98 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do + use Ecto.Schema + + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectValidators.Types + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1] + + @primary_key false + @derive Jason.Encoder + + embedded_schema do + field(:id, Types.ObjectID, primary_key: true) + field(:to, Types.Recipients, default: []) + field(:type, :string) + field(:content, Types.SafeText) + field(:actor, Types.ObjectID) + field(:published, Types.DateTime) + field(:emoji, :map, default: %{}) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + def fix(data) do + data + |> fix_emoji() + |> Map.put_new("actor", data["attributedTo"]) + end + + def changeset(struct, data) do + data = fix(data) + + struct + |> cast(data, __schema__(:fields)) + end + + def validate_data(data_cng) do + data_cng + |> validate_inclusion(:type, ["ChatMessage"]) + |> validate_required([:id, :actor, :to, :type, :content, :published]) + |> validate_length(:to, is: 1) + |> validate_length(:content, max: Pleroma.Config.get([:instance, :remote_limit])) + |> validate_local_concern() + end + + @doc """ + Validates the following + - If both users are in our system + - If at least one of the users in this ChatMessage is a local user + - If the recipient is not blocking the actor + """ + def validate_local_concern(cng) do + with actor_ap <- get_field(cng, :actor), + {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)}, + {_, %User{} = recipient} <- + {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())}, + {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)}, + {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do + cng + else + {:blocking_actor?, true} -> + cng + |> add_error(:actor, "actor is blocked by recipient") + + {:local?, false} -> + cng + |> add_error(:actor, "actor and recipient are both remote") + + {:find_actor, _} -> + cng + |> add_error(:actor, "can't find user") + + {:find_recipient, _} -> + cng + |> add_error(:to, "can't find user") + end + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex index b479c3918..02f3a6438 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -8,7 +8,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do alias Pleroma.Object alias Pleroma.User - def validate_actor_presence(cng, field_name \\ :actor) do + def validate_actor_presence(cng) do + validate_user_presence(cng, :actor) + end + + def validate_user_presence(cng, field_name) do cng |> validate_change(field_name, fn field_name, actor -> if User.get_cached_by_ap_id(actor) do diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex new file mode 100644 index 000000000..fc582400b --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +# NOTES +# - Can probably be a generic create validator +# - doesn't embed, will only get the object id +defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do + use Ecto.Schema + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ObjectValidators.Types + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + + @primary_key false + + embedded_schema do + field(:id, Types.ObjectID, primary_key: true) + field(:actor, Types.ObjectID) + field(:type, :string) + field(:to, Types.Recipients, default: []) + field(:object, Types.ObjectID) + 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) + |> validate_data(meta) + end + + def validate_data(cng, meta \\ []) do + cng + |> validate_required([:id, :actor, :to, :type, :object]) + |> validate_inclusion(:type, ["Create"]) + |> validate_actor_presence() + |> validate_recipients_match(meta) + |> validate_actors_match(meta) + |> validate_object_nonexistence() + end + + def validate_object_nonexistence(cng) do + cng + |> validate_change(:object, fn :object, object_id -> + if Object.get_cached_by_ap_id(object_id) do + [{:object, "The object to create already exists"}] + else + [] + end + end) + end + + def validate_actors_match(cng, meta) do + object_actor = meta[:object_data]["actor"] + + cng + |> validate_change(:actor, fn :actor, actor -> + if actor == object_actor do + [] + else + [{:actor, "Actor doesn't match with object actor"}] + end + end) + end + + def validate_recipients_match(cng, meta) do + object_recipients = meta[:object_data]["to"] || [] + + cng + |> validate_change(:to, fn :to, recipients -> + activity_set = MapSet.new(recipients) + object_set = MapSet.new(object_recipients) + + if MapSet.equal?(activity_set, object_set) do + [] + else + [{:to, "Recipients don't match with object recipients"}] + end + end) + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex index 926804ce7..926804ce7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex new file mode 100644 index 000000000..408e0f6ee --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex @@ -0,0 +1,36 @@ +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do + use Ecto.Type + + alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID + + def type, do: {:array, ObjectID} + + def cast(object) when is_binary(object) do + cast([object]) + end + + def cast(data) when is_list(data) do + data + |> Enum.reduce_while({:ok, []}, fn element, {:ok, list} -> + case ObjectID.cast(element) do + {:ok, id} -> + {:cont, {:ok, [id | list]}} + + _ -> + {:halt, :error} + end + end) + end + + def cast(_) do + :error + end + + def dump(data) do + {:ok, data} + end + + def load(data) do + {:ok, data} + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex new file mode 100644 index 000000000..822e8d2c1 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText do + use Ecto.Type + + alias Pleroma.HTML + + def type, do: :string + + def cast(str) when is_binary(str) do + {:ok, HTML.strip_tags(str)} + end + + def cast(_), do: :error + + def dump(data) do + {:ok, data} + end + + def load(data) do + {:ok, data} + end +end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 6a8f1af96..e394c75d7 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -5,8 +5,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do liked object, a `Follow` activity will add the user to the follower collection, and so on. """ + alias Pleroma.Chat alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils def handle(object, meta \\ []) @@ -23,8 +27,42 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do {:ok, object, meta} end + # Tasks this handles + # - Actually create object + # - Rollback if we couldn't create it + # - Set up notifications + def handle(%{data: %{"type" => "Create"}} = activity, meta) do + with {:ok, _object, _meta} <- handle_object_creation(meta[:object_data], meta) do + Notification.create_notifications(activity) + {:ok, activity, meta} + else + e -> Repo.rollback(e) + end + end + # Nothing to do def handle(object, meta) do {:ok, object, meta} end + + def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do + with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do + actor = User.get_cached_by_ap_id(object.data["actor"]) + recipient = User.get_cached_by_ap_id(hd(object.data["to"])) + + [[actor, recipient], [recipient, actor]] + |> Enum.each(fn [user, other_user] -> + if user.local do + Chat.bump_or_create(user.id, other_user.ap_id) + end + end) + + {:ok, object, meta} + end + end + + # Nothing to do + def handle_object_creation(object) do + {:ok, object} + end end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 3a4d364e7..40a505b91 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -656,6 +656,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> handle_incoming(options) end + def handle_incoming( + %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, + _options + ) do + with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), + {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do + {:ok, activity} + end + end + def handle_incoming(%{"type" => "Like"} = data, _options) do with :ok <- ObjectValidator.fetch_actor_and_object(data), {:ok, activity, _meta} <- |