From b777083f3f396a7d8c357ec968f72679befc691c Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 25 Oct 2019 19:14:18 +0700 Subject: [PATCH 1/9] Add `also_known_as` field to Pleroma.User --- lib/pleroma/user.ex | 10 ++++-- lib/pleroma/web/activity_pub/activity_pub.ex | 3 +- .../web/activity_pub/transmogrifier.ex | 2 +- ...91025081729_add_also_known_as_to_users.exs | 9 ++++++ priv/static/schemas/litepub-0.1.jsonld | 4 +++ .../tesla_mock/admin@mastdon.example.org.json | 9 ++++-- test/web/activity_pub/transmogrifier_test.exs | 31 +++++++++++++++++++ 7 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 priv/repo/migrations/20191025081729_add_also_known_as_to_users.exs diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 7bef6e281..f25345140 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -105,6 +105,7 @@ defmodule Pleroma.User do field(:discoverable, :boolean, default: false) field(:invisible, :boolean, default: false) field(:skip_thread_containment, :boolean, default: false) + field(:also_known_as, {:array, :string}, default: []) field(:notification_settings, :map, default: %{ @@ -324,7 +325,8 @@ def remote_user_creation(params) do :fields, :following_count, :discoverable, - :invisible + :invisible, + :also_known_as ] ) |> validate_required([:name, :ap_id]) @@ -373,7 +375,8 @@ def update_changeset(struct, params \\ %{}) do :fields, :raw_fields, :pleroma_settings_store, - :discoverable + :discoverable, + :also_known_as ] ) |> unique_constraint(:nickname) @@ -413,7 +416,8 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do :hide_followers, :discoverable, :hide_followers_count, - :hide_follows_count + :hide_follows_count, + :also_known_as ] ) |> unique_constraint(:nickname) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 07dde3537..dc962673c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1117,7 +1117,8 @@ defp object_to_user_data(data) do name: data["name"], follower_address: data["followers"], following_address: data["following"], - bio: data["summary"] + bio: data["summary"], + also_known_as: Map.get(data, "alsoKnownAs", []) } # nickname can be nil because of virtual actors diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 9b3ee842b..6ada38e1a 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -616,7 +616,7 @@ def handle_incoming( update_data = new_user_data - |> Map.take([:avatar, :banner, :bio, :name]) + |> Map.take([:avatar, :banner, :bio, :name, :also_known_as]) |> Map.put(:fields, fields) |> Map.put(:locked, locked) |> Map.put(:invisible, invisible) diff --git a/priv/repo/migrations/20191025081729_add_also_known_as_to_users.exs b/priv/repo/migrations/20191025081729_add_also_known_as_to_users.exs new file mode 100644 index 000000000..3d9e0a3cf --- /dev/null +++ b/priv/repo/migrations/20191025081729_add_also_known_as_to_users.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddAlsoKnownAsToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add(:also_known_as, {:array, :string}, default: []) + end + end +end diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index c8e69cab5..509df4bb7 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -28,6 +28,10 @@ "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" + }, + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" } } ] diff --git a/test/fixtures/tesla_mock/admin@mastdon.example.org.json b/test/fixtures/tesla_mock/admin@mastdon.example.org.json index 8159dc20a..9fdd6557c 100644 --- a/test/fixtures/tesla_mock/admin@mastdon.example.org.json +++ b/test/fixtures/tesla_mock/admin@mastdon.example.org.json @@ -9,7 +9,11 @@ "inReplyToAtomUri": "ostatus:inReplyToAtomUri", "conversation": "ostatus:conversation", "toot": "http://joinmastodon.org/ns#", - "Emoji": "toot:Emoji" + "Emoji": "toot:Emoji", + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + } }], "id": "http://mastodon.example.org/users/admin", "type": "Person", @@ -50,5 +54,6 @@ "type": "Image", "mediaType": "image/png", "url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" - } + }, + "alsoKnownAs": ["http://example.org/users/foo"] } diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 6f7e1da1f..8fd2f5053 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -592,6 +592,37 @@ test "it works for incoming update activities" do assert user.bio == "

Some bio

