From: lain Date: Wed, 6 May 2020 15:36:08 +0000 (+0200) Subject: Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms X-Git-Url: https://git.squeep.com/?a=commitdiff_plain;h=1748e2694815da20689005110937ea42cd554d70;p=akkoma Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms --- 1748e2694815da20689005110937ea42cd554d70 diff --cc lib/pleroma/web/activity_pub/builder.ex index 67e65c7b9,1345a3a3e..6e3a375e7 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@@ -11,42 -10,33 +11,69 @@@ defmodule Pleroma.Web.ActivityPub.Build alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()} + def delete(actor, object_id) do + object = Object.normalize(object_id, false) + + user = !object && User.get_cached_by_ap_id(object_id) + + to = + case {object, user} do + {%Object{}, _} -> + # We are deleting an object, address everyone who was originally mentioned + (object.data["to"] || []) ++ (object.data["cc"] || []) + + {_, %User{follower_address: follower_address}} -> + # We are deleting a user, address the followers of that user + [follower_address] + end + + {:ok, + %{ + "id" => Utils.generate_activity_id(), + "actor" => actor.ap_id, + "object" => object_id, + "to" => to, + "type" => "Delete" + }, []} + end + + 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, opts \\ []) do + basic = %{ + "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) + } + + case opts[:attachment] do + %Object{data: attachment_data} -> + { + :ok, + Map.put(basic, "attachment", attachment_data), + [] + } + + _ -> + {:ok, basic, []} + end + 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 --cc lib/pleroma/web/activity_pub/object_validator.ex index d6c14f7b8,479f922f5..cc5ca1d9e --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@@ -11,20 -11,27 +11,31 @@@ defmodule Pleroma.Web.ActivityPub.Objec alias Pleroma.Object alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator 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" => "Delete"} = object, meta) do + with cng <- DeleteValidator.cast_and_validate(object), + do_not_federate <- DeleteValidator.do_not_federate?(cng), + {:ok, object} <- Ecto.Changeset.apply_action(cng, :insert) do + object = stringify_keys(object) + meta = Keyword.put(meta, :do_not_federate, do_not_federate) + {:ok, object, meta} + end + end + 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 diff --cc lib/pleroma/web/activity_pub/side_effects.ex index e394c75d7,7b53abeaf..8bdc433ff --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@@ -5,12 -5,10 +5,13 @@@ defmodule Pleroma.Web.ActivityPub.SideE 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.ActivityPub + alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils def handle(object, meta \\ []) @@@ -27,19 -25,49 +28,62 @@@ {: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 + + # Tasks this handles: + # - Delete and unpins the create activity + # - Replace object with Tombstone + # - Set up notification + # - Reduce the user note count + # - Reduce the reply count + # - Stream out the activity + def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do + deleted_object = + Object.normalize(deleted_object, false) || User.get_cached_by_ap_id(deleted_object) + + result = + case deleted_object do + %Object{} -> + with {:ok, deleted_object, activity} <- Object.delete(deleted_object), + %User{} = user <- User.get_cached_by_ap_id(deleted_object.data["actor"]) do + User.remove_pinnned_activity(user, activity) + + {:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object) + + if in_reply_to = deleted_object.data["inReplyTo"] do + Object.decrease_replies_count(in_reply_to) + end + + ActivityPub.stream_out(object) + ActivityPub.stream_out_participations(deleted_object, user) + :ok + end + + %User{} -> + with {:ok, _} <- User.delete(deleted_object) do + :ok + end + end + + if result == :ok do + Notification.create_notifications(object) + {:ok, object, meta} + else + {:error, result} + end + end + # Nothing to do def handle(object, meta) do {:ok, object, meta} diff --cc test/web/activity_pub/object_validator_test.exs index fcc54c8a1,744c46781..c9eace866 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@@ -12,150 -10,98 +12,242 @@@ defmodule Pleroma.Web.ActivityPub.Objec import Pleroma.Factory + describe "attachments" do + test "it turns mastodon attachments into our attachments" do + attachment = %{ + "url" => + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + "type" => "Document", + "name" => nil, + "mediaType" => "image/jpeg" + } + + {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert [ + %{ + href: + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + type: "Link", + mediaType: "image/jpeg" + } + ] = attachment.url + end + end + + describe "chat message create activities" do + test "it is invalid if the object already exists" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey") + object = Object.normalize(activity, false) + + {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:object, {"The object to create already exists", []}} in cng.errors + end + + test "it is invalid if the object data has a different `to` or `actor` field" do + user = insert(:user) + recipient = insert(:user) + {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey") + + {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors + assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors + end + end + + describe "chat messages" do + setup do + clear_config([:instance, :remote_limit]) + user = insert(:user) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:") + + %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} + end + + test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert Map.put(valid_chat_message, "attachment", nil) == object + end + + test "validates for a basic object with an attachment", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "does not validate if the message is longer than the remote_limit", %{ + valid_chat_message: valid_chat_message + } do + Pleroma.Config.put([:instance, :remote_limit], 2) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the recipient is blocking the actor", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + Pleroma.User.block(recipient, user) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the actor or the recipient is not in our system", %{ + valid_chat_message: valid_chat_message + } do + chat_message = + valid_chat_message + |> Map.put("actor", "https://raymoo.com/raymoo") + + {:error, _} = ObjectValidator.validate(chat_message, []) + + chat_message = + valid_chat_message + |> Map.put("to", ["https://raymoo.com/raymoo"]) + + {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate for a message with multiple recipients", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + chat_message = + valid_chat_message + |> Map.put("to", [user.ap_id, recipient.ap_id]) + + assert {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate if it doesn't concern local users" do + user = insert(:user, local: false) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") + assert {:error, _} = ObjectValidator.validate(valid_chat_message, []) + end + end + + describe "deletes" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{"status" => "cancel me daddy"}) + + {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"]) + {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id) + + %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete} + end + + test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do + {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, []) + + assert valid_post_delete["deleted_activity_id"] + end + + test "it is invalid if the object isn't in a list of certain types", %{ + valid_post_delete: valid_post_delete + } do + object = Object.get_by_ap_id(valid_post_delete["object"]) + + data = + object.data + |> Map.put("type", "Like") + + {:ok, _object} = + object + |> Ecto.Changeset.change(%{data: data}) + |> Object.update_and_set_cache() + + {:error, cng} = ObjectValidator.validate(valid_post_delete, []) + assert {:object, {"object not in allowed types", []}} in cng.errors + end + + test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do + assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, [])) + end + + test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do + no_id = + valid_post_delete + |> Map.delete("id") + + {:error, cng} = ObjectValidator.validate(no_id, []) + + assert {:id, {"can't be blank", [validation: :required]}} in cng.errors + end + + test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do + missing_object = + valid_post_delete + |> Map.put("object", "http://does.not/exist") + + {:error, cng} = ObjectValidator.validate(missing_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + end + + test "it's invalid if the actor of the object and the actor of delete are from different domains", + %{valid_post_delete: valid_post_delete} do + valid_user = insert(:user) + + valid_other_actor = + valid_post_delete + |> Map.put("actor", valid_user.ap_id) + + assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, [])) + + invalid_other_actor = + valid_post_delete + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + {:error, cng} = ObjectValidator.validate(invalid_other_actor, []) + + assert {:actor, {"is not allowed to delete object", []}} in cng.errors + end + + test "it's valid if the actor of the object is a local superuser", + %{valid_post_delete: valid_post_delete} do + user = + insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo") + + valid_other_actor = + valid_post_delete + |> Map.put("actor", user.ap_id) + + {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, []) + assert meta[:do_not_federate] + end + end + describe "likes" do setup do user = insert(:user) diff --cc test/web/activity_pub/side_effects_test.exs index 19abac6a6,a9598d7b3..a631e5c6b --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@@ -3,9 -3,10 +3,11 @@@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.SideEffectsTest do + use Oban.Testing, repo: Pleroma.Repo use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Chat alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo diff --cc test/web/common_api/common_api_test.exs index 58ffda72f,62a2665b6..ef7c479c0 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@@ -22,55 -23,84 +24,133 @@@ defmodule Pleroma.Web.CommonAPITest d setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :max_pinned_statuses]) + describe "posting chat messages" do + setup do: clear_config([:instance, :chat_limit]) + + test "it posts a chat message" do + author = insert(:user) + recipient = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "a test message :firefox:" + ) + + assert activity.data["type"] == "Create" + assert activity.local + object = Object.normalize(activity) + + assert object.data["type"] == "ChatMessage" + assert object.data["to"] == [recipient.ap_id] + + assert object.data["content"] == + "a test message <script>alert('uuu')</script> :firefox:" + + assert object.data["emoji"] == %{ + "firefox" => "http://localhost:4001/emoji/Firefox.gif" + } + + assert Chat.get(author.id, recipient.ap_id) + assert Chat.get(recipient.id, author.ap_id) + end + + test "it reject messages over the local limit" do + Pleroma.Config.put([:instance, :chat_limit], 2) + + author = insert(:user) + recipient = insert(:user) + + {:error, message} = + CommonAPI.post_chat_message( + author, + recipient, + "123" + ) + + assert message == :content_too_long + end + end + + describe "deletion" do + test "it allows users to delete their posts" do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"}) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, delete} = CommonAPI.delete(post.id, user) + assert delete.local + assert called(Pleroma.Web.Federator.publish(delete)) + end + + refute Activity.get_by_id(post.id) + end + + test "it does not allow a user to delete their posts" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"}) + + assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user) + assert Activity.get_by_id(post.id) + end + + test "it allows moderators to delete other user's posts" do + user = insert(:user) + moderator = insert(:user, is_moderator: true) + + {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"}) + + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + + refute Activity.get_by_id(post.id) + end + + test "it allows admins to delete other user's posts" do + user = insert(:user) + moderator = insert(:user, is_admin: true) + + {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"}) + + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + + refute Activity.get_by_id(post.id) + end + + test "superusers deleting non-local posts won't federate the delete" do + # This is the user of the ingested activity + _user = + insert(:user, + local: false, + ap_id: "http://mastodon.example.org/users/admin", + last_refreshed_at: NaiveDateTime.utc_now() + ) + + moderator = insert(:user, is_admin: true) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Jason.decode!() + + {:ok, post} = Transmogrifier.handle_incoming(data) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + refute called(Pleroma.Web.Federator.publish(:_)) + end + + refute Activity.get_by_id(post.id) + end + end + test "favoriting race condition" do user = insert(:user) users_serial = insert_list(10, :user)