" end + test "it works with alsoKnownAs" do + {:ok, %Activity{data: %{"actor" => actor}}} = + "test/fixtures/mastodon-post-activity.json" + |> File.read!() + |> Poison.decode!() + |> Transmogrifier.handle_incoming() + + assert User.get_cached_by_ap_id(actor).also_known_as == ["http://example.org/users/foo"] + + {:ok, _activity} = + "test/fixtures/mastodon-update.json" + |> File.read!() + |> Poison.decode!() + |> Map.put("actor", actor) + |> Map.update!("object", fn object -> + object + |> Map.put("actor", actor) + |> Map.put("id", actor) + |> Map.put("alsoKnownAs", [ + "http://mastodon.example.org/users/foo", + "http://example.org/users/bar" + ]) + end) + |> Transmogrifier.handle_incoming() + + assert User.get_cached_by_ap_id(actor).also_known_as == [ + "http://mastodon.example.org/users/foo", + "http://example.org/users/bar" + ] + end + test "it works with custom profile fields" do {:ok, activity} = "test/fixtures/mastodon-post-activity.json" From 61fc739ab8917ccb5a12d6ab6db6130dc231990b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 30 Oct 2019 18:21:49 +0700 Subject: [PATCH 2/9] Handle "Move" activity --- lib/pleroma/following_relationship.ex | 24 ++++++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 24 ++++++++++ .../web/activity_pub/transmogrifier.ex | 18 +++++++ lib/pleroma/web/activity_pub/visibility.ex | 1 + lib/pleroma/workers/background_worker.ex | 7 +++ test/web/activity_pub/activity_pub_test.exs | 48 +++++++++++++++++++ test/web/activity_pub/transmogrifier_test.exs | 24 ++++++++++ 7 files changed, 146 insertions(+) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 2ffac17ee..2f89eb4cf 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -107,4 +107,28 @@ def following(%User{} = user) do [user.follower_address | following] end end + + def move_following(origin, target) do + following_relationships = + __MODULE__ + |> where(following_id: ^origin.id) + |> preload([:follower]) + |> limit(50) + |> Repo.all() + + case following_relationships do + [] -> + :ok + + following_relationships -> + Enum.each(following_relationships, fn following_relationship -> + Repo.transaction(fn -> + Repo.delete(following_relationship) + User.follow(following_relationship.follower, target) + end) + end) + + move_following(origin, target) + end + end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 6adadacdc..67c2cb659 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -514,6 +514,30 @@ def flag( end end + def move(%User{} = origin, %User{} = target, local \\ true) do + params = %{ + "type" => "Move", + "actor" => origin.ap_id, + "object" => origin.ap_id, + "target" => target.ap_id + } + + with true <- origin.ap_id in target.also_known_as, + {:ok, activity} <- insert(params, local) do + maybe_federate(activity) + + BackgroundWorker.enqueue("move_following", %{ + "origin_id" => origin.id, + "target_id" => target.id + }) + + {:ok, activity} + else + false -> {:error, "Target account must have the origin in `alsoKnownAs`"} + err -> err + end + end + defp fetch_activities_for_context_query(context, opts) do public = [Pleroma.Constants.as_public()] diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index c23f2dcd0..78ee6192a 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -785,6 +785,24 @@ def handle_incoming( end end + def handle_incoming( + %{ + "type" => "Move", + "actor" => origin_actor, + "object" => origin_actor, + "target" => target_actor + }, + _options + ) do + with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor), + {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor), + true <- origin_actor in target_user.also_known_as do + ActivityPub.move(origin_user, target_user, false) + else + _e -> :error + end + end + def handle_incoming(_, _), do: :error @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index cd4097493..e172f6d3f 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do @spec is_public?(Object.t() | Activity.t() | map()) :: boolean() def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false def is_public?(%Object{data: data}), do: is_public?(data) + def is_public?(%Activity{data: %{"type" => "Move"}}), do: true def is_public?(%Activity{data: data}), do: is_public?(data) def is_public?(%{"directMessage" => true}), do: false def is_public?(data), do: Utils.label_in_message?(Pleroma.Constants.as_public(), data) diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index 7ffc8eabe..323a4da1e 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -71,4 +71,11 @@ def perform(%{"op" => "fetch_data_for_activity", "activity_id" => activity_id}, activity = Activity.get_by_id(activity_id) Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity) end + + def perform(%{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}, _) do + origin = User.get_cached_by_id(origin_id) + target = User.get_cached_by_id(target_id) + + Pleroma.FollowingRelationship.move_following(origin, target) + end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 0e5eb0c50..3f79593e5 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -4,6 +4,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + alias Pleroma.Activity alias Pleroma.Builders.ActivityBuilder alias Pleroma.Object @@ -1420,4 +1422,50 @@ test "detects hidden follows" do assert follow_info.hide_follows == true end end + + describe "Move activity" do + test "create" do + %{ap_id: old_ap_id} = old_user = insert(:user) + %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) + follower = insert(:user) + + User.follow(follower, old_user) + + assert User.following?(follower, old_user) + + assert {:ok, activity} = ActivityPub.move(old_user, new_user) + + assert %Activity{ + actor: ^old_ap_id, + data: %{ + "actor" => ^old_ap_id, + "object" => ^old_ap_id, + "target" => ^new_ap_id, + "type" => "Move" + }, + local: true + } = activity + + params = %{ + "op" => "move_following", + "origin_id" => old_user.id, + "target_id" => new_user.id + } + + assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params) + + Pleroma.Workers.BackgroundWorker.perform(params, nil) + + refute User.following?(follower, old_user) + assert User.following?(follower, new_user) + end + + test "old user must be in the new user's `also_known_as` list" do + old_user = insert(:user) + new_user = insert(:user) + + assert {:error, "Target account must have the origin in `alsoKnownAs`"} = + ActivityPub.move(old_user, new_user) + end + end end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index b36adcf9b..76e7ad8ea 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -1181,6 +1181,30 @@ test "it correctly processes messages with non-array cc field" do assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] assert [user.follower_address] == activity.data["to"] end + + test "it accepts Move activities" do + old_user = insert(:user) + new_user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Move", + "actor" => old_user.ap_id, + "object" => old_user.ap_id, + "target" => new_user.ap_id + } + + assert :error = Transmogrifier.handle_incoming(message) + + {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]}) + + assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message) + assert activity.actor == old_user.ap_id + assert activity.data["actor"] == old_user.ap_id + assert activity.data["object"] == old_user.ap_id + assert activity.data["target"] == new_user.ap_id + assert activity.data["type"] == "Move" + end end describe "prepare outgoing" do From e6d7e27bd603806e96dfc2774f90cadb3cf73a8c Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 12 Nov 2019 18:36:50 +0700 Subject: [PATCH 3/9] Add `allow_following_move` setting to User --- docs/API/differences_in_mastoapi_responses.md | 2 ++ lib/pleroma/following_relationship.ex | 34 ++++++++----------- lib/pleroma/user.ex | 3 ++ .../controllers/account_controller.ex | 1 + .../web/mastodon_api/views/account_view.ex | 7 ++++ ...91025081729_add_also_known_as_to_users.exs | 9 ----- ...191025081729_add_move_support_to_users.exs | 10 ++++++ test/web/activity_pub/activity_pub_test.exs | 6 ++++ .../update_credentials_test.exs | 15 ++++++++ .../mastodon_api/views/account_view_test.exs | 2 +- 10 files changed, 59 insertions(+), 30 deletions(-) delete mode 100644 priv/repo/migrations/20191025081729_add_also_known_as_to_users.exs create mode 100644 priv/repo/migrations/20191025081729_add_move_support_to_users.exs diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index aca0f5e0e..41965f872 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -57,6 +57,7 @@ Has these additional fields under the `pleroma` object: - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials` - `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials` - `deactivated`: boolean, true when the user is deactivated +- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts - `unread_conversation_count`: The count of unread conversations. Only returned to the account owner. ### Source @@ -130,6 +131,7 @@ Additional parameters can be added to the JSON body/Form data: - `default_scope` - the scope returned under `privacy` key in Source subentity - `pleroma_settings_store` - Opaque user settings to be saved on the backend. - `skip_thread_containment` - if true, skip filtering out broken threads +- `allow_following_move` - if true, allows automatically follow moved following accounts - `pleroma_background_image` - sets the background image of the user. ### Pleroma Settings Store diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 2f89eb4cf..40538f7bf 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -109,26 +109,20 @@ def following(%User{} = user) do end def move_following(origin, target) do - following_relationships = - __MODULE__ - |> where(following_id: ^origin.id) - |> preload([:follower]) - |> limit(50) - |> Repo.all() - - case following_relationships do - [] -> - :ok - - following_relationships -> - Enum.each(following_relationships, fn following_relationship -> - Repo.transaction(fn -> - Repo.delete(following_relationship) - User.follow(following_relationship.follower, target) - end) - end) - - move_following(origin, target) + __MODULE__ + |> join(:inner, [r], f in assoc(r, :follower)) + |> where(following_id: ^origin.id) + |> where([r, f], f.allow_following_move == true) + |> limit(50) + |> preload([:follower]) + |> Repo.all() + |> Enum.map(fn following_relationship -> + Repo.delete(following_relationship) + Pleroma.Web.CommonAPI.follow(following_relationship.follower, target) + end) + |> case do + [] -> :ok + _ -> move_following(origin, target) end end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 8715b37de..d40f6ed08 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -104,6 +104,7 @@ defmodule Pleroma.User do field(:raw_fields, {:array, :map}, default: []) field(:discoverable, :boolean, default: false) field(:invisible, :boolean, default: false) + field(:allow_following_move, :boolean, default: true) field(:skip_thread_containment, :boolean, default: false) field(:also_known_as, {:array, :string}, default: []) @@ -314,6 +315,7 @@ def update_changeset(struct, params \\ %{}) do :hide_followers_count, :hide_follows_count, :hide_favorites, + :allow_following_move, :background, :show_role, :skip_thread_containment, @@ -359,6 +361,7 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do :hide_follows, :fields, :hide_followers, + :allow_following_move, :discoverable, :hide_followers_count, :hide_follows_count, diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 73fad519e..7df7dc097 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -152,6 +152,7 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do :hide_favorites, :show_role, :skip_thread_containment, + :allow_following_move, :discoverable ] |> Enum.reduce(%{}, fn key, acc -> diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index e30fed610..7aae7d188 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -163,6 +163,7 @@ defp do_render("show.json", %{user: user} = opts) do |> maybe_put_chat_token(user, opts[:for], opts) |> maybe_put_activation_status(user, opts[:for]) |> maybe_put_follow_requests_count(user, opts[:for]) + |> maybe_put_allow_following_move(user, opts[:for]) |> maybe_put_unread_conversation_count(user, opts[:for]) end @@ -239,6 +240,12 @@ defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: defp maybe_put_notification_settings(data, _, _), do: data + defp maybe_put_allow_following_move(data, %User{id: user_id} = user, %User{id: user_id}) do + Kernel.put_in(data, [:pleroma, :allow_following_move], user.allow_following_move) + end + + defp maybe_put_allow_following_move(data, _, _), do: data + defp maybe_put_activation_status(data, user, %User{is_admin: true}) do Kernel.put_in(data, [:pleroma, :deactivated], user.deactivated) end diff --git a/priv/repo/migrations/20191025081729_add_also_known_as_to_users.exs b/priv/repo/migrations/20191025081729_add_also_known_as_to_users.exs deleted file mode 100644 index 3d9e0a3cf..000000000 --- a/priv/repo/migrations/20191025081729_add_also_known_as_to_users.exs +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Pleroma.Repo.Migrations.AddAlsoKnownAsToUsers do - use Ecto.Migration - - def change do - alter table(:users) do - add(:also_known_as, {:array, :string}, default: []) - end - end -end diff --git a/priv/repo/migrations/20191025081729_add_move_support_to_users.exs b/priv/repo/migrations/20191025081729_add_move_support_to_users.exs new file mode 100644 index 000000000..580b9eb0f --- /dev/null +++ b/priv/repo/migrations/20191025081729_add_move_support_to_users.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.AddMoveSupportToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add(:also_known_as, {:array, :string}, default: [], null: false) + add(:allow_following_move, :boolean, default: true, null: false) + end + end +end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 3f79593e5..6c5e5d38e 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1428,10 +1428,13 @@ test "create" do %{ap_id: old_ap_id} = old_user = insert(:user) %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) follower = insert(:user) + follower_move_opted_out = insert(:user, allow_following_move: false) User.follow(follower, old_user) + User.follow(follower_move_opted_out, old_user) assert User.following?(follower, old_user) + assert User.following?(follower_move_opted_out, old_user) assert {:ok, activity} = ActivityPub.move(old_user, new_user) @@ -1458,6 +1461,9 @@ test "create" do refute User.following?(follower, old_user) assert User.following?(follower, new_user) + + assert User.following?(follower_move_opted_out, old_user) + refute User.following?(follower_move_opted_out, new_user) end test "old user must be in the new user's `also_known_as` list" do diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs index 519b56d6c..77cfce4fa 100644 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -103,6 +103,21 @@ test "updates the user's locking status", %{conn: conn} do assert user["locked"] == true end + test "updates the user's allow_following_move", %{conn: conn} do + user = insert(:user) + + assert user.allow_following_move == true + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{allow_following_move: "false"}) + + assert refresh_record(user).allow_following_move == false + assert user = json_response(conn, 200) + assert user["pleroma"]["allow_following_move"] == false + end + test "updates the user's default scope", %{conn: conn} do user = insert(:user) diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index af88841ed..3aa5d2e3b 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -102,7 +102,7 @@ test "Represent the user account for the account owner" do privacy = user.default_scope assert %{ - pleroma: %{notification_settings: ^notification_settings}, + pleroma: %{notification_settings: ^notification_settings, allow_following_move: true}, source: %{privacy: ^privacy} } = AccountView.render("show.json", %{user: user, for: user}) end From 27cd1374e3c1768a1a0b8abb40d5b56e6e021d2d Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 12 Nov 2019 18:48:14 +0700 Subject: [PATCH 4/9] Add a notification for Move activities --- lib/pleroma/notification.ex | 33 ++++++++++----------- lib/pleroma/web/common_api/utils.ex | 21 +++++++++---- test/web/activity_pub/activity_pub_test.exs | 9 ++++++ 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index b7ecf51e4..f37e7ec67 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -251,10 +251,13 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act end end - def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) - when type in ["Like", "Announce", "Follow"] do - users = get_notified_from_activity(activity) - notifications = Enum.map(users, fn user -> create_notification(activity, user) end) + def create_notifications(%Activity{data: %{"type" => type}} = activity) + when type in ["Like", "Announce", "Follow", "Move"] do + notifications = + activity + |> get_notified_from_activity() + |> Enum.map(&create_notification(activity, &1)) + {:ok, notifications} end @@ -276,19 +279,15 @@ def create_notification(%Activity{} = activity, %User{} = user) do def get_notified_from_activity(activity, local_only \\ true) - def get_notified_from_activity( - %Activity{data: %{"to" => _, "type" => type} = _data} = activity, - local_only - ) - when type in ["Create", "Like", "Announce", "Follow"] do - recipients = - [] - |> Utils.maybe_notify_to_recipients(activity) - |> Utils.maybe_notify_mentioned_recipients(activity) - |> Utils.maybe_notify_subscribers(activity) - |> Enum.uniq() - - User.get_users_from_set(recipients, local_only) + def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) + when type in ["Create", "Like", "Announce", "Follow", "Move"] do + [] + |> Utils.maybe_notify_to_recipients(activity) + |> Utils.maybe_notify_mentioned_recipients(activity) + |> Utils.maybe_notify_subscribers(activity) + |> Utils.maybe_notify_followers(activity) + |> Enum.uniq() + |> User.get_users_from_set(local_only) end def get_notified_from_activity(_, _local_only), do: [] diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 88a5f434a..43b67d0f0 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -451,6 +451,8 @@ def maybe_notify_to_recipients( recipients ++ to end + def maybe_notify_to_recipients(recipients, _), do: recipients + def maybe_notify_mentioned_recipients( recipients, %Activity{data: %{"to" => _to, "type" => type} = data} = activity @@ -484,12 +486,8 @@ def maybe_notify_subscribers(recipients, %Activity{ recipients end - def maybe_notify_subscribers( - recipients, - %Activity{data: %{"actor" => actor, "type" => type}} = activity - ) - when type == "Create" do - with %User{} = user <- User.get_cached_by_ap_id(actor) do + def maybe_notify_subscribers(recipients, %Activity{data: %{"type" => "Create"}} = activity) do + with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do subscriber_ids = user |> User.subscribers() @@ -502,6 +500,17 @@ def maybe_notify_subscribers( def maybe_notify_subscribers(recipients, _), do: recipients + def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do + with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do + user + |> User.get_followers() + |> Enum.map(& &1.ap_id) + |> Enum.concat(recipients) + end + end + + def maybe_notify_followers(recipients, _), do: recipients + def maybe_extract_mentions(%{"tag" => tag}) do tag |> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 6c5e5d38e..bbae3fcf1 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do alias Pleroma.Activity alias Pleroma.Builders.ActivityBuilder + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -1464,6 +1465,14 @@ test "create" do assert User.following?(follower_move_opted_out, old_user) refute User.following?(follower_move_opted_out, new_user) + + activity = %Activity{activity | object: nil} + + assert [%Notification{activity: ^activity}] = + Notification.for_user_since(follower, ~N[2019-04-13 11:22:33]) + + assert [%Notification{activity: ^activity}] = + Notification.for_user_since(follower_move_opted_out, ~N[2019-04-13 11:22:33]) end test "old user must be in the new user's `also_known_as` list" do From 63c493cd536155b98063be5cb680914413597312 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 12 Nov 2019 18:51:12 +0700 Subject: [PATCH 5/9] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51e5424c6..acfe4ceb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Admin API: `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array), `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body). - Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays - Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read +- ActivityPub: Support `Move` activities - Mastodon API: Add `/api/v1/markers` for managing timeline read markers ### Changed From 768c1a5de172151beb34e6dda13d4fb05e05ed87 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 12 Nov 2019 19:13:19 +0700 Subject: [PATCH 6/9] Fix tests --- lib/pleroma/web/common_api/utils.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 43b67d0f0..cbb64f8d2 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -486,8 +486,12 @@ def maybe_notify_subscribers(recipients, %Activity{ recipients end - def maybe_notify_subscribers(recipients, %Activity{data: %{"type" => "Create"}} = activity) do - with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do + def maybe_notify_subscribers( + recipients, + %Activity{data: %{"actor" => actor, "type" => type}} = activity + ) + when type == "Create" do + with %User{} = user <- User.get_cached_by_ap_id(actor) do subscriber_ids = user |> User.subscribers() From 1fc28a4b441fdc0de8b2b43e2b045da38ee50b0a Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 26 Nov 2019 18:48:56 +0700 Subject: [PATCH 7/9] Add a view for the move notification --- docs/API/differences_in_mastoapi_responses.md | 6 +++ lib/pleroma/activity.ex | 3 +- .../mastodon_api/views/notification_view.ex | 38 ++++++++----------- lib/pleroma/web/push/impl.ex | 2 +- test/notification_test.exs | 29 ++++++++++++++ .../views/notification_view_test.exs | 24 ++++++++++++ 6 files changed, 77 insertions(+), 25 deletions(-) diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index e649bdf24..006d17c1b 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -92,6 +92,12 @@ Has these additional fields under the `pleroma` object: - `is_seen`: true if the notification was read by the user +### Move Notification + +The `type` value is `move`. Has an additional field: + +- `target`: new account + ## GET `/api/v1/notifications` Accepts additional parameters: diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index c1065611b..992298c2d 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -28,7 +28,8 @@ defmodule Pleroma.Activity do "Create" => "mention", "Follow" => "follow", "Announce" => "reblog", - "Like" => "favourite" + "Like" => "favourite", + "Move" => "move" } @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types, diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 5e3dbe728..ddd7f5318 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -37,32 +37,24 @@ def render("show.json", %{ } case mastodon_type do - "mention" -> - response - |> Map.merge(%{ - status: StatusView.render("show.json", %{activity: activity, for: user}) - }) - - "favourite" -> - response - |> Map.merge(%{ - status: StatusView.render("show.json", %{activity: parent_activity, for: user}) - }) - - "reblog" -> - response - |> Map.merge(%{ - status: StatusView.render("show.json", %{activity: parent_activity, for: user}) - }) - - "follow" -> - response - - _ -> - nil + "mention" -> put_status(response, activity, user) + "favourite" -> put_status(response, parent_activity, user) + "reblog" -> put_status(response, parent_activity, user) + "move" -> put_target(response, activity, user) + "follow" -> response + _ -> nil end else _ -> nil end end + + defp put_status(response, activity, user) do + Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user})) + end + + defp put_target(response, activity, user) do + target = User.get_cached_by_ap_id(activity.data["target"]) + Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user})) + end end diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index dd445e8bf..8c131409d 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.Push.Impl do require Logger import Ecto.Query - @types ["Create", "Follow", "Announce", "Like"] + @types ["Create", "Follow", "Announce", "Like", "Move"] @doc "Performs sending notifications for user subscriptions" @spec perform(Notification.t()) :: list(any) | :error diff --git a/test/notification_test.exs b/test/notification_test.exs index f8d429223..dcbffeafe 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -630,6 +630,35 @@ test "notifications are deleted if a remote user is deleted" do assert Enum.empty?(Notification.for_user(local_user)) end + + test "move activity generates a notification" do + %{ap_id: old_ap_id} = old_user = insert(:user) + %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) + follower = insert(:user) + other_follower = insert(:user, %{allow_following_move: false}) + + User.follow(follower, old_user) + User.follow(other_follower, old_user) + + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + ObanHelpers.perform_all() + + assert [ + %{ + activity: %{ + data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} + } + } + ] = Notification.for_user(follower) + + assert [ + %{ + activity: %{ + data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} + } + } + ] = Notification.for_user(other_follower) + end end describe "for_user" do diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index c9043a69a..80b6d414c 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -107,4 +107,28 @@ test "Follow notification" do assert [] == NotificationView.render("index.json", %{notifications: [notification], for: followed}) end + + test "Move notification" do + %{ap_id: old_ap_id} = old_user = insert(:user) + %{ap_id: _new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) + follower = insert(:user) + + User.follow(follower, old_user) + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + Pleroma.Tests.ObanHelpers.perform_all() + + [notification] = Notification.for_user(follower) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "move", + account: AccountView.render("show.json", %{user: old_user, for: follower}), + target: AccountView.render("show.json", %{user: new_user, for: follower}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + assert [expected] == + NotificationView.render("index.json", %{notifications: [notification], for: follower}) + end end From cf52106e059560afb50de5ae8044d0a51278fae1 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 2 Dec 2019 13:38:35 +0700 Subject: [PATCH 8/9] Update Floki dependency --- lib/pleroma/html.ex | 1 - mix.lock | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 4acd46253..997e965f0 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -91,7 +91,6 @@ def extract_first_external_url(object, content) do Cachex.fetch!(:scrubber_cache, key, fn _key -> result = content - |> HtmlEntities.decode() |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]") |> Floki.attribute("a", "href") |> Enum.at(0) diff --git a/mix.lock b/mix.lock index 0d41c4ad5..49bdad128 100644 --- a/mix.lock +++ b/mix.lock @@ -38,7 +38,7 @@ "fast_html": {:hex, :fast_html, "0.99.4", "d80812664f0429607e1d880fba0ef04da87a2e4fa596701bcaae17953535695c", [:make, :mix], [], "hexpm"}, "fast_sanitize": {:hex, :fast_sanitize, "0.1.4", "6c2e7203ca2f8275527a3021ba6e9d5d4ee213a47dc214a97c128737c9e56df1", [:mix], [{:fast_html, "~> 0.99", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, - "floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"}, + "floki": {:hex, :floki, "0.23.1", "e100306ce7d8841d70a559748e5091542e2cfc67ffb3ade92b89a8435034dab1", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm"}, "gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"}, "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, From ed7f850ad2c9271268098959d8a86b24b187fa22 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Wed, 4 Dec 2019 16:22:01 +0300 Subject: [PATCH 9/9] fix test --- test/web/mastodon_api/views/notification_view_test.exs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index 80b6d414c..26e1afc85 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -117,6 +117,9 @@ test "Move notification" do Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) Pleroma.Tests.ObanHelpers.perform_all() + old_user = refresh_record(old_user) + new_user = refresh_record(new_user) + [notification] = Notification.for_user(follower) expected = %{