From b777083f3f396a7d8c357ec968f72679befc691c Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 25 Oct 2019 19:14:18 +0700 Subject: [PATCH 01/70] 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 02/70] 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 3db988250bcd279f20bd1742ca454aa187d89368 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 10 Nov 2019 16:30:21 +0300 Subject: [PATCH 03/70] [#1335] User: refactored :blocks field into :blocked_users relation. Introduced UserBlock. --- lib/pleroma/notification.ex | 4 +- lib/pleroma/user.ex | 57 +++++++++------ lib/pleroma/user/search.ex | 15 +++- lib/pleroma/user_block.ex | 72 +++++++++++++++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 8 +-- .../controllers/account_controller.ex | 4 +- lib/pleroma/web/streamer/worker.ex | 2 +- .../20191108161911_create_user_blocks.exs | 14 ++++ ...11_data_migration_populate_user_blocks.exs | 50 +++++++++++++ test/conversation/participation_test.exs | 6 +- test/notification_test.exs | 6 +- test/user_test.exs | 27 +++---- test/web/activity_pub/activity_pub_test.exs | 12 ++-- .../transmogrifier/follow_handling_test.exs | 2 +- .../controllers/account_controller_test.exs | 2 +- .../controllers/status_controller_test.exs | 4 +- .../controllers/timeline_controller_test.exs | 2 +- .../mastodon_api/views/account_view_test.exs | 6 +- test/web/streamer/streamer_test.exs | 6 +- test/web/twitter_api/util_controller_test.exs | 4 +- 20 files changed, 234 insertions(+), 69 deletions(-) create mode 100644 lib/pleroma/user_block.ex create mode 100644 priv/repo/migrations/20191108161911_create_user_blocks.exs create mode 100644 priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index b7ecf51e4..82faef85e 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -60,8 +60,10 @@ def for_user_query(user, opts \\ []) do end defp exclude_blocked(query, user) do + blocked_ap_ids = User.blocked_ap_ids(user) + query - |> where([n, a], a.actor not in ^user.blocks) + |> where([n, a], a.actor not in ^blocked_ap_ids) |> where( [n, a], fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index f8c2db1e1..d4fc1c093 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -7,6 +7,7 @@ defmodule Pleroma.User do import Ecto.Changeset import Ecto.Query + import Ecto, only: [assoc: 2] alias Comeonin.Pbkdf2 alias Ecto.Multi @@ -21,6 +22,7 @@ defmodule Pleroma.User do alias Pleroma.Repo alias Pleroma.RepoStreamer alias Pleroma.User + alias Pleroma.UserBlock alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils @@ -74,7 +76,6 @@ defmodule Pleroma.User do field(:password_reset_pending, :boolean, default: false) field(:confirmation_token, :string, default: nil) field(:default_scope, :string, default: "public") - field(:blocks, {:array, :string}, default: []) field(:domain_blocks, {:array, :string}, default: []) field(:mutes, {:array, :string}, default: []) field(:muted_reblogs, {:array, :string}, default: []) @@ -118,9 +119,16 @@ defmodule Pleroma.User do has_many(:notifications, Notification) has_many(:registrations, Registration) has_many(:deliveries, Delivery) + has_many(:blocker_blocks, UserBlock, foreign_key: :blocker_id) + has_many(:blockee_blocks, UserBlock, foreign_key: :blockee_id) + has_many(:blocked_users, through: [:blocker_blocks, :blockee]) + has_many(:blocker_users, through: [:blockee_blocks, :blocker]) field(:info, :map, default: %{}) + # `:blocks` is deprecated (replaced with `blocked_users` relation) + field(:blocks, {:array, :string}, default: []) + timestamps() end @@ -986,7 +994,7 @@ def unsubscribe(unsubscriber, %{ap_id: ap_id}) do end end - def block(blocker, %User{ap_id: ap_id} = blocked) do + def block(blocker, %User{} = blocked) do # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213) blocker = if following?(blocker, blocked) do @@ -1015,7 +1023,7 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do {:ok, blocker} = update_follower_count(blocker) {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked) - add_to_block(blocker, ap_id) + add_to_block(blocker, blocked) end # helper to handle the block given only an actor's AP id @@ -1023,8 +1031,13 @@ def block(blocker, %{ap_id: ap_id}) do block(blocker, get_cached_by_ap_id(ap_id)) end + def unblock(blocker, %User{} = blocked) do + remove_from_block(blocker, blocked) + end + + # helper to handle the block given only an actor's AP id def unblock(blocker, %{ap_id: ap_id}) do - remove_from_block(blocker, ap_id) + unblock(blocker, get_cached_by_ap_id(ap_id)) end def mutes?(nil, _), do: false @@ -1043,7 +1056,7 @@ def blocks?(%User{} = user, %User{} = target) do def blocks?(nil, _), do: false def blocks_ap_id?(%User{} = user, %User{} = target) do - Enum.member?(user.blocks, target.ap_id) + UserBlock.exists?(user, target) end def blocks_ap_id?(_, _), do: false @@ -1070,10 +1083,20 @@ def muted_users(user) do @spec blocked_users(User.t()) :: [User.t()] def blocked_users(user) do - User.Query.build(%{ap_id: user.blocks, deactivated: false}) + user + |> assoc(:blocked_users) + |> restrict_deactivated() |> Repo.all() end + def blocked_ap_ids(user) do + Repo.all( + from(u in assoc(user, :blocked_users), + select: u.ap_id + ) + ) + end + @spec subscribers(User.t()) :: [User.t()] def subscribers(user) do User.Query.build(%{ap_id: user.subscribers, deactivated: false}) @@ -1179,7 +1202,7 @@ def perform(:blocks_import, %User{} = blocker, blocked_identifiers) blocked_identifiers, fn blocked_identifier -> with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), - {:ok, blocker} <- block(blocker, blocked), + {:ok, _user_block} <- block(blocker, blocked), {:ok, _} <- ActivityPub.block(blocker, blocked) do blocked else @@ -1844,21 +1867,15 @@ def unblock_domain(user, domain_blocked) do set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked)) end - defp set_blocks(user, blocks) do - params = %{blocks: blocks} - - user - |> cast(params, [:blocks]) - |> validate_required([:blocks]) - |> update_and_set_cache() + @spec add_to_block(User.t(), User.t()) :: {:ok, UserBlock.t()} | {:error, Ecto.Changeset.t()} + defp add_to_block(%User{} = user, %User{} = blocked) do + UserBlock.create(user, blocked) end - def add_to_block(user, blocked) do - set_blocks(user, Enum.uniq([blocked | user.blocks])) - end - - def remove_from_block(user, blocked) do - set_blocks(user, List.delete(user.blocks, blocked)) + @spec add_to_block(User.t(), User.t()) :: + {:ok, UserBlock.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()} + defp remove_from_block(%User{} = user, %User{} = blocked) do + UserBlock.delete(user, blocked) end defp set_mutes(user, mutes) do diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 09664db76..8fa1e2e97 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -98,9 +98,18 @@ defp trigram_rank(query, query_string) do defp base_query(_user, false), do: User defp base_query(user, true), do: User.get_followers_query(user) - defp filter_blocked_user(query, %User{blocks: blocks}) - when length(blocks) > 0 do - from(q in query, where: not (q.ap_id in ^blocks)) + defp filter_blocked_user(query, %User{} = blocker) do + blocker_id = FlakeId.from_string(blocker.id) + + from( + q in query, + where: + fragment( + "? NOT IN (SELECT blockee_id FROM user_blocks WHERE user_blocks.blocker_id = ?)", + q.id, + ^blocker_id + ) + ) end defp filter_blocked_user(query, _), do: query diff --git a/lib/pleroma/user_block.ex b/lib/pleroma/user_block.ex new file mode 100644 index 000000000..1cdb15886 --- /dev/null +++ b/lib/pleroma/user_block.ex @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserBlock do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.UserBlock + + schema "user_blocks" do + belongs_to(:blocker, User, type: FlakeId.Ecto.CompatType) + belongs_to(:blockee, User, type: FlakeId.Ecto.CompatType) + + timestamps(updated_at: false) + end + + def changeset(%UserBlock{} = user_block, params \\ %{}) do + user_block + |> cast(params, [:blocker_id, :blockee_id]) + |> validate_required([:blocker_id, :blockee_id]) + |> unique_constraint(:blockee_id, name: :user_blocks_blocker_id_blockee_id_index) + |> validate_not_self_block() + end + + def exists?(%User{} = blocker, %User{} = blockee) do + UserBlock + |> where(blocker_id: ^blocker.id, blockee_id: ^blockee.id) + |> Repo.exists?() + end + + def create(%User{} = blocker, %User{} = blockee) do + %UserBlock{} + |> changeset(%{blocker_id: blocker.id, blockee_id: blockee.id}) + |> Repo.insert( + on_conflict: :replace_all_except_primary_key, + conflict_target: [:blocker_id, :blockee_id] + ) + end + + def delete(%User{} = blocker, %User{} = blockee) do + attrs = %{blocker_id: blocker.id, blockee_id: blockee.id} + + if is_nil(existing_record = Repo.get_by(UserBlock, attrs)) do + {:ok, nil} + else + Repo.delete(existing_record) + end + end + + defp validate_not_self_block(%Ecto.Changeset{} = changeset) do + changeset + |> validate_change(:blockee_id, fn _, blockee_id -> + if blockee_id == changeset.changes[:blocker_id] || changeset.data.blocker_id do + [blockee_id: "can't be equal to blocker_id"] + else + [] + end + end) + |> validate_change(:blocker_id, fn _, blocker_id -> + if blocker_id == changeset.changes[:blockee_id] || changeset.data.blockee_id do + [blocker_id: "can't be equal to blockee_id"] + else + [] + end + end) + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 65dd251f3..82203ef5c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -876,7 +876,7 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do defp restrict_muted(query, _), do: query defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do - blocks = user.blocks || [] + blocked_ap_ids = User.blocked_ap_ids(user) domain_blocks = user.domain_blocks || [] query = @@ -884,14 +884,14 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do from( [activity, object: o] in query, - where: fragment("not (? = ANY(?))", activity.actor, ^blocks), - where: fragment("not (? && ?)", activity.recipients, ^blocks), + where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids), + where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids), where: fragment( "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", activity.data, activity.data, - ^blocks + ^blocked_ap_ids ), where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks), where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 73fad519e..7c94a35d9 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -341,7 +341,7 @@ def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do @doc "POST /api/v1/accounts/:id/block" def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do - with {:ok, blocker} <- User.block(blocker, blocked), + with {:ok, _user_block} <- User.block(blocker, blocked), {:ok, _activity} <- ActivityPub.block(blocker, blocked) do render(conn, "relationship.json", user: blocker, target: blocked) else @@ -351,7 +351,7 @@ def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do @doc "POST /api/v1/accounts/:id/unblock" def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do - with {:ok, blocker} <- User.unblock(blocker, blocked), + with {:ok, _user_block} <- User.unblock(blocker, blocked), {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do render(conn, "relationship.json", user: blocker, target: blocked) else diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex index 33b24840d..672dd52ee 100644 --- a/lib/pleroma/web/streamer/worker.ex +++ b/lib/pleroma/web/streamer/worker.ex @@ -129,7 +129,7 @@ defp do_stream(%{topic: topic, item: item}) do end defp should_send?(%User{} = user, %Activity{} = item) do - blocks = user.blocks || [] + blocks = User.blocked_ap_ids(user) mutes = user.mutes || [] reblog_mutes = user.muted_reblogs || [] recipient_blocks = MapSet.new(blocks ++ mutes) diff --git a/priv/repo/migrations/20191108161911_create_user_blocks.exs b/priv/repo/migrations/20191108161911_create_user_blocks.exs new file mode 100644 index 000000000..c882d2bd6 --- /dev/null +++ b/priv/repo/migrations/20191108161911_create_user_blocks.exs @@ -0,0 +1,14 @@ +defmodule Pleroma.Repo.Migrations.CreateUserBlocks do + use Ecto.Migration + + def change do + create_if_not_exists table(:user_blocks) do + add(:blocker_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:blockee_id, references(:users, type: :uuid, on_delete: :delete_all)) + + timestamps(updated_at: false) + end + + create_if_not_exists(unique_index(:user_blocks, [:blocker_id, :blockee_id])) + end +end diff --git a/priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs b/priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs new file mode 100644 index 000000000..728da8211 --- /dev/null +++ b/priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs @@ -0,0 +1,50 @@ +defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserBlocks do + use Ecto.Migration + + alias Ecto.Adapters.SQL + alias Pleroma.Repo + + require Logger + + def up do + {:ok, %{rows: block_rows}} = + SQL.query(Repo, "SELECT id, blocks FROM users WHERE blocks != '{}'") + + blockee_ap_ids = + Enum.flat_map( + block_rows, + fn [_, ap_ids] -> ap_ids end + ) + |> Enum.uniq() + + # Selecting ids of all blockees at once in order to reduce the number of SELECT queries + {:ok, %{rows: blockee_ap_id_id}} = + SQL.query(Repo, "SELECT ap_id, id FROM users WHERE ap_id = ANY($1)", [blockee_ap_ids]) + + blockee_id_by_ap_id = Enum.into(blockee_ap_id_id, %{}, fn [k, v] -> {k, v} end) + + Enum.each( + block_rows, + fn [blocker_id, blockee_ap_ids] -> + blocker_uuid = Ecto.UUID.cast!(blocker_id) + + for blockee_ap_id <- blockee_ap_ids do + blockee_id = blockee_id_by_ap_id[blockee_ap_id] + blockee_uuid = blockee_id && Ecto.UUID.cast!(blockee_id) + + with {:ok, blockee_uuid} <- Ecto.UUID.cast(blockee_id) do + execute( + "INSERT INTO user_blocks(blocker_id, blockee_id, inserted_at) " <> + "VALUES('#{blocker_uuid}'::uuid, '#{blockee_uuid}'::uuid, now()) " <> + "ON CONFLICT (blocker_id, blockee_id) DO NOTHING" + ) + else + _ -> Logger.warn("Missing reference: (#{blocker_uuid}, #{blockee_id})") + end + end + end + ) + end + + def down, do: :noop +end diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs index 863270022..a36f45a5a 100644 --- a/test/conversation/participation_test.exs +++ b/test/conversation/participation_test.exs @@ -252,7 +252,7 @@ test "when the user blocks a recipient, the existing conversations with them are assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4 - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_block} = User.block(blocker, blocked) # The conversations with the blocked user are marked as read assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] = @@ -274,7 +274,7 @@ test "the new conversation with the blocked user is not marked as unread " do blocked = insert(:user) third_user = insert(:user) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_block} = User.block(blocker, blocked) # When the blocked user is the author {:ok, _direct1} = @@ -311,7 +311,7 @@ test "the conversation with the blocked user is not marked as unread on a reply" "visibility" => "direct" }) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_block} = User.block(blocker, blocked) assert [%{read: true}] = Participation.for_user(blocker) assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 diff --git a/test/notification_test.exs b/test/notification_test.exs index f8d429223..5388214ba 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -93,7 +93,7 @@ test "it creates a notification for user if the user blocks the activity author" activity = insert(:note_activity) author = User.get_cached_by_ap_id(activity.data["actor"]) user = insert(:user) - {:ok, user} = User.block(user, author) + {:ok, _user_block} = User.block(user, author) assert Notification.create_notification(activity, user) end @@ -656,7 +656,7 @@ test "it doesn't return notifications for muted user with notifications" do test "it doesn't return notifications for blocked user" do user = insert(:user) blocked = insert(:user) - {:ok, user} = User.block(user, blocked) + {:ok, _user_block} = User.block(user, blocked) {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) @@ -696,7 +696,7 @@ test "it returns notifications from a muted user when with_muted is set" do test "it doesn't return notifications from a blocked user when with_muted is set" do user = insert(:user) blocked = insert(:user) - {:ok, user} = User.block(user, blocked) + {:ok, _user_block} = User.block(user, blocked) {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) diff --git a/test/user_test.exs b/test/user_test.exs index 6b1b24ce5..fa8d336f2 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -100,7 +100,7 @@ test "clears follow requests when requester is blocked" do CommonAPI.follow(follower, followed) assert [_activity] = User.get_follow_requests(followed) - {:ok, _follower} = User.block(followed, follower) + {:ok, _user_block} = User.block(followed, follower) assert [] = User.get_follow_requests(followed) end @@ -113,8 +113,8 @@ test "follow_all follows mutliple users" do not_followed = insert(:user) reverse_blocked = insert(:user) - {:ok, user} = User.block(user, blocked) - {:ok, reverse_blocked} = User.block(reverse_blocked, user) + {:ok, _user_block} = User.block(user, blocked) + {:ok, _user_block} = User.block(reverse_blocked, user) {:ok, user} = User.follow(user, followed_zero) @@ -166,7 +166,7 @@ test "can't follow a user who blocked us" do blocker = insert(:user) blockee = insert(:user) - {:ok, blocker} = User.block(blocker, blockee) + {:ok, _user_block} = User.block(blocker, blockee) {:error, _} = User.follow(blockee, blocker) end @@ -175,7 +175,7 @@ test "can't subscribe to a user who blocked us" do blocker = insert(:user) blocked = insert(:user) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_block} = User.block(blocker, blocked) {:error, _} = User.subscribe(blocked, blocker) end @@ -711,7 +711,7 @@ test "it blocks people" do refute User.blocks?(user, blocked_user) - {:ok, user} = User.block(user, blocked_user) + {:ok, _user_block} = User.block(user, blocked_user) assert User.blocks?(user, blocked_user) end @@ -720,8 +720,8 @@ test "it unblocks users" do user = insert(:user) blocked_user = insert(:user) - {:ok, user} = User.block(user, blocked_user) - {:ok, user} = User.unblock(user, blocked_user) + {:ok, _user_block} = User.block(user, blocked_user) + {:ok, _user_block} = User.unblock(user, blocked_user) refute User.blocks?(user, blocked_user) end @@ -736,7 +736,7 @@ test "blocks tear down cyclical follow relationships" do assert User.following?(blocker, blocked) assert User.following?(blocked, blocker) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_block} = User.block(blocker, blocked) blocked = User.get_cached_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -754,7 +754,7 @@ test "blocks tear down blocker->blocked follow relationships" do assert User.following?(blocker, blocked) refute User.following?(blocked, blocker) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_block} = User.block(blocker, blocked) blocked = User.get_cached_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -772,7 +772,7 @@ test "blocks tear down blocked->blocker follow relationships" do refute User.following?(blocker, blocked) assert User.following?(blocked, blocker) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_block} = User.block(blocker, blocked) blocked = User.get_cached_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -790,7 +790,7 @@ test "blocks tear down blocked->blocker subscription relationships" do assert User.subscribed_to?(blocked, blocker) refute User.subscribed_to?(blocker, blocked) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_block} = User.block(blocker, blocked) assert User.blocks?(blocker, blocked) refute User.subscribed_to?(blocker, blocked) @@ -1313,7 +1313,8 @@ test "follower count is updated when a follower is blocked" do {:ok, _follower2} = User.follow(follower2, user) {:ok, _follower3} = User.follow(follower3, user) - {:ok, user} = User.block(user, follower) + {:ok, _user_block} = User.block(user, follower) + user = refresh_record(user) assert User.user_info(user).follower_count == 2 end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 0d0281faf..17d283954 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -484,7 +484,7 @@ test "retrieves activities that have a given context" do activity_five = insert(:note_activity) user = insert(:user) - {:ok, user} = User.block(user, %{ap_id: activity_five.data["actor"]}) + {:ok, _user_block} = User.block(user, %{ap_id: activity_five.data["actor"]}) activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user}) assert activities == [activity_two, activity] @@ -497,7 +497,7 @@ test "doesn't return blocked activities" do activity_three = insert(:note_activity) user = insert(:user) booster = insert(:user) - {:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]}) + {:ok, _user_block} = User.block(user, %{ap_id: activity_one.data["actor"]}) activities = ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) @@ -506,7 +506,7 @@ test "doesn't return blocked activities" do assert Enum.member?(activities, activity_three) refute Enum.member?(activities, activity_one) - {:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) + {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) activities = ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) @@ -515,7 +515,7 @@ test "doesn't return blocked activities" do assert Enum.member?(activities, activity_three) assert Enum.member?(activities, activity_one) - {:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]}) + {:ok, _user_block} = User.block(user, %{ap_id: activity_three.data["actor"]}) {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster) %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) activity_three = Activity.get_by_id(activity_three.id) @@ -542,7 +542,7 @@ test "doesn't return transitive interactions concerning blocked users" do blockee = insert(:user) friend = insert(:user) - {:ok, blocker} = User.block(blocker, blockee) + {:ok, _user_block} = User.block(blocker, blockee) {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"}) @@ -565,7 +565,7 @@ test "doesn't return announce activities concerning blocked users" do blockee = insert(:user) friend = insert(:user) - {:ok, blocker} = User.block(blocker, blockee) + {:ok, _user_block} = User.block(blocker, blockee) {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"}) diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 75cfbea2e..5c67d41c5 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -128,7 +128,7 @@ test "it rejects incoming follow requests from blocked users when deny_follow_bl user = insert(:user) {:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin") - {:ok, user} = User.block(user, target) + {:ok, _user_block} = User.block(user, target) data = File.read!("test/fixtures/mastodon-follow-activity.json") diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 8fc2d9300..b98d9c8e3 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -883,7 +883,7 @@ test "getting a list of blocks", %{conn: conn} do user = insert(:user) other_user = insert(:user) - {:ok, user} = User.block(user, other_user) + {:ok, _user_block} = User.block(user, other_user) conn = conn diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index a96fd860b..e8a1054ac 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -1108,7 +1108,7 @@ test "does not return users who have favorited the status but are blocked", %{ activity: activity } do other_user = insert(:user) - {:ok, user} = User.block(user, other_user) + {:ok, _user_block} = User.block(user, other_user) {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) @@ -1205,7 +1205,7 @@ test "does not return users who have reblogged the status but are blocked", %{ activity: activity } do other_user = insert(:user) - {:ok, user} = User.block(user, other_user) + {:ok, _user_block} = User.block(user, other_user) {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 61b6cea75..524529d1f 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -194,7 +194,7 @@ test "doesn't include DMs from blocked users", %{conn: conn} do blocker = insert(:user) blocked = insert(:user) user = insert(:user) - {:ok, blocker} = User.block(blocker, blocked) + {:ok, _user_block} = User.block(blocker, blocked) {:ok, _blocked_direct} = CommonAPI.post(blocked, %{ diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index af88841ed..c34bf7ba9 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -219,8 +219,8 @@ test "represent a relationship for the blocking and blocked user" do {:ok, user} = User.follow(user, other_user) {:ok, other_user} = User.subscribe(user, other_user) - {:ok, user} = User.block(user, other_user) - {:ok, other_user} = User.block(other_user, user) + {:ok, _user_block} = User.block(user, other_user) + {:ok, _user_block} = User.block(other_user, user) expected = %{ id: to_string(other_user.id), @@ -291,7 +291,7 @@ test "represent an embedded relationship" do other_user = insert(:user) {:ok, other_user} = User.follow(other_user, user) - {:ok, other_user} = User.block(other_user, user) + {:ok, _user_block} = User.block(other_user, user) {:ok, _} = User.follow(insert(:user), user) expected = %{ diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 80a7541b2..6461fc4c3 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -59,7 +59,7 @@ test "it doesn't send notify to the 'user:notification' stream when a user is bl user: user } do blocked = insert(:user) - {:ok, user} = User.block(user, blocked) + {:ok, _user_block} = User.block(user, blocked) task = Task.async(fn -> refute_receive {:text, _}, 4_000 end) @@ -259,7 +259,7 @@ test "it sends message if recipients invalid and thread containment is enabled b test "it doesn't send messages involving blocked users" do user = insert(:user) blocked_user = insert(:user) - {:ok, user} = User.block(user, blocked_user) + {:ok, _user_block} = User.block(user, blocked_user) task = Task.async(fn -> @@ -301,7 +301,7 @@ test "it doesn't send messages transitively involving blocked users" do "public" => [fake_socket] } - {:ok, blocker} = User.block(blocker, blockee) + {:ok, _user_block} = User.block(blocker, blockee) {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"}) diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index f0211f59c..986ee01f3 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -387,7 +387,7 @@ test "returns error when user is blocked", %{conn: conn} do user = insert(:user) user2 = insert(:user) - {:ok, _user} = Pleroma.User.block(user2, user) + {:ok, _user_block} = Pleroma.User.block(user2, user) response = conn @@ -485,7 +485,7 @@ test "returns error when user is blocked", %{conn: conn} do Pleroma.Config.put([:user, :deny_follow_blocked], true) user = insert(:user) user2 = insert(:user) - {:ok, _user} = Pleroma.User.block(user2, user) + {:ok, _user_block} = Pleroma.User.block(user2, user) response = conn From 5cf2c7422b006d477498af8d7618a0d6f7659290 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 11 Nov 2019 14:16:57 +0300 Subject: [PATCH 04/70] [#1335] Applied code review suggestions. --- lib/pleroma/user.ex | 9 ++++----- lib/pleroma/user/search.ex | 15 +++++---------- lib/pleroma/user_block.ex | 11 +++++------ 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index d4fc1c093..9dd1ad8fc 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1090,11 +1090,10 @@ def blocked_users(user) do end def blocked_ap_ids(user) do - Repo.all( - from(u in assoc(user, :blocked_users), - select: u.ap_id - ) - ) + user + |> assoc(:blocked_users) + |> select([u], u.ap_id) + |> Repo.all() end @spec subscribers(User.t()) :: [User.t()] diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 8fa1e2e97..049e1a634 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -99,17 +99,12 @@ defp base_query(_user, false), do: User defp base_query(user, true), do: User.get_followers_query(user) defp filter_blocked_user(query, %User{} = blocker) do - blocker_id = FlakeId.from_string(blocker.id) - - from( - q in query, - where: - fragment( - "? NOT IN (SELECT blockee_id FROM user_blocks WHERE user_blocks.blocker_id = ?)", - q.id, - ^blocker_id - ) + query + |> join(:left, [u], b in Pleroma.UserBlock, + as: :blocks, + on: b.blocker_id == ^blocker.id and u.id == b.blockee_id ) + |> where([blocks: b], is_nil(b.blockee_id)) end defp filter_blocked_user(query, _), do: query diff --git a/lib/pleroma/user_block.ex b/lib/pleroma/user_block.ex index 1cdb15886..bcf4b64d9 100644 --- a/lib/pleroma/user_block.ex +++ b/lib/pleroma/user_block.ex @@ -45,24 +45,23 @@ def create(%User{} = blocker, %User{} = blockee) do def delete(%User{} = blocker, %User{} = blockee) do attrs = %{blocker_id: blocker.id, blockee_id: blockee.id} - if is_nil(existing_record = Repo.get_by(UserBlock, attrs)) do - {:ok, nil} - else - Repo.delete(existing_record) + case Repo.get_by(UserBlock, attrs) do + %UserBlock{} = existing_record -> Repo.delete(existing_record) + nil -> {:ok, nil} end end defp validate_not_self_block(%Ecto.Changeset{} = changeset) do changeset |> validate_change(:blockee_id, fn _, blockee_id -> - if blockee_id == changeset.changes[:blocker_id] || changeset.data.blocker_id do + if blockee_id == get_field(changeset, :blocker_id) do [blockee_id: "can't be equal to blocker_id"] else [] end end) |> validate_change(:blocker_id, fn _, blocker_id -> - if blocker_id == changeset.changes[:blockee_id] || changeset.data.blockee_id do + if blocker_id == get_field(changeset, :blockee_id) do [blocker_id: "can't be equal to blockee_id"] else [] From f86a7d5d8bb07ed57dcbb50e0746bc37916eb627 Mon Sep 17 00:00:00 2001 From: eugenijm Date: Sat, 9 Nov 2019 13:56:27 +0300 Subject: [PATCH 05/70] Fix exclude_visibilities filter for followers-only Like notifications --- lib/pleroma/notification.ex | 34 ++-- .../notification_controller_test.exs | 168 ++++++++++++++---- 2 files changed, 153 insertions(+), 49 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index b7ecf51e4..98bcadc98 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -87,10 +87,28 @@ defp exclude_visibility(query, %{exclude_visibilities: visibility}) when is_list(visibility) do if Enum.all?(visibility, &(&1 in @valid_visibilities)) do query + |> join(:left, [n, a], mutated_activity in Pleroma.Activity, + on: + fragment("?->>'context'", a.data) == + fragment("?->>'context'", mutated_activity.data) and + fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and + fragment("?->>'type'", mutated_activity.data) == "Create", + as: :mutated_activity + ) |> where( - [n, a], + [n, a, mutated_activity: mutated_activity], not fragment( - "activity_visibility(?, ?, ?) = ANY (?)", + """ + CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce' + THEN (activity_visibility(?, ?, ?) = ANY (?)) + ELSE (activity_visibility(?, ?, ?) = ANY (?)) END + """, + a.data, + a.data, + mutated_activity.actor, + mutated_activity.recipients, + mutated_activity.data, + ^visibility, a.actor, a.recipients, a.data, @@ -105,17 +123,7 @@ defp exclude_visibility(query, %{exclude_visibilities: visibility}) defp exclude_visibility(query, %{exclude_visibilities: visibility}) when visibility in @valid_visibilities do - query - |> where( - [n, a], - not fragment( - "activity_visibility(?, ?, ?) = (?)", - a.actor, - a.recipients, - a.data, - ^visibility - ) - ) + exclude_visibility(query, [visibility]) end defp exclude_visibility(query, %{exclude_visibilities: visibility}) diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index fa55a7cf9..d70defe36 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -137,55 +137,151 @@ test "paginates notifications using min_id, since_id, max_id, and limit", %{conn assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result end - test "filters notifications using exclude_visibilities", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) + describe "exclude_visibilities" do + test "filters notifications for mentions", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) - {:ok, public_activity} = - CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "public"}) + {:ok, public_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "public"}) - {:ok, direct_activity} = - CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "direct"}) + {:ok, direct_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "direct"}) - {:ok, unlisted_activity} = - CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "unlisted"}) + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "unlisted"}) - {:ok, private_activity} = - CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"}) + {:ok, private_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"}) - conn = assign(conn, :user, user) + conn = assign(conn, :user, user) - conn_res = - get(conn, "/api/v1/notifications", %{ - exclude_visibilities: ["public", "unlisted", "private"] - }) + conn_res = + get(conn, "/api/v1/notifications", %{ + exclude_visibilities: ["public", "unlisted", "private"] + }) - assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) - assert id == direct_activity.id + assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) + assert id == direct_activity.id - conn_res = - get(conn, "/api/v1/notifications", %{ - exclude_visibilities: ["public", "unlisted", "direct"] - }) + conn_res = + get(conn, "/api/v1/notifications", %{ + exclude_visibilities: ["public", "unlisted", "direct"] + }) - assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) - assert id == private_activity.id + assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) + assert id == private_activity.id - conn_res = - get(conn, "/api/v1/notifications", %{ - exclude_visibilities: ["public", "private", "direct"] - }) + conn_res = + get(conn, "/api/v1/notifications", %{ + exclude_visibilities: ["public", "private", "direct"] + }) - assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) - assert id == unlisted_activity.id + assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) + assert id == unlisted_activity.id - conn_res = - get(conn, "/api/v1/notifications", %{ - exclude_visibilities: ["unlisted", "private", "direct"] - }) + conn_res = + get(conn, "/api/v1/notifications", %{ + exclude_visibilities: ["unlisted", "private", "direct"] + }) - assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) - assert id == public_activity.id + assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200) + assert id == public_activity.id + end + + test "filters notifications for Like activities", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, public_activity} = + CommonAPI.post(other_user, %{"status" => ".", "visibility" => "public"}) + + {:ok, direct_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "direct"}) + + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{"status" => ".", "visibility" => "unlisted"}) + + {:ok, private_activity} = + CommonAPI.post(other_user, %{"status" => ".", "visibility" => "private"}) + + {:ok, _, _} = CommonAPI.favorite(public_activity.id, user) + {:ok, _, _} = CommonAPI.favorite(direct_activity.id, user) + {:ok, _, _} = CommonAPI.favorite(unlisted_activity.id, user) + {:ok, _, _} = CommonAPI.favorite(private_activity.id, user) + + activity_ids = + conn + |> assign(:user, other_user) + |> get("/api/v1/notifications", %{exclude_visibilities: ["direct"]}) + |> json_response(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + refute direct_activity.id in activity_ids + + activity_ids = + conn + |> assign(:user, other_user) + |> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]}) + |> json_response(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + refute unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + assert direct_activity.id in activity_ids + + activity_ids = + conn + |> assign(:user, other_user) + |> get("/api/v1/notifications", %{exclude_visibilities: ["private"]}) + |> json_response(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + refute private_activity.id in activity_ids + assert direct_activity.id in activity_ids + + activity_ids = + conn + |> assign(:user, other_user) + |> get("/api/v1/notifications", %{exclude_visibilities: ["public"]}) + |> json_response(200) + |> Enum.map(& &1["status"]["id"]) + + refute public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + assert direct_activity.id in activity_ids + end + + test "filters notifications for Announce activities", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, public_activity} = + CommonAPI.post(other_user, %{"status" => ".", "visibility" => "public"}) + + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{"status" => ".", "visibility" => "unlisted"}) + + {:ok, _, _} = CommonAPI.repeat(public_activity.id, user) + {:ok, _, _} = CommonAPI.repeat(unlisted_activity.id, user) + + activity_ids = + conn + |> assign(:user, other_user) + |> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]}) + |> json_response(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + refute unlisted_activity.id in activity_ids + end end test "filters notifications using exclude_types", %{conn: conn} do From e6d7e27bd603806e96dfc2774f90cadb3cf73a8c Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 12 Nov 2019 18:36:50 +0700 Subject: [PATCH 06/70] 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 07/70] 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 08/70] 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 09/70] 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 c31ddce51ea18f052c1c3ad30a221b77c7a94e71 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 15 Nov 2019 21:38:54 +0300 Subject: [PATCH 10/70] [#1335] Reorganized `users.mutes` as relation to UserMute entity. --- lib/pleroma/user.ex | 114 ++++++++++++------ lib/pleroma/user_mute.ex | 71 +++++++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 6 +- .../controllers/account_controller.ex | 8 +- .../web/mastodon_api/views/account_view.ex | 4 +- lib/pleroma/web/streamer/worker.ex | 3 +- ...11_data_migration_populate_user_blocks.exs | 3 +- .../20191112151559_create_user_mutes.exs | 14 +++ ...614_data_migration_populate_user_mutes.exs | 48 ++++++++ test/notification_test.exs | 11 +- test/user_test.exs | 11 +- test/web/activity_pub/activity_pub_test.exs | 9 +- .../controllers/account_controller_test.exs | 2 +- .../notification_controller_test.exs | 9 +- .../mastodon_api/views/account_view_test.exs | 5 +- .../mastodon_api/views/status_view_test.exs | 4 +- 16 files changed, 258 insertions(+), 64 deletions(-) create mode 100644 lib/pleroma/user_mute.ex create mode 100644 priv/repo/migrations/20191112151559_create_user_mutes.exs create mode 100644 priv/repo/migrations/20191112151614_data_migration_populate_user_mutes.exs diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 9dd1ad8fc..383c45992 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -23,6 +23,7 @@ defmodule Pleroma.User do alias Pleroma.RepoStreamer alias Pleroma.User alias Pleroma.UserBlock + alias Pleroma.UserMute alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils @@ -119,11 +120,17 @@ defmodule Pleroma.User do has_many(:notifications, Notification) has_many(:registrations, Registration) has_many(:deliveries, Delivery) + has_many(:blocker_blocks, UserBlock, foreign_key: :blocker_id) has_many(:blockee_blocks, UserBlock, foreign_key: :blockee_id) has_many(:blocked_users, through: [:blocker_blocks, :blockee]) has_many(:blocker_users, through: [:blockee_blocks, :blocker]) + has_many(:muter_mutes, UserMute, foreign_key: :muter_id) + has_many(:mutee_mutes, UserMute, foreign_key: :mutee_id) + has_many(:muted_users, through: [:muter_mutes, :mutee]) + has_many(:muter_users, through: [:mutee_mutes, :muter]) + field(:info, :map, default: %{}) # `:blocks` is deprecated (replaced with `blocked_users` relation) @@ -968,12 +975,12 @@ def get_recipients_from_activity(%Activity{recipients: to}) do end @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()} - def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do - add_to_mutes(muter, ap_id, notifications?) + def mute(muter, %User{} = mutee, notifications? \\ true) do + add_to_mutes(muter, mutee, notifications?) end - def unmute(muter, %{ap_id: ap_id}) do - remove_from_mutes(muter, ap_id) + def unmute(muter, %User{} = mutee) do + remove_from_mutes(muter, mutee) end def subscribe(subscriber, %{ap_id: ap_id}) do @@ -1041,7 +1048,11 @@ def unblock(blocker, %{ap_id: ap_id}) do end def mutes?(nil, _), do: false - def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id) + def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target) + + def mutes_user?(%User{} = user, %User{} = target) do + UserMute.exists?(user, target) + end @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean() def muted_notifications?(nil, _), do: false @@ -1049,17 +1060,17 @@ def muted_notifications?(nil, _), do: false def muted_notifications?(user, %{ap_id: ap_id}), do: Enum.member?(user.muted_notifications, ap_id) - def blocks?(%User{} = user, %User{} = target) do - blocks_ap_id?(user, target) || blocks_domain?(user, target) - end - def blocks?(nil, _), do: false - def blocks_ap_id?(%User{} = user, %User{} = target) do + def blocks?(%User{} = user, %User{} = target) do + blocks_user?(user, target) || blocks_domain?(user, target) + end + + def blocks_user?(%User{} = user, %User{} = target) do UserBlock.exists?(user, target) end - def blocks_ap_id?(_, _), do: false + def blocks_user?(_, _), do: false def blocks_domain?(%User{} = user, %User{} = target) do domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) @@ -1077,7 +1088,16 @@ def subscribed_to?(user, %{ap_id: ap_id}) do @spec muted_users(User.t()) :: [User.t()] def muted_users(user) do - User.Query.build(%{ap_id: user.mutes, deactivated: false}) + user + |> assoc(:muted_users) + |> restrict_deactivated() + |> Repo.all() + end + + def muted_ap_ids(user) do + user + |> assoc(:muted_users) + |> select([u], u.ap_id) |> Repo.all() end @@ -1096,6 +1116,35 @@ def blocked_ap_ids(user) do |> Repo.all() end + defp related_ap_ids_sql(join_table, source_column, target_column) do + "(SELECT array_agg(u.ap_id) FROM users as u " <> + "INNER JOIN #{join_table} AS join_table " <> + "ON join_table.#{source_column} = $1 " <> + "WHERE u.id = join_table.#{target_column})" + end + + @related_ap_ids_sql_params %{ + blocked_users: ["user_blocks", "blocker_id", "blockee_id"], + muted_users: ["user_mutes", "muter_id", "mutee_id"] + } + + def related_ap_ids(user, relations) when is_list(relations) do + query = + relations + |> Enum.map(fn r -> @related_ap_ids_sql_params[r] end) + |> Enum.filter(& &1) + |> Enum.map(fn [join_table, source_column, target_column] -> + related_ap_ids_sql(join_table, source_column, target_column) + end) + |> Enum.join(", ") + + with {:ok, %{rows: [ap_ids_arrays]}} <- + Repo.query("SELECT #{query}", [FlakeId.from_string(user.id)]) do + ap_ids_arrays = Enum.map(ap_ids_arrays, &(&1 || [])) + {:ok, ap_ids_arrays} + end + end + @spec subscribers(User.t()) :: [User.t()] def subscribers(user) do User.Query.build(%{ap_id: user.subscribers, deactivated: false}) @@ -1877,32 +1926,27 @@ defp remove_from_block(%User{} = user, %User{} = blocked) do UserBlock.delete(user, blocked) end - defp set_mutes(user, mutes) do - params = %{mutes: mutes} - - user - |> cast(params, [:mutes]) - |> validate_required([:mutes]) - |> update_and_set_cache() - end - - def add_to_mutes(user, muted, notifications?) do - with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do - set_notification_mutes( - user, - Enum.uniq([muted | user.muted_notifications]), - notifications? - ) + defp add_to_mutes(%User{} = user, %User{ap_id: ap_id} = muted_user, notifications?) do + with {:ok, user_mute} <- UserMute.create(user, muted_user), + {:ok, _user} <- + set_notification_mutes( + user, + Enum.uniq([ap_id | user.muted_notifications]), + notifications? + ) do + {:ok, user_mute} end end - def remove_from_mutes(user, muted) do - with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do - set_notification_mutes( - user, - List.delete(user.muted_notifications, muted), - true - ) + defp remove_from_mutes(user, %User{ap_id: ap_id} = muted_user) do + with {:ok, user_mute} <- UserMute.delete(user, muted_user), + {:ok, _user} <- + set_notification_mutes( + user, + List.delete(user.muted_notifications, ap_id), + true + ) do + {:ok, user_mute} end end diff --git a/lib/pleroma/user_mute.ex b/lib/pleroma/user_mute.ex new file mode 100644 index 000000000..417a5ff84 --- /dev/null +++ b/lib/pleroma/user_mute.ex @@ -0,0 +1,71 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserMute do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.UserMute + + schema "user_mutes" do + belongs_to(:muter, User, type: FlakeId.Ecto.CompatType) + belongs_to(:mutee, User, type: FlakeId.Ecto.CompatType) + + timestamps(updated_at: false) + end + + def changeset(%UserMute{} = user_mute, params \\ %{}) do + user_mute + |> cast(params, [:muter_id, :mutee_id]) + |> validate_required([:muter_id, :mutee_id]) + |> unique_constraint(:mutee_id, name: :user_mutes_muter_id_mutee_id_index) + |> validate_not_self_mute() + end + + def exists?(%User{} = muter, %User{} = mutee) do + UserMute + |> where(muter_id: ^muter.id, mutee_id: ^mutee.id) + |> Repo.exists?() + end + + def create(%User{} = muter, %User{} = mutee) do + %UserMute{} + |> changeset(%{muter_id: muter.id, mutee_id: mutee.id}) + |> Repo.insert( + on_conflict: :replace_all_except_primary_key, + conflict_target: [:muter_id, :mutee_id] + ) + end + + def delete(%User{} = muter, %User{} = mutee) do + attrs = %{muter_id: muter.id, mutee_id: mutee.id} + + case Repo.get_by(UserMute, attrs) do + %UserMute{} = existing_record -> Repo.delete(existing_record) + nil -> {:ok, nil} + end + end + + defp validate_not_self_mute(%Ecto.Changeset{} = changeset) do + changeset + |> validate_change(:mutee_id, fn _, mutee_id -> + if mutee_id == get_field(changeset, :muter_id) do + [mutee_id: "can't be equal to muter_id"] + else + [] + end + end) + |> validate_change(:muter_id, fn _, muter_id -> + if muter_id == get_field(changeset, :mutee_id) do + [muter_id: "can't be equal to mutee_id"] + else + [] + end + end) + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 82203ef5c..cba2abfc3 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -858,7 +858,7 @@ defp restrict_reblogs(query, _), do: query defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do - mutes = user.mutes + mutes = opts["muted_ap_ids"] || User.muted_ap_ids(user) query = from([activity] in query, @@ -875,8 +875,8 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do defp restrict_muted(query, _), do: query - defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do - blocked_ap_ids = User.blocked_ap_ids(user) + defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do + blocked_ap_ids = opts["blocked_ap_ids"] || User.blocked_ap_ids(user) domain_blocks = user.domain_blocks || [] query = diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 7c94a35d9..e37d3bb07 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -323,7 +323,9 @@ def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) d def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do notifications? = params |> Map.get("notifications", true) |> truthy_param?() - with {:ok, muter} <- User.mute(muter, muted, notifications?) do + with {:ok, _user_mute} <- User.mute(muter, muted, notifications?) do + # TODO: remove `muter` refresh once `muted_notifications` field is deprecated + muter = User.get_cached_by_id(muter.id) render(conn, "relationship.json", user: muter, target: muted) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -332,7 +334,9 @@ def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do @doc "POST /api/v1/accounts/:id/unmute" def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do - with {:ok, muter} <- User.unmute(muter, muted) do + with {:ok, _user_mute} <- User.unmute(muter, muted) do + # TODO: remove `muter` refresh once `muted_notifications` field is deprecated + muter = User.get_cached_by_id(muter.id) render(conn, "relationship.json", user: muter, target: muted) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index e30fed610..37dc8e194 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -50,8 +50,8 @@ def render("relationship.json", %{user: %User{} = user, target: %User{} = target id: to_string(target.id), following: User.following?(user, target), followed_by: User.following?(target, user), - blocking: User.blocks_ap_id?(user, target), - blocked_by: User.blocks_ap_id?(target, user), + blocking: User.blocks_user?(user, target), + blocked_by: User.blocks_user?(target, user), muting: User.mutes?(user, target), muting_notifications: User.muted_notifications?(user, target), subscribing: User.subscribed_to?(user, target), diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex index 672dd52ee..45f9c7c53 100644 --- a/lib/pleroma/web/streamer/worker.ex +++ b/lib/pleroma/web/streamer/worker.ex @@ -129,8 +129,7 @@ defp do_stream(%{topic: topic, item: item}) do end defp should_send?(%User{} = user, %Activity{} = item) do - blocks = User.blocked_ap_ids(user) - mutes = user.mutes || [] + {:ok, [blocks, mutes]} = User.related_ap_ids(user, [:blocked_users, :muted_users]) reblog_mutes = user.muted_reblogs || [] recipient_blocks = MapSet.new(blocks ++ mutes) recipients = MapSet.new(item.recipients) diff --git a/priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs b/priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs index 728da8211..fe537679d 100644 --- a/priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs +++ b/priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs @@ -30,9 +30,8 @@ def up do for blockee_ap_id <- blockee_ap_ids do blockee_id = blockee_id_by_ap_id[blockee_ap_id] - blockee_uuid = blockee_id && Ecto.UUID.cast!(blockee_id) - with {:ok, blockee_uuid} <- Ecto.UUID.cast(blockee_id) do + with {:ok, blockee_uuid} <- blockee_id && Ecto.UUID.cast(blockee_id) do execute( "INSERT INTO user_blocks(blocker_id, blockee_id, inserted_at) " <> "VALUES('#{blocker_uuid}'::uuid, '#{blockee_uuid}'::uuid, now()) " <> diff --git a/priv/repo/migrations/20191112151559_create_user_mutes.exs b/priv/repo/migrations/20191112151559_create_user_mutes.exs new file mode 100644 index 000000000..eaa285de2 --- /dev/null +++ b/priv/repo/migrations/20191112151559_create_user_mutes.exs @@ -0,0 +1,14 @@ +defmodule Pleroma.Repo.Migrations.CreateUserMutes do + use Ecto.Migration + + def change do + create_if_not_exists table(:user_mutes) do + add(:muter_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:mutee_id, references(:users, type: :uuid, on_delete: :delete_all)) + + timestamps(updated_at: false) + end + + create_if_not_exists(unique_index(:user_mutes, [:muter_id, :mutee_id])) + end +end diff --git a/priv/repo/migrations/20191112151614_data_migration_populate_user_mutes.exs b/priv/repo/migrations/20191112151614_data_migration_populate_user_mutes.exs new file mode 100644 index 000000000..a8bdd072e --- /dev/null +++ b/priv/repo/migrations/20191112151614_data_migration_populate_user_mutes.exs @@ -0,0 +1,48 @@ +defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserMutes do + use Ecto.Migration + + alias Ecto.Adapters.SQL + alias Pleroma.Repo + + require Logger + + def up do + {:ok, %{rows: mute_rows}} = SQL.query(Repo, "SELECT id, mutes FROM users WHERE mutes != '{}'") + + mutee_ap_ids = + Enum.flat_map( + mute_rows, + fn [_, ap_ids] -> ap_ids end + ) + |> Enum.uniq() + + # Selecting ids of all mutees at once in order to reduce the number of SELECT queries + {:ok, %{rows: mutee_ap_id_id}} = + SQL.query(Repo, "SELECT ap_id, id FROM users WHERE ap_id = ANY($1)", [mutee_ap_ids]) + + mutee_id_by_ap_id = Enum.into(mutee_ap_id_id, %{}, fn [k, v] -> {k, v} end) + + Enum.each( + mute_rows, + fn [muter_id, mutee_ap_ids] -> + muter_uuid = Ecto.UUID.cast!(muter_id) + + for mutee_ap_id <- mutee_ap_ids do + mutee_id = mutee_id_by_ap_id[mutee_ap_id] + + with {:ok, mutee_uuid} <- mutee_id && Ecto.UUID.cast(mutee_id) do + execute( + "INSERT INTO user_mutes(muter_id, mutee_id, inserted_at) " <> + "VALUES('#{muter_uuid}'::uuid, '#{mutee_uuid}'::uuid, now()) " <> + "ON CONFLICT (muter_id, mutee_id) DO NOTHING" + ) + else + _ -> Logger.warn("Missing reference: (#{muter_uuid}, #{mutee_id})") + end + end + end + ) + end + + def down, do: :noop +end diff --git a/test/notification_test.exs b/test/notification_test.exs index 5388214ba..d3d409c68 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -112,7 +112,7 @@ test "notification created if user is muted without notifications" do muter = insert(:user) muted = insert(:user) - {:ok, muter} = User.mute(muter, muted, false) + {:ok, _user_mute} = User.mute(muter, muted, false) {:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"}) @@ -636,7 +636,7 @@ test "notifications are deleted if a remote user is deleted" do test "it returns notifications for muted user without notifications" do user = insert(:user) muted = insert(:user) - {:ok, user} = User.mute(user, muted, false) + {:ok, _user_mute} = User.mute(user, muted, false) {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"}) @@ -646,7 +646,10 @@ test "it returns notifications for muted user without notifications" do test "it doesn't return notifications for muted user with notifications" do user = insert(:user) muted = insert(:user) - {:ok, user} = User.mute(user, muted) + {:ok, _user_mute} = User.mute(user, muted) + + # Refreshing to reflect embedded ap id relation fields (remove once removed) + user = refresh_record(user) {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"}) @@ -686,7 +689,7 @@ test "it doesn't return notifications for muted thread" do test "it returns notifications from a muted user when with_muted is set" do user = insert(:user) muted = insert(:user) - {:ok, user} = User.mute(user, muted) + {:ok, _user_mute} = User.mute(user, muted) {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"}) diff --git a/test/user_test.exs b/test/user_test.exs index fa8d336f2..15b4f67f6 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -673,7 +673,10 @@ test "it mutes people" do refute User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) - {:ok, user} = User.mute(user, muted_user) + {:ok, _user_mute} = User.mute(user, muted_user) + + # Refreshing to reflect embedded ap id relation fields (remove once removed) + user = refresh_record(user) assert User.mutes?(user, muted_user) assert User.muted_notifications?(user, muted_user) @@ -683,8 +686,8 @@ test "it unmutes users" do user = insert(:user) muted_user = insert(:user) - {:ok, user} = User.mute(user, muted_user) - {:ok, user} = User.unmute(user, muted_user) + {:ok, _user_mute} = User.mute(user, muted_user) + {:ok, _user_mute} = User.unmute(user, muted_user) refute User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) @@ -697,7 +700,7 @@ test "it mutes user without notifications" do refute User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) - {:ok, user} = User.mute(user, muted_user, false) + {:ok, _user_mute} = User.mute(user, muted_user, false) assert User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 17d283954..de8472691 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -611,7 +611,9 @@ test "doesn't return muted activities" do activity_three = insert(:note_activity) user = insert(:user) booster = insert(:user) - {:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]}) + + activity_one_actor = User.get_by_ap_id(activity_one.data["actor"]) + {:ok, _user_mute} = User.mute(user, activity_one_actor) activities = ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) @@ -632,7 +634,7 @@ test "doesn't return muted activities" do assert Enum.member?(activities, activity_three) assert Enum.member?(activities, activity_one) - {:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]}) + {:ok, _user_mute} = User.unmute(user, activity_one_actor) activities = ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) @@ -641,7 +643,8 @@ test "doesn't return muted activities" do assert Enum.member?(activities, activity_three) assert Enum.member?(activities, activity_one) - {:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]}) + activity_three_actor = User.get_by_ap_id(activity_three.data["actor"]) + {:ok, _user_mute} = User.mute(user, activity_three_actor) {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster) %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) activity_three = Activity.get_by_id(activity_three.id) diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index b98d9c8e3..c5ef96bb7 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -868,7 +868,7 @@ test "getting a list of mutes", %{conn: conn} do user = insert(:user) other_user = insert(:user) - {:ok, user} = User.mute(user, other_user) + {:ok, _user_mute} = User.mute(user, other_user) conn = conn diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index fa55a7cf9..75b871a43 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -289,7 +289,10 @@ test "doesn't see notifications after muting user with notifications", %{conn: c assert length(json_response(conn, 200)) == 1 - {:ok, user} = User.mute(user, user2) + {:ok, _user_mute} = User.mute(user, user2) + + # Refreshing to reflect embedded ap id relation fields (remove once removed) + user = refresh_record(user) conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications") @@ -310,7 +313,7 @@ test "see notifications after muting user without notifications", %{conn: conn} assert length(json_response(conn, 200)) == 1 - {:ok, user} = User.mute(user, user2, false) + {:ok, _user_mute} = User.mute(user, user2, false) conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications") @@ -333,7 +336,7 @@ test "see notifications after muting user with notifications and with_muted para assert length(json_response(conn, 200)) == 1 - {:ok, user} = User.mute(user, user2) + {:ok, _user_mute} = User.mute(user, user2) conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"}) diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index c34bf7ba9..f8b74553b 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -191,9 +191,12 @@ test "represent a relationship for the following and followed user" do {:ok, user} = User.follow(user, other_user) {:ok, other_user} = User.follow(other_user, user) {:ok, other_user} = User.subscribe(user, other_user) - {:ok, user} = User.mute(user, other_user, true) + {:ok, _user_mute} = User.mute(user, other_user, true) {:ok, user} = CommonAPI.hide_reblogs(user, other_user) + # Refreshing to reflect embedded ap id relation fields (remove once removed) + user = refresh_record(user) + expected = %{ id: to_string(other_user.id), following: true, diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index d46ecc646..89b146e1e 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -183,7 +183,7 @@ test "tells if the message is muted for some reason" do user = insert(:user) other_user = insert(:user) - {:ok, user} = User.mute(user, other_user) + {:ok, _user_mute} = User.mute(user, other_user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) status = StatusView.render("show.json", %{activity: activity}) @@ -199,7 +199,7 @@ test "tells if the message is thread muted" do user = insert(:user) other_user = insert(:user) - {:ok, user} = User.mute(user, other_user) + {:ok, _user_mute} = User.mute(user, other_user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) status = StatusView.render("show.json", %{activity: activity, for: user}) From aad6576130c3e47a5fcc102c736ce6414c0efd7a Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 18 Nov 2019 20:38:56 +0300 Subject: [PATCH 11/70] [#1335] Refactored UserMute and UserBlock into UserRelationship, introduced EctoEnum. --- lib/pleroma/ecto_enums.ex | 7 ++ lib/pleroma/user.ex | 99 ++++++++++--------- lib/pleroma/user/search.ex | 6 +- lib/pleroma/user_block.ex | 71 ------------- lib/pleroma/user_mute.ex | 71 ------------- lib/pleroma/user_relationship.ex | 90 +++++++++++++++++ lib/pleroma/web/streamer/worker.ex | 4 +- mix.exs | 1 + mix.lock | 1 + .../20191108161911_create_user_blocks.exs | 14 --- ...11_data_migration_populate_user_blocks.exs | 49 --------- .../20191112151559_create_user_mutes.exs | 14 --- ...614_data_migration_populate_user_mutes.exs | 48 --------- ...191118084425_create_user_relationships.exs | 17 ++++ ..._migration_populate_user_relationships.exs | 64 ++++++++++++ 15 files changed, 239 insertions(+), 317 deletions(-) create mode 100644 lib/pleroma/ecto_enums.ex delete mode 100644 lib/pleroma/user_block.ex delete mode 100644 lib/pleroma/user_mute.ex create mode 100644 lib/pleroma/user_relationship.ex delete mode 100644 priv/repo/migrations/20191108161911_create_user_blocks.exs delete mode 100644 priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs delete mode 100644 priv/repo/migrations/20191112151559_create_user_mutes.exs delete mode 100644 priv/repo/migrations/20191112151614_data_migration_populate_user_mutes.exs create mode 100644 priv/repo/migrations/20191118084425_create_user_relationships.exs create mode 100644 priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex new file mode 100644 index 000000000..bad5ec523 --- /dev/null +++ b/lib/pleroma/ecto_enums.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +import EctoEnum + +defenum(UserRelationshipTypeEnum, block: 1, mute: 2, reblog_mute: 3, notification_mute: 4) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index d6bc02d04..4dd03c6c6 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -22,8 +22,7 @@ defmodule Pleroma.User do alias Pleroma.Repo alias Pleroma.RepoStreamer alias Pleroma.User - alias Pleroma.UserBlock - alias Pleroma.UserMute + alias Pleroma.UserRelationship alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils @@ -78,7 +77,6 @@ defmodule Pleroma.User do field(:confirmation_token, :string, default: nil) field(:default_scope, :string, default: "public") field(:domain_blocks, {:array, :string}, default: []) - field(:mutes, {:array, :string}, default: []) field(:muted_reblogs, {:array, :string}, default: []) field(:muted_notifications, {:array, :string}, default: []) field(:subscribers, {:array, :string}, default: []) @@ -121,20 +119,41 @@ defmodule Pleroma.User do has_many(:registrations, Registration) has_many(:deliveries, Delivery) - has_many(:blocker_blocks, UserBlock, foreign_key: :blocker_id) - has_many(:blockee_blocks, UserBlock, foreign_key: :blockee_id) - has_many(:blocked_users, through: [:blocker_blocks, :blockee]) - has_many(:blocker_users, through: [:blockee_blocks, :blocker]) + has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id) + has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id) - has_many(:muter_mutes, UserMute, foreign_key: :muter_id) - has_many(:mutee_mutes, UserMute, foreign_key: :mutee_id) - has_many(:muted_users, through: [:muter_mutes, :mutee]) - has_many(:muter_users, through: [:mutee_mutes, :muter]) + has_many(:blocker_blocks, UserRelationship, + foreign_key: :source_id, + where: [relationship_type: :block] + ) + + has_many(:blockee_blocks, UserRelationship, + foreign_key: :target_id, + where: [relationship_type: :block] + ) + + has_many(:blocked_users, through: [:blocker_blocks, :target]) + has_many(:blocker_users, through: [:blockee_blocks, :source]) + + has_many(:muter_mutes, UserRelationship, + foreign_key: :source_id, + where: [relationship_type: :mute] + ) + + has_many(:mutee_mutes, UserRelationship, + foreign_key: :target_id, + where: [relationship_type: :mute] + ) + + has_many(:muted_users, through: [:muter_mutes, :target]) + has_many(:muter_users, through: [:mutee_mutes, :source]) field(:info, :map, default: %{}) # `:blocks` is deprecated (replaced with `blocked_users` relation) field(:blocks, {:array, :string}, default: []) + # `:mutes` is deprecated (replaced with `muted_users` relation) + field(:mutes, {:array, :string}, default: []) timestamps() end @@ -1054,7 +1073,7 @@ def mutes?(nil, _), do: false def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target) def mutes_user?(%User{} = user, %User{} = target) do - UserMute.exists?(user, target) + UserRelationship.mute_exists?(user, target) end @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean() @@ -1070,7 +1089,7 @@ def blocks?(%User{} = user, %User{} = target) do end def blocks_user?(%User{} = user, %User{} = target) do - UserBlock.exists?(user, target) + UserRelationship.block_exists?(user, target) end def blocks_user?(_, _), do: false @@ -1119,33 +1138,20 @@ def blocked_ap_ids(user) do |> Repo.all() end - defp related_ap_ids_sql(join_table, source_column, target_column) do - "(SELECT array_agg(u.ap_id) FROM users as u " <> - "INNER JOIN #{join_table} AS join_table " <> - "ON join_table.#{source_column} = $1 " <> - "WHERE u.id = join_table.#{target_column})" - end - - @related_ap_ids_sql_params %{ - blocked_users: ["user_blocks", "blocker_id", "blockee_id"], - muted_users: ["user_mutes", "muter_id", "mutee_id"] - } - - def related_ap_ids(user, relations) when is_list(relations) do - query = - relations - |> Enum.map(fn r -> @related_ap_ids_sql_params[r] end) - |> Enum.filter(& &1) - |> Enum.map(fn [join_table, source_column, target_column] -> - related_ap_ids_sql(join_table, source_column, target_column) - end) - |> Enum.join(", ") - - with {:ok, %{rows: [ap_ids_arrays]}} <- - Repo.query("SELECT #{query}", [FlakeId.from_string(user.id)]) do - ap_ids_arrays = Enum.map(ap_ids_arrays, &(&1 || [])) - {:ok, ap_ids_arrays} - end + @doc """ + Returns map of related AP IDs list by relation type. + E.g. `related_ap_ids(user, [:blocks])` -> `%{blocks: ["https://some.site/users/userapid"]}` + """ + @spec related_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())} + def related_ap_ids(%User{} = user, relationship_types) when is_list(relationship_types) do + user + |> assoc(:outgoing_relationships) + |> join(:inner, [user_rel], u in assoc(user_rel, :target)) + |> where([user_rel, u], user_rel.relationship_type in ^relationship_types) + |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)]) + |> group_by([user_rel, u], user_rel.relationship_type) + |> Repo.all() + |> Enum.into(%{}, fn [k, v] -> {k, v} end) end @spec subscribers(User.t()) :: [User.t()] @@ -1918,19 +1924,20 @@ def unblock_domain(user, domain_blocked) do set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked)) end - @spec add_to_block(User.t(), User.t()) :: {:ok, UserBlock.t()} | {:error, Ecto.Changeset.t()} + @spec add_to_block(User.t(), User.t()) :: + {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()} defp add_to_block(%User{} = user, %User{} = blocked) do - UserBlock.create(user, blocked) + UserRelationship.create_block(user, blocked) end @spec add_to_block(User.t(), User.t()) :: - {:ok, UserBlock.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()} + {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()} defp remove_from_block(%User{} = user, %User{} = blocked) do - UserBlock.delete(user, blocked) + UserRelationship.delete_block(user, blocked) end defp add_to_mutes(%User{} = user, %User{ap_id: ap_id} = muted_user, notifications?) do - with {:ok, user_mute} <- UserMute.create(user, muted_user), + with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user), {:ok, _user} <- set_notification_mutes( user, @@ -1942,7 +1949,7 @@ defp add_to_mutes(%User{} = user, %User{ap_id: ap_id} = muted_user, notification end defp remove_from_mutes(user, %User{ap_id: ap_id} = muted_user) do - with {:ok, user_mute} <- UserMute.delete(user, muted_user), + with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user), {:ok, _user} <- set_notification_mutes( user, diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 049e1a634..2b3c7b5b4 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -100,11 +100,11 @@ defp base_query(user, true), do: User.get_followers_query(user) defp filter_blocked_user(query, %User{} = blocker) do query - |> join(:left, [u], b in Pleroma.UserBlock, + |> join(:left, [u], b in Pleroma.UserRelationship, as: :blocks, - on: b.blocker_id == ^blocker.id and u.id == b.blockee_id + on: b.relationship_type == ^:block and b.source_id == ^blocker.id and u.id == b.target_id ) - |> where([blocks: b], is_nil(b.blockee_id)) + |> where([blocks: b], is_nil(b.target_id)) end defp filter_blocked_user(query, _), do: query diff --git a/lib/pleroma/user_block.ex b/lib/pleroma/user_block.ex deleted file mode 100644 index bcf4b64d9..000000000 --- a/lib/pleroma/user_block.ex +++ /dev/null @@ -1,71 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UserBlock do - use Ecto.Schema - - import Ecto.Changeset - import Ecto.Query - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.UserBlock - - schema "user_blocks" do - belongs_to(:blocker, User, type: FlakeId.Ecto.CompatType) - belongs_to(:blockee, User, type: FlakeId.Ecto.CompatType) - - timestamps(updated_at: false) - end - - def changeset(%UserBlock{} = user_block, params \\ %{}) do - user_block - |> cast(params, [:blocker_id, :blockee_id]) - |> validate_required([:blocker_id, :blockee_id]) - |> unique_constraint(:blockee_id, name: :user_blocks_blocker_id_blockee_id_index) - |> validate_not_self_block() - end - - def exists?(%User{} = blocker, %User{} = blockee) do - UserBlock - |> where(blocker_id: ^blocker.id, blockee_id: ^blockee.id) - |> Repo.exists?() - end - - def create(%User{} = blocker, %User{} = blockee) do - %UserBlock{} - |> changeset(%{blocker_id: blocker.id, blockee_id: blockee.id}) - |> Repo.insert( - on_conflict: :replace_all_except_primary_key, - conflict_target: [:blocker_id, :blockee_id] - ) - end - - def delete(%User{} = blocker, %User{} = blockee) do - attrs = %{blocker_id: blocker.id, blockee_id: blockee.id} - - case Repo.get_by(UserBlock, attrs) do - %UserBlock{} = existing_record -> Repo.delete(existing_record) - nil -> {:ok, nil} - end - end - - defp validate_not_self_block(%Ecto.Changeset{} = changeset) do - changeset - |> validate_change(:blockee_id, fn _, blockee_id -> - if blockee_id == get_field(changeset, :blocker_id) do - [blockee_id: "can't be equal to blocker_id"] - else - [] - end - end) - |> validate_change(:blocker_id, fn _, blocker_id -> - if blocker_id == get_field(changeset, :blockee_id) do - [blocker_id: "can't be equal to blockee_id"] - else - [] - end - end) - end -end diff --git a/lib/pleroma/user_mute.ex b/lib/pleroma/user_mute.ex deleted file mode 100644 index 417a5ff84..000000000 --- a/lib/pleroma/user_mute.ex +++ /dev/null @@ -1,71 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UserMute do - use Ecto.Schema - - import Ecto.Changeset - import Ecto.Query - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.UserMute - - schema "user_mutes" do - belongs_to(:muter, User, type: FlakeId.Ecto.CompatType) - belongs_to(:mutee, User, type: FlakeId.Ecto.CompatType) - - timestamps(updated_at: false) - end - - def changeset(%UserMute{} = user_mute, params \\ %{}) do - user_mute - |> cast(params, [:muter_id, :mutee_id]) - |> validate_required([:muter_id, :mutee_id]) - |> unique_constraint(:mutee_id, name: :user_mutes_muter_id_mutee_id_index) - |> validate_not_self_mute() - end - - def exists?(%User{} = muter, %User{} = mutee) do - UserMute - |> where(muter_id: ^muter.id, mutee_id: ^mutee.id) - |> Repo.exists?() - end - - def create(%User{} = muter, %User{} = mutee) do - %UserMute{} - |> changeset(%{muter_id: muter.id, mutee_id: mutee.id}) - |> Repo.insert( - on_conflict: :replace_all_except_primary_key, - conflict_target: [:muter_id, :mutee_id] - ) - end - - def delete(%User{} = muter, %User{} = mutee) do - attrs = %{muter_id: muter.id, mutee_id: mutee.id} - - case Repo.get_by(UserMute, attrs) do - %UserMute{} = existing_record -> Repo.delete(existing_record) - nil -> {:ok, nil} - end - end - - defp validate_not_self_mute(%Ecto.Changeset{} = changeset) do - changeset - |> validate_change(:mutee_id, fn _, mutee_id -> - if mutee_id == get_field(changeset, :muter_id) do - [mutee_id: "can't be equal to muter_id"] - else - [] - end - end) - |> validate_change(:muter_id, fn _, muter_id -> - if muter_id == get_field(changeset, :mutee_id) do - [muter_id: "can't be equal to mutee_id"] - else - [] - end - end) - end -end diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex new file mode 100644 index 000000000..5cb99ae50 --- /dev/null +++ b/lib/pleroma/user_relationship.ex @@ -0,0 +1,90 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserRelationship do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.UserRelationship + + schema "user_relationships" do + belongs_to(:source, User, type: FlakeId.Ecto.CompatType) + belongs_to(:target, User, type: FlakeId.Ecto.CompatType) + field(:relationship_type, UserRelationshipTypeEnum) + + timestamps(updated_at: false) + end + + def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do + user_relationship + |> cast(params, [:relationship_type, :source_id, :target_id]) + |> validate_required([:relationship_type, :source_id, :target_id]) + |> unique_constraint(:relationship_type, + name: :user_relationships_source_id_relationship_type_target_id_index + ) + |> validate_not_self_relationship() + end + + def exists?(relationship_type, %User{} = source, %User{} = target) do + UserRelationship + |> where(relationship_type: ^relationship_type, source_id: ^source.id, target_id: ^target.id) + |> Repo.exists?() + end + + def block_exists?(%User{} = blocker, %User{} = blockee), do: exists?(:block, blocker, blockee) + + def mute_exists?(%User{} = muter, %User{} = mutee), do: exists?(:mute, muter, mutee) + + def create(relationship_type, %User{} = source, %User{} = target) do + %UserRelationship{} + |> changeset(%{ + relationship_type: relationship_type, + source_id: source.id, + target_id: target.id + }) + |> Repo.insert( + on_conflict: :replace_all_except_primary_key, + conflict_target: [:source_id, :relationship_type, :target_id] + ) + end + + def create_block(%User{} = blocker, %User{} = blockee), do: create(:block, blocker, blockee) + + def create_mute(%User{} = muter, %User{} = mutee), do: create(:mute, muter, mutee) + + def delete(relationship_type, %User{} = source, %User{} = target) do + attrs = %{relationship_type: relationship_type, source_id: source.id, target_id: target.id} + + case Repo.get_by(UserRelationship, attrs) do + %UserRelationship{} = existing_record -> Repo.delete(existing_record) + nil -> {:ok, nil} + end + end + + def delete_block(%User{} = blocker, %User{} = blockee), do: delete(:block, blocker, blockee) + + def delete_mute(%User{} = muter, %User{} = mutee), do: delete(:mute, muter, mutee) + + defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do + changeset + |> validate_change(:target_id, fn _, target_id -> + if target_id == get_field(changeset, :source_id) do + [target_id: "can't be equal to source_id"] + else + [] + end + end) + |> validate_change(:source_id, fn _, source_id -> + if source_id == get_field(changeset, :target_id) do + [source_id: "can't be equal to target_id"] + else + [] + end + end) + end +end diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex index 45f9c7c53..020112949 100644 --- a/lib/pleroma/web/streamer/worker.ex +++ b/lib/pleroma/web/streamer/worker.ex @@ -129,7 +129,9 @@ defp do_stream(%{topic: topic, item: item}) do end defp should_send?(%User{} = user, %Activity{} = item) do - {:ok, [blocks, mutes]} = User.related_ap_ids(user, [:blocked_users, :muted_users]) + related_ap_ids = User.related_ap_ids(user, [:block, :mute]) + blocks = related_ap_ids[:block] || [] + mutes = related_ap_ids[:mute] || [] reblog_mutes = user.muted_reblogs || [] recipient_blocks = MapSet.new(blocks ++ mutes) recipients = MapSet.new(item.recipients) diff --git a/mix.exs b/mix.exs index 81ce4f25c..c870b330f 100644 --- a/mix.exs +++ b/mix.exs @@ -100,6 +100,7 @@ defp deps do {:plug_cowboy, "~> 2.0"}, {:phoenix_pubsub, "~> 1.1"}, {:phoenix_ecto, "~> 4.0"}, + {:ecto_enum, "~> 1.4"}, {:ecto_sql, "~> 3.2"}, {:postgrex, ">= 0.13.5"}, {:oban, "~> 0.8.1"}, diff --git a/mix.lock b/mix.lock index d4a80df77..fc092b79f 100644 --- a/mix.lock +++ b/mix.lock @@ -24,6 +24,7 @@ "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"}, "ecto": {:hex, :ecto, "3.2.3", "51274df79862845b388733fddcf6f107d0c8c86e27abe7131fa98f8d30761bda", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"}, "ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"}, diff --git a/priv/repo/migrations/20191108161911_create_user_blocks.exs b/priv/repo/migrations/20191108161911_create_user_blocks.exs deleted file mode 100644 index c882d2bd6..000000000 --- a/priv/repo/migrations/20191108161911_create_user_blocks.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Pleroma.Repo.Migrations.CreateUserBlocks do - use Ecto.Migration - - def change do - create_if_not_exists table(:user_blocks) do - add(:blocker_id, references(:users, type: :uuid, on_delete: :delete_all)) - add(:blockee_id, references(:users, type: :uuid, on_delete: :delete_all)) - - timestamps(updated_at: false) - end - - create_if_not_exists(unique_index(:user_blocks, [:blocker_id, :blockee_id])) - end -end diff --git a/priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs b/priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs deleted file mode 100644 index fe537679d..000000000 --- a/priv/repo/migrations/20191108173911_data_migration_populate_user_blocks.exs +++ /dev/null @@ -1,49 +0,0 @@ -defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserBlocks do - use Ecto.Migration - - alias Ecto.Adapters.SQL - alias Pleroma.Repo - - require Logger - - def up do - {:ok, %{rows: block_rows}} = - SQL.query(Repo, "SELECT id, blocks FROM users WHERE blocks != '{}'") - - blockee_ap_ids = - Enum.flat_map( - block_rows, - fn [_, ap_ids] -> ap_ids end - ) - |> Enum.uniq() - - # Selecting ids of all blockees at once in order to reduce the number of SELECT queries - {:ok, %{rows: blockee_ap_id_id}} = - SQL.query(Repo, "SELECT ap_id, id FROM users WHERE ap_id = ANY($1)", [blockee_ap_ids]) - - blockee_id_by_ap_id = Enum.into(blockee_ap_id_id, %{}, fn [k, v] -> {k, v} end) - - Enum.each( - block_rows, - fn [blocker_id, blockee_ap_ids] -> - blocker_uuid = Ecto.UUID.cast!(blocker_id) - - for blockee_ap_id <- blockee_ap_ids do - blockee_id = blockee_id_by_ap_id[blockee_ap_id] - - with {:ok, blockee_uuid} <- blockee_id && Ecto.UUID.cast(blockee_id) do - execute( - "INSERT INTO user_blocks(blocker_id, blockee_id, inserted_at) " <> - "VALUES('#{blocker_uuid}'::uuid, '#{blockee_uuid}'::uuid, now()) " <> - "ON CONFLICT (blocker_id, blockee_id) DO NOTHING" - ) - else - _ -> Logger.warn("Missing reference: (#{blocker_uuid}, #{blockee_id})") - end - end - end - ) - end - - def down, do: :noop -end diff --git a/priv/repo/migrations/20191112151559_create_user_mutes.exs b/priv/repo/migrations/20191112151559_create_user_mutes.exs deleted file mode 100644 index eaa285de2..000000000 --- a/priv/repo/migrations/20191112151559_create_user_mutes.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Pleroma.Repo.Migrations.CreateUserMutes do - use Ecto.Migration - - def change do - create_if_not_exists table(:user_mutes) do - add(:muter_id, references(:users, type: :uuid, on_delete: :delete_all)) - add(:mutee_id, references(:users, type: :uuid, on_delete: :delete_all)) - - timestamps(updated_at: false) - end - - create_if_not_exists(unique_index(:user_mutes, [:muter_id, :mutee_id])) - end -end diff --git a/priv/repo/migrations/20191112151614_data_migration_populate_user_mutes.exs b/priv/repo/migrations/20191112151614_data_migration_populate_user_mutes.exs deleted file mode 100644 index a8bdd072e..000000000 --- a/priv/repo/migrations/20191112151614_data_migration_populate_user_mutes.exs +++ /dev/null @@ -1,48 +0,0 @@ -defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserMutes do - use Ecto.Migration - - alias Ecto.Adapters.SQL - alias Pleroma.Repo - - require Logger - - def up do - {:ok, %{rows: mute_rows}} = SQL.query(Repo, "SELECT id, mutes FROM users WHERE mutes != '{}'") - - mutee_ap_ids = - Enum.flat_map( - mute_rows, - fn [_, ap_ids] -> ap_ids end - ) - |> Enum.uniq() - - # Selecting ids of all mutees at once in order to reduce the number of SELECT queries - {:ok, %{rows: mutee_ap_id_id}} = - SQL.query(Repo, "SELECT ap_id, id FROM users WHERE ap_id = ANY($1)", [mutee_ap_ids]) - - mutee_id_by_ap_id = Enum.into(mutee_ap_id_id, %{}, fn [k, v] -> {k, v} end) - - Enum.each( - mute_rows, - fn [muter_id, mutee_ap_ids] -> - muter_uuid = Ecto.UUID.cast!(muter_id) - - for mutee_ap_id <- mutee_ap_ids do - mutee_id = mutee_id_by_ap_id[mutee_ap_id] - - with {:ok, mutee_uuid} <- mutee_id && Ecto.UUID.cast(mutee_id) do - execute( - "INSERT INTO user_mutes(muter_id, mutee_id, inserted_at) " <> - "VALUES('#{muter_uuid}'::uuid, '#{mutee_uuid}'::uuid, now()) " <> - "ON CONFLICT (muter_id, mutee_id) DO NOTHING" - ) - else - _ -> Logger.warn("Missing reference: (#{muter_uuid}, #{mutee_id})") - end - end - end - ) - end - - def down, do: :noop -end diff --git a/priv/repo/migrations/20191118084425_create_user_relationships.exs b/priv/repo/migrations/20191118084425_create_user_relationships.exs new file mode 100644 index 000000000..c281f887d --- /dev/null +++ b/priv/repo/migrations/20191118084425_create_user_relationships.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.CreateUserRelationships do + use Ecto.Migration + + def change do + create_if_not_exists table(:user_relationships) do + add(:source_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:target_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:relationship_type, :integer, null: false) + + timestamps(updated_at: false) + end + + create_if_not_exists( + unique_index(:user_relationships, [:source_id, :relationship_type, :target_id]) + ) + end +end diff --git a/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs b/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs new file mode 100644 index 000000000..f8dde7626 --- /dev/null +++ b/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs @@ -0,0 +1,64 @@ +defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserRelationships do + use Ecto.Migration + + alias Ecto.Adapters.SQL + alias Pleroma.Repo + + require Logger + + def up do + Enum.each( + [blocks: 1, mutes: 2, muted_reblogs: 3, muted_notifications: 4], + fn {field, relationship_type_code} -> + migrate(field, relationship_type_code) + end + ) + end + + def down, do: :noop + + defp migrate(field, relationship_type_code) do + Logger.info("Processing users.#{field}...") + + {:ok, %{rows: field_rows}} = + SQL.query(Repo, "SELECT id, #{field} FROM users WHERE #{field} != '{}'") + + target_ap_ids = + Enum.flat_map( + field_rows, + fn [_, ap_ids] -> ap_ids end + ) + |> Enum.uniq() + + # Selecting ids of all targets at once in order to reduce the number of SELECT queries + {:ok, %{rows: target_ap_id_id}} = + SQL.query(Repo, "SELECT ap_id, id FROM users WHERE ap_id = ANY($1)", [target_ap_ids]) + + target_id_by_ap_id = Enum.into(target_ap_id_id, %{}, fn [k, v] -> {k, v} end) + + Enum.each( + field_rows, + fn [source_id, target_ap_ids] -> + source_uuid = Ecto.UUID.cast!(source_id) + + for target_ap_id <- target_ap_ids do + target_id = target_id_by_ap_id[target_ap_id] + + with {:ok, target_uuid} <- target_id && Ecto.UUID.cast(target_id) do + execute(""" + INSERT INTO user_relationships( + source_id, target_id, relationship_type, inserted_at + ) + VALUES( + '#{source_uuid}'::uuid, '#{target_uuid}'::uuid, #{relationship_type_code}, now() + ) + ON CONFLICT (source_id, relationship_type, target_id) DO NOTHING + """) + else + _ -> Logger.warn("Unresolved #{field} reference: (#{source_uuid}, #{target_id})") + end + end + end + ) + end +end From 1fcd579b6d8c26557dcc6f9d3c9f247d03e7b5a4 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 19 Nov 2019 21:11:15 +0300 Subject: [PATCH 12/70] benchmarks/ added favourites timeline --- benchmarks/load_testing/fetcher.ex | 13 ++++++++++++- benchmarks/load_testing/generator.ex | 18 ++++++++++++++++++ benchmarks/mix/tasks/pleroma/load_testing.ex | 4 ++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex index cdc073b2e..776105d34 100644 --- a/benchmarks/load_testing/fetcher.ex +++ b/benchmarks/load_testing/fetcher.ex @@ -57,6 +57,9 @@ def query_timelines(user) do Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities( mastodon_federated_timeline_params ) + end, + "User favourites timeline" => fn -> + Pleroma.Web.ActivityPub.ActivityPub.fetch_favourites(user) end }) @@ -74,6 +77,8 @@ def query_timelines(user) do mastodon_federated_timeline_params ) + user_favourites = Pleroma.Web.ActivityPub.ActivityPub.fetch_favourites(user) + Benchee.run(%{ "Rendering home timeline" => fn -> Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ @@ -95,7 +100,13 @@ def query_timelines(user) do for: user, as: :activity }) - end + end, + "Rendering favorites timeline" => fn -> + Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ + activities: user_favourites, + for: user, + as: :activity}) + end, }) end diff --git a/benchmarks/load_testing/generator.ex b/benchmarks/load_testing/generator.ex index b4432bdb7..f42fd363e 100644 --- a/benchmarks/load_testing/generator.ex +++ b/benchmarks/load_testing/generator.ex @@ -2,6 +2,24 @@ defmodule Pleroma.LoadTesting.Generator do use Pleroma.LoadTesting.Helper alias Pleroma.Web.CommonAPI + def generate_like_activities(user, posts) do + count_likes = Kernel.trunc(length(posts) / 4) + IO.puts("Starting generating #{count_likes} like activities...") + + {time, _} = + :timer.tc(fn -> + Task.async_stream( + Enum.take_random(posts, count_likes), + fn post -> {:ok, _, _} = CommonAPI.favorite(post.id, user) end, + max_concurrency: 10, + timeout: 30_000 + ) + |> Stream.run() + end) + + IO.puts("Inserting like activities take #{to_sec(time)} sec.\n") + end + def generate_users(opts) do IO.puts("Starting generating #{opts[:users_max]} users...") {time, _} = :timer.tc(fn -> do_generate_users(opts) end) diff --git a/benchmarks/mix/tasks/pleroma/load_testing.ex b/benchmarks/mix/tasks/pleroma/load_testing.ex index 4fa3eec49..0a751adac 100644 --- a/benchmarks/mix/tasks/pleroma/load_testing.ex +++ b/benchmarks/mix/tasks/pleroma/load_testing.ex @@ -100,6 +100,10 @@ def run(args) do generate_remote_activities(user, remote_users) + generate_like_activities( + user, Pleroma.Repo.all(Pleroma.Activity.Queries.by_type("Create")) + ) + generate_dms(user, users, opts) {:ok, activity} = generate_long_thread(user, users, opts) From ba5cc3016514080b4bf7eaefd3e25936c0e222ba Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 19 Nov 2019 23:22:10 +0300 Subject: [PATCH 13/70] [#1335] Implemented notification mutes and reblog mutes as UserRelationships. User to UserRelationship relations and functions refactoring. --- lib/pleroma/notification.ex | 6 +- lib/pleroma/user.ex | 216 +++++++++--------- lib/pleroma/user_relationship.ex | 26 ++- lib/pleroma/web/activity_pub/activity_pub.ex | 6 +- lib/pleroma/web/common_api/common_api.ex | 13 +- .../controllers/account_controller.ex | 14 +- lib/pleroma/web/mastodon_api/mastodon_api.ex | 11 +- lib/pleroma/web/streamer/worker.ex | 14 +- test/conversation/participation_test.exs | 6 +- test/notification_test.exs | 17 +- test/user_test.exs | 33 ++- test/web/activity_pub/activity_pub_test.exs | 20 +- .../transmogrifier/follow_handling_test.exs | 2 +- test/web/common_api/common_api_test.exs | 6 +- .../controllers/account_controller_test.exs | 4 +- .../notification_controller_test.exs | 9 +- .../controllers/status_controller_test.exs | 4 +- .../controllers/timeline_controller_test.exs | 2 +- .../mastodon_api/views/account_view_test.exs | 13 +- .../mastodon_api/views/status_view_test.exs | 4 +- test/web/streamer/streamer_test.exs | 6 +- 21 files changed, 203 insertions(+), 229 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 82faef85e..15b3337f4 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -60,7 +60,7 @@ def for_user_query(user, opts \\ []) do end defp exclude_blocked(query, user) do - blocked_ap_ids = User.blocked_ap_ids(user) + blocked_ap_ids = User.blocked_users_ap_ids(user) query |> where([n, a], a.actor not in ^blocked_ap_ids) @@ -75,8 +75,10 @@ defp exclude_muted(query, _, %{with_muted: true}) do end defp exclude_muted(query, user, _opts) do + notification_muted_ap_ids = User.notification_muted_users_ap_ids(user) + query - |> where([n, a], a.actor not in ^user.muted_notifications) + |> where([n, a], a.actor not in ^notification_muted_ap_ids) |> join(:left, [n, a], tm in Pleroma.ThreadMute, on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data) ) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 4dd03c6c6..615f1b725 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -44,6 +44,20 @@ defmodule Pleroma.User do @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/ @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/ + # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]] + @user_relationships_config [ + block: [blocker_blocks: :blocked_users, blockee_blocks: :blocker_users], + mute: [muter_mutes: :muted_users, mutee_mutes: :muter_users], + reblog_mute: [ + reblog_muter_mutes: :reblog_muted_users, + reblog_mutee_mutes: :reblog_muter_users + ], + notification_mute: [ + notification_muter_mutes: :notification_muted_users, + notification_mutee_mutes: :notification_muter_users + ] + ] + schema "users" do field(:bio, :string) field(:email, :string) @@ -63,7 +77,6 @@ defmodule Pleroma.User do field(:tags, {:array, :string}, default: []) field(:last_refreshed_at, :naive_datetime_usec) field(:last_digest_emailed_at, :naive_datetime) - field(:banner, :map, default: %{}) field(:background, :map, default: %{}) field(:source_data, :map, default: %{}) @@ -77,8 +90,6 @@ defmodule Pleroma.User do field(:confirmation_token, :string, default: nil) field(:default_scope, :string, default: "public") field(:domain_blocks, {:array, :string}, default: []) - field(:muted_reblogs, {:array, :string}, default: []) - field(:muted_notifications, {:array, :string}, default: []) field(:subscribers, {:array, :string}, default: []) field(:deactivated, :boolean, default: false) field(:no_rich_text, :boolean, default: false) @@ -122,31 +133,29 @@ defmodule Pleroma.User do has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id) has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id) - has_many(:blocker_blocks, UserRelationship, - foreign_key: :source_id, - where: [relationship_type: :block] - ) + for {relationship_type, + [ + {outgoing_relation, outgoing_relation_target}, + {incoming_relation, incoming_relation_source} + ]} <- @user_relationships_config do + # Definitions of `has_many :blocker_blocks`, `has_many :muter_mutes` etc. + has_many(outgoing_relation, UserRelationship, + foreign_key: :source_id, + where: [relationship_type: relationship_type] + ) - has_many(:blockee_blocks, UserRelationship, - foreign_key: :target_id, - where: [relationship_type: :block] - ) + # Definitions of `has_many :blockee_blocks`, `has_many :mutee_mutes` etc. + has_many(incoming_relation, UserRelationship, + foreign_key: :target_id, + where: [relationship_type: relationship_type] + ) - has_many(:blocked_users, through: [:blocker_blocks, :target]) - has_many(:blocker_users, through: [:blockee_blocks, :source]) + # Definitions of `has_many :blocked_users`, `has_many :muted_users` etc. + has_many(outgoing_relation_target, through: [outgoing_relation, :target]) - has_many(:muter_mutes, UserRelationship, - foreign_key: :source_id, - where: [relationship_type: :mute] - ) - - has_many(:mutee_mutes, UserRelationship, - foreign_key: :target_id, - where: [relationship_type: :mute] - ) - - has_many(:muted_users, through: [:muter_mutes, :target]) - has_many(:muter_users, through: [:mutee_mutes, :source]) + # Definitions of `has_many :blocker_users`, `has_many :muter_users` etc. + has_many(incoming_relation_source, through: [incoming_relation, :source]) + end field(:info, :map, default: %{}) @@ -154,10 +163,49 @@ defmodule Pleroma.User do field(:blocks, {:array, :string}, default: []) # `:mutes` is deprecated (replaced with `muted_users` relation) field(:mutes, {:array, :string}, default: []) + # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation) + field(:muted_reblogs, {:array, :string}, default: []) + # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation) + field(:muted_notifications, {:array, :string}, default: []) timestamps() end + for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <- + @user_relationships_config do + # Definitions of `blocked_users_relation/1`, `muted_users_relation/1`, etc. + def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do + target_users_query = assoc(user, unquote(outgoing_relation_target)) + + if restrict_deactivated? do + restrict_deactivated(target_users_query) + else + target_users_query + end + end + + # Definitions of `blocked_users/1`, `muted_users/1`, etc. + def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do + __MODULE__ + |> apply(unquote(:"#{outgoing_relation_target}_relation"), [ + user, + restrict_deactivated? + ]) + |> Repo.all() + end + + # Definitions of `blocked_users_ap_ids/1`, `muted_users_ap_ids/1`, etc. + def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do + __MODULE__ + |> apply(unquote(:"#{outgoing_relation_target}_relation"), [ + user, + restrict_deactivated? + ]) + |> select([u], u.ap_id) + |> Repo.all() + end + end + @doc "Returns if the user should be allowed to authenticate" def auth_active?(%User{deactivated: true}), do: false @@ -996,7 +1044,8 @@ def get_recipients_from_activity(%Activity{recipients: to}) do |> Repo.all() end - @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()} + @spec mute(User.t(), User.t(), boolean()) :: + {:ok, list(UserRelationship.t())} | {:error, String.t()} def mute(muter, %User{} = mutee, notifications? \\ true) do add_to_mutes(muter, mutee, notifications?) end @@ -1079,8 +1128,8 @@ def mutes_user?(%User{} = user, %User{} = target) do @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean() def muted_notifications?(nil, _), do: false - def muted_notifications?(user, %{ap_id: ap_id}), - do: Enum.member?(user.muted_notifications, ap_id) + def muted_notifications?(user, %User{} = target), + do: UserRelationship.notification_mute_exists?(user, target) def blocks?(nil, _), do: false @@ -1108,50 +1157,27 @@ def subscribed_to?(user, %{ap_id: ap_id}) do end end - @spec muted_users(User.t()) :: [User.t()] - def muted_users(user) do - user - |> assoc(:muted_users) - |> restrict_deactivated() - |> Repo.all() - end - - def muted_ap_ids(user) do - user - |> assoc(:muted_users) - |> select([u], u.ap_id) - |> Repo.all() - end - - @spec blocked_users(User.t()) :: [User.t()] - def blocked_users(user) do - user - |> assoc(:blocked_users) - |> restrict_deactivated() - |> Repo.all() - end - - def blocked_ap_ids(user) do - user - |> assoc(:blocked_users) - |> select([u], u.ap_id) - |> Repo.all() - end - @doc """ Returns map of related AP IDs list by relation type. - E.g. `related_ap_ids(user, [:blocks])` -> `%{blocks: ["https://some.site/users/userapid"]}` + E.g. `related_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}` """ @spec related_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())} def related_ap_ids(%User{} = user, relationship_types) when is_list(relationship_types) do - user - |> assoc(:outgoing_relationships) - |> join(:inner, [user_rel], u in assoc(user_rel, :target)) - |> where([user_rel, u], user_rel.relationship_type in ^relationship_types) - |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)]) - |> group_by([user_rel, u], user_rel.relationship_type) - |> Repo.all() - |> Enum.into(%{}, fn [k, v] -> {k, v} end) + db_result = + user + |> assoc(:outgoing_relationships) + |> join(:inner, [user_rel], u in assoc(user_rel, :target)) + |> where([user_rel, u], user_rel.relationship_type in ^relationship_types) + |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)]) + |> group_by([user_rel, u], user_rel.relationship_type) + |> Repo.all() + |> Enum.into(%{}, fn [k, v] -> {k, v} end) + + Enum.into( + relationship_types, + %{}, + fn rel_type -> {rel_type, db_result[rel_type] || []} end + ) end @spec subscribers(User.t()) :: [User.t()] @@ -1572,7 +1598,7 @@ def all_superusers do end def showing_reblogs?(%User{} = user, %User{} = target) do - target.ap_id not in user.muted_reblogs + not UserRelationship.reblog_mute_exists?(user, target) end @doc """ @@ -1936,59 +1962,23 @@ defp remove_from_block(%User{} = user, %User{} = blocked) do UserRelationship.delete_block(user, blocked) end - defp add_to_mutes(%User{} = user, %User{ap_id: ap_id} = muted_user, notifications?) do + defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user), - {:ok, _user} <- - set_notification_mutes( - user, - Enum.uniq([ap_id | user.muted_notifications]), - notifications? - ) do - {:ok, user_mute} + {:ok, user_notification_mute} <- + (notifications? && UserRelationship.create_notification_mute(user, muted_user)) || + {:ok, nil} do + {:ok, Enum.filter([user_mute, user_notification_mute], & &1)} end end - defp remove_from_mutes(user, %User{ap_id: ap_id} = muted_user) do + defp remove_from_mutes(user, %User{} = muted_user) do with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user), - {:ok, _user} <- - set_notification_mutes( - user, - List.delete(user.muted_notifications, ap_id), - true - ) do - {:ok, user_mute} + {:ok, user_notification_mute} <- + UserRelationship.delete_notification_mute(user, muted_user) do + {:ok, [user_mute, user_notification_mute]} end end - defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do - {:ok, user} - end - - defp set_notification_mutes(user, muted_notifications, true = _notifications?) do - params = %{muted_notifications: muted_notifications} - - user - |> cast(params, [:muted_notifications]) - |> validate_required([:muted_notifications]) - |> update_and_set_cache() - end - - def add_reblog_mute(user, ap_id) do - params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]} - - user - |> cast(params, [:muted_reblogs]) - |> update_and_set_cache() - end - - def remove_reblog_mute(user, ap_id) do - params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)} - - user - |> cast(params, [:muted_reblogs]) - |> update_and_set_cache() - end - def set_invisible(user, invisible) do params = %{invisible: invisible} diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 5cb99ae50..24c724549 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -20,6 +20,20 @@ defmodule Pleroma.UserRelationship do timestamps(updated_at: false) end + for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do + # Definitions of `create_block/2`, `create_mute/2` etc. + def unquote(:"create_#{relationship_type}")(source, target), + do: create(unquote(relationship_type), source, target) + + # Definitions of `delete_block/2`, `delete_mute/2` etc. + def unquote(:"delete_#{relationship_type}")(source, target), + do: delete(unquote(relationship_type), source, target) + + # Definitions of `block_exists?/2`, `mute_exists?/2` etc. + def unquote(:"#{relationship_type}_exists?")(source, target), + do: exists?(unquote(relationship_type), source, target) + end + def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do user_relationship |> cast(params, [:relationship_type, :source_id, :target_id]) @@ -36,10 +50,6 @@ def exists?(relationship_type, %User{} = source, %User{} = target) do |> Repo.exists?() end - def block_exists?(%User{} = blocker, %User{} = blockee), do: exists?(:block, blocker, blockee) - - def mute_exists?(%User{} = muter, %User{} = mutee), do: exists?(:mute, muter, mutee) - def create(relationship_type, %User{} = source, %User{} = target) do %UserRelationship{} |> changeset(%{ @@ -53,10 +63,6 @@ def create(relationship_type, %User{} = source, %User{} = target) do ) end - def create_block(%User{} = blocker, %User{} = blockee), do: create(:block, blocker, blockee) - - def create_mute(%User{} = muter, %User{} = mutee), do: create(:mute, muter, mutee) - def delete(relationship_type, %User{} = source, %User{} = target) do attrs = %{relationship_type: relationship_type, source_id: source.id, target_id: target.id} @@ -66,10 +72,6 @@ def delete(relationship_type, %User{} = source, %User{} = target) do end end - def delete_block(%User{} = blocker, %User{} = blockee), do: delete(:block, blocker, blockee) - - def delete_mute(%User{} = muter, %User{} = mutee), do: delete(:mute, muter, mutee) - defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do changeset |> validate_change(:target_id, fn _, target_id -> diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 7a4a4791e..61b7f9280 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -884,7 +884,7 @@ defp restrict_reblogs(query, _), do: query defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do - mutes = opts["muted_ap_ids"] || User.muted_ap_ids(user) + mutes = opts["muted_ap_ids"] || User.muted_users_ap_ids(user) query = from([activity] in query, @@ -902,7 +902,7 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do defp restrict_muted(query, _), do: query defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do - blocked_ap_ids = opts["blocked_ap_ids"] || User.blocked_ap_ids(user) + blocked_ap_ids = opts["blocked_ap_ids"] || User.blocked_users_ap_ids(user) domain_blocks = user.domain_blocks || [] query = @@ -945,7 +945,7 @@ defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) defp restrict_pinned(query, _), do: query defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do - muted_reblogs = user.muted_reblogs || [] + muted_reblogs = User.reblog_muted_users_ap_ids(user) from( activity in query, diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index fe6e26a90..afda9ffce 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Object alias Pleroma.ThreadMute alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility @@ -420,15 +421,11 @@ defp set_visibility(activity, %{"visibility" => visibility}) do defp set_visibility(activity, _), do: {:ok, activity} - def hide_reblogs(user, %{ap_id: ap_id} = _muted) do - if ap_id not in user.muted_reblogs do - User.add_reblog_mute(user, ap_id) - end + def hide_reblogs(%User{} = user, %User{} = target) do + UserRelationship.create_reblog_mute(user, target) end - def show_reblogs(user, %{ap_id: ap_id} = _muted) do - if ap_id in user.muted_reblogs do - User.remove_reblog_mute(user, ap_id) - end + def show_reblogs(%User{} = user, %User{} = target) do + UserRelationship.delete_reblog_mute(user, target) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 750f5c690..5b6158b93 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -323,9 +323,7 @@ def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) d def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do notifications? = params |> Map.get("notifications", true) |> truthy_param?() - with {:ok, _user_mute} <- User.mute(muter, muted, notifications?) do - # TODO: remove `muter` refresh once `muted_notifications` field is deprecated - muter = User.get_cached_by_id(muter.id) + with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do render(conn, "relationship.json", user: muter, target: muted) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -334,9 +332,7 @@ def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do @doc "POST /api/v1/accounts/:id/unmute" def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do - with {:ok, _user_mute} <- User.unmute(muter, muted) do - # TODO: remove `muter` refresh once `muted_notifications` field is deprecated - muter = User.get_cached_by_id(muter.id) + with {:ok, _user_relationships} <- User.unmute(muter, muted) do render(conn, "relationship.json", user: muter, target: muted) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -377,12 +373,14 @@ def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do @doc "GET /api/v1/mutes" def mutes(%{assigns: %{user: user}} = conn, _) do - render(conn, "index.json", users: User.muted_users(user), for: user, as: :user) + users = User.muted_users(user, _restrict_deactivated = true) + render(conn, "index.json", users: users, for: user, as: :user) end @doc "GET /api/v1/blocks" def blocks(%{assigns: %{user: user}} = conn, _) do - render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user) + users = User.blocked_users(user, _restrict_deactivated = true) + render(conn, "index.json", users: users, for: user, as: :user) end @doc "GET /api/v1/endorsements" diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index d875a5788..ee253a342 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -24,19 +24,16 @@ def follow(follower, followed, params \\ %{}) do with {:ok, follower, _followed, _} <- result do options = cast_params(params) - - case reblogs_visibility(options[:reblogs], result) do - {:ok, follower} -> {:ok, follower} - _ -> {:ok, follower} - end + set_reblogs_visibility(options[:reblogs], result) + {:ok, follower} end end - defp reblogs_visibility(false, {:ok, follower, followed, _}) do + defp set_reblogs_visibility(false, {:ok, follower, followed, _}) do CommonAPI.hide_reblogs(follower, followed) end - defp reblogs_visibility(_, {:ok, follower, followed, _}) do + defp set_reblogs_visibility(_, {:ok, follower, followed, _}) do CommonAPI.show_reblogs(follower, followed) end diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex index 020112949..07e677d29 100644 --- a/lib/pleroma/web/streamer/worker.ex +++ b/lib/pleroma/web/streamer/worker.ex @@ -129,17 +129,17 @@ defp do_stream(%{topic: topic, item: item}) do end defp should_send?(%User{} = user, %Activity{} = item) do - related_ap_ids = User.related_ap_ids(user, [:block, :mute]) - blocks = related_ap_ids[:block] || [] - mutes = related_ap_ids[:mute] || [] - reblog_mutes = user.muted_reblogs || [] - recipient_blocks = MapSet.new(blocks ++ mutes) + %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = + User.related_ap_ids(user, [:block, :mute, :reblog_mute]) + + recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids) recipients = MapSet.new(item.recipients) domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) with parent <- Object.normalize(item) || item, - true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), - true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), + true <- + Enum.all?([blocked_ap_ids, muted_ap_ids, reblog_muted_ap_ids], &(item.actor not in &1)), + true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)), true <- MapSet.disjoint?(recipients, recipient_blocks), %{host: item_host} <- URI.parse(item.actor), %{host: parent_host} <- URI.parse(parent.data["actor"]), diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs index a36f45a5a..9b2c97963 100644 --- a/test/conversation/participation_test.exs +++ b/test/conversation/participation_test.exs @@ -252,7 +252,7 @@ test "when the user blocks a recipient, the existing conversations with them are assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4 - {:ok, _user_block} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) # The conversations with the blocked user are marked as read assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] = @@ -274,7 +274,7 @@ test "the new conversation with the blocked user is not marked as unread " do blocked = insert(:user) third_user = insert(:user) - {:ok, _user_block} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) # When the blocked user is the author {:ok, _direct1} = @@ -311,7 +311,7 @@ test "the conversation with the blocked user is not marked as unread on a reply" "visibility" => "direct" }) - {:ok, _user_block} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) assert [%{read: true}] = Participation.for_user(blocker) assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 diff --git a/test/notification_test.exs b/test/notification_test.exs index d3d409c68..44f1bb414 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -93,7 +93,7 @@ test "it creates a notification for user if the user blocks the activity author" activity = insert(:note_activity) author = User.get_cached_by_ap_id(activity.data["actor"]) user = insert(:user) - {:ok, _user_block} = User.block(user, author) + {:ok, _user_relationship} = User.block(user, author) assert Notification.create_notification(activity, user) end @@ -112,7 +112,7 @@ test "notification created if user is muted without notifications" do muter = insert(:user) muted = insert(:user) - {:ok, _user_mute} = User.mute(muter, muted, false) + {:ok, _user_relationships} = User.mute(muter, muted, false) {:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"}) @@ -636,7 +636,7 @@ test "notifications are deleted if a remote user is deleted" do test "it returns notifications for muted user without notifications" do user = insert(:user) muted = insert(:user) - {:ok, _user_mute} = User.mute(user, muted, false) + {:ok, _user_relationships} = User.mute(user, muted, false) {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"}) @@ -646,10 +646,7 @@ test "it returns notifications for muted user without notifications" do test "it doesn't return notifications for muted user with notifications" do user = insert(:user) muted = insert(:user) - {:ok, _user_mute} = User.mute(user, muted) - - # Refreshing to reflect embedded ap id relation fields (remove once removed) - user = refresh_record(user) + {:ok, _user_relationships} = User.mute(user, muted) {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"}) @@ -659,7 +656,7 @@ test "it doesn't return notifications for muted user with notifications" do test "it doesn't return notifications for blocked user" do user = insert(:user) blocked = insert(:user) - {:ok, _user_block} = User.block(user, blocked) + {:ok, _user_relationship} = User.block(user, blocked) {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) @@ -689,7 +686,7 @@ test "it doesn't return notifications for muted thread" do test "it returns notifications from a muted user when with_muted is set" do user = insert(:user) muted = insert(:user) - {:ok, _user_mute} = User.mute(user, muted) + {:ok, _user_relationships} = User.mute(user, muted) {:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"}) @@ -699,7 +696,7 @@ test "it returns notifications from a muted user when with_muted is set" do test "it doesn't return notifications from a blocked user when with_muted is set" do user = insert(:user) blocked = insert(:user) - {:ok, _user_block} = User.block(user, blocked) + {:ok, _user_relationship} = User.block(user, blocked) {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) diff --git a/test/user_test.exs b/test/user_test.exs index 9e3c4b984..b0838e498 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -100,7 +100,7 @@ test "clears follow requests when requester is blocked" do CommonAPI.follow(follower, followed) assert [_activity] = User.get_follow_requests(followed) - {:ok, _user_block} = User.block(followed, follower) + {:ok, _user_relationship} = User.block(followed, follower) assert [] = User.get_follow_requests(followed) end @@ -113,8 +113,8 @@ test "follow_all follows mutliple users" do not_followed = insert(:user) reverse_blocked = insert(:user) - {:ok, _user_block} = User.block(user, blocked) - {:ok, _user_block} = User.block(reverse_blocked, user) + {:ok, _user_relationship} = User.block(user, blocked) + {:ok, _user_relationship} = User.block(reverse_blocked, user) {:ok, user} = User.follow(user, followed_zero) @@ -166,7 +166,7 @@ test "can't follow a user who blocked us" do blocker = insert(:user) blockee = insert(:user) - {:ok, _user_block} = User.block(blocker, blockee) + {:ok, _user_relationship} = User.block(blocker, blockee) {:error, _} = User.follow(blockee, blocker) end @@ -175,7 +175,7 @@ test "can't subscribe to a user who blocked us" do blocker = insert(:user) blocked = insert(:user) - {:ok, _user_block} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) {:error, _} = User.subscribe(blocked, blocker) end @@ -673,10 +673,7 @@ test "it mutes people" do refute User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) - {:ok, _user_mute} = User.mute(user, muted_user) - - # Refreshing to reflect embedded ap id relation fields (remove once removed) - user = refresh_record(user) + {:ok, _user_relationships} = User.mute(user, muted_user) assert User.mutes?(user, muted_user) assert User.muted_notifications?(user, muted_user) @@ -686,7 +683,7 @@ test "it unmutes users" do user = insert(:user) muted_user = insert(:user) - {:ok, _user_mute} = User.mute(user, muted_user) + {:ok, _user_relationships} = User.mute(user, muted_user) {:ok, _user_mute} = User.unmute(user, muted_user) refute User.mutes?(user, muted_user) @@ -700,7 +697,7 @@ test "it mutes user without notifications" do refute User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) - {:ok, _user_mute} = User.mute(user, muted_user, false) + {:ok, _user_relationships} = User.mute(user, muted_user, false) assert User.mutes?(user, muted_user) refute User.muted_notifications?(user, muted_user) @@ -714,7 +711,7 @@ test "it blocks people" do refute User.blocks?(user, blocked_user) - {:ok, _user_block} = User.block(user, blocked_user) + {:ok, _user_relationship} = User.block(user, blocked_user) assert User.blocks?(user, blocked_user) end @@ -723,7 +720,7 @@ test "it unblocks users" do user = insert(:user) blocked_user = insert(:user) - {:ok, _user_block} = User.block(user, blocked_user) + {:ok, _user_relationship} = User.block(user, blocked_user) {:ok, _user_block} = User.unblock(user, blocked_user) refute User.blocks?(user, blocked_user) @@ -739,7 +736,7 @@ test "blocks tear down cyclical follow relationships" do assert User.following?(blocker, blocked) assert User.following?(blocked, blocker) - {:ok, _user_block} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) blocked = User.get_cached_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -757,7 +754,7 @@ test "blocks tear down blocker->blocked follow relationships" do assert User.following?(blocker, blocked) refute User.following?(blocked, blocker) - {:ok, _user_block} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) blocked = User.get_cached_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -775,7 +772,7 @@ test "blocks tear down blocked->blocker follow relationships" do refute User.following?(blocker, blocked) assert User.following?(blocked, blocker) - {:ok, _user_block} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) blocked = User.get_cached_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -793,7 +790,7 @@ test "blocks tear down blocked->blocker subscription relationships" do assert User.subscribed_to?(blocked, blocker) refute User.subscribed_to?(blocker, blocked) - {:ok, _user_block} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) assert User.blocks?(blocker, blocked) refute User.subscribed_to?(blocker, blocked) @@ -1323,7 +1320,7 @@ test "follower count is updated when a follower is blocked" do {:ok, _follower2} = User.follow(follower2, user) {:ok, _follower3} = User.follow(follower3, user) - {:ok, _user_block} = User.block(user, follower) + {:ok, _user_relationship} = User.block(user, follower) user = refresh_record(user) assert User.user_info(user).follower_count == 2 diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 4e2321966..1946a603e 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -484,7 +484,7 @@ test "retrieves activities that have a given context" do activity_five = insert(:note_activity) user = insert(:user) - {:ok, _user_block} = User.block(user, %{ap_id: activity_five.data["actor"]}) + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]}) activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user}) assert activities == [activity_two, activity] @@ -497,7 +497,7 @@ test "doesn't return blocked activities" do activity_three = insert(:note_activity) user = insert(:user) booster = insert(:user) - {:ok, _user_block} = User.block(user, %{ap_id: activity_one.data["actor"]}) + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]}) activities = ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) @@ -515,7 +515,7 @@ test "doesn't return blocked activities" do assert Enum.member?(activities, activity_three) assert Enum.member?(activities, activity_one) - {:ok, _user_block} = User.block(user, %{ap_id: activity_three.data["actor"]}) + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]}) {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster) %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) activity_three = Activity.get_by_id(activity_three.id) @@ -542,7 +542,7 @@ test "doesn't return transitive interactions concerning blocked users" do blockee = insert(:user) friend = insert(:user) - {:ok, _user_block} = User.block(blocker, blockee) + {:ok, _user_relationship} = User.block(blocker, blockee) {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"}) @@ -565,7 +565,7 @@ test "doesn't return announce activities concerning blocked users" do blockee = insert(:user) friend = insert(:user) - {:ok, _user_block} = User.block(blocker, blockee) + {:ok, _user_relationship} = User.block(blocker, blockee) {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"}) @@ -613,7 +613,7 @@ test "doesn't return muted activities" do booster = insert(:user) activity_one_actor = User.get_by_ap_id(activity_one.data["actor"]) - {:ok, _user_mute} = User.mute(user, activity_one_actor) + {:ok, _user_relationships} = User.mute(user, activity_one_actor) activities = ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) @@ -644,7 +644,7 @@ test "doesn't return muted activities" do assert Enum.member?(activities, activity_one) activity_three_actor = User.get_by_ap_id(activity_three.data["actor"]) - {:ok, _user_mute} = User.mute(user, activity_three_actor) + {:ok, _user_relationships} = User.mute(user, activity_three_actor) {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster) %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) activity_three = Activity.get_by_id(activity_three.id) @@ -791,7 +791,7 @@ test "doesn't return reblogs for users for whom reblogs have been muted" do activity = insert(:note_activity) user = insert(:user) booster = insert(:user) - {:ok, user} = CommonAPI.hide_reblogs(user, booster) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) {:ok, activity, _} = CommonAPI.repeat(activity.id, booster) @@ -804,8 +804,8 @@ test "returns reblogs for users for whom reblogs have not been muted" do activity = insert(:note_activity) user = insert(:user) booster = insert(:user) - {:ok, user} = CommonAPI.hide_reblogs(user, booster) - {:ok, user} = CommonAPI.show_reblogs(user, booster) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) + {:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster) {:ok, activity, _} = CommonAPI.repeat(activity.id, booster) diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 5c67d41c5..7d6d0814d 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -128,7 +128,7 @@ test "it rejects incoming follow requests from blocked users when deny_follow_bl user = insert(:user) {:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin") - {:ok, _user_block} = User.block(user, target) + {:ok, _user_relationship} = User.block(user, target) data = File.read!("test/fixtures/mastodon-follow-activity.json") diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 138488d44..667ca89b9 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -509,14 +509,14 @@ test "updates state of multiple reports" do end test "add a reblog mute", %{muter: muter, muted: muted} do - {:ok, muter} = CommonAPI.hide_reblogs(muter, muted) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) assert User.showing_reblogs?(muter, muted) == false end test "remove a reblog mute", %{muter: muter, muted: muted} do - {:ok, muter} = CommonAPI.hide_reblogs(muter, muted) - {:ok, muter} = CommonAPI.show_reblogs(muter, muted) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) + {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted) assert User.showing_reblogs?(muter, muted) == true end diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index c5ef96bb7..9ff008a50 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -868,7 +868,7 @@ test "getting a list of mutes", %{conn: conn} do user = insert(:user) other_user = insert(:user) - {:ok, _user_mute} = User.mute(user, other_user) + {:ok, _user_relationships} = User.mute(user, other_user) conn = conn @@ -883,7 +883,7 @@ test "getting a list of blocks", %{conn: conn} do user = insert(:user) other_user = insert(:user) - {:ok, _user_block} = User.block(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user) conn = conn diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index 75b871a43..00a85169e 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -289,10 +289,7 @@ test "doesn't see notifications after muting user with notifications", %{conn: c assert length(json_response(conn, 200)) == 1 - {:ok, _user_mute} = User.mute(user, user2) - - # Refreshing to reflect embedded ap id relation fields (remove once removed) - user = refresh_record(user) + {:ok, _user_relationships} = User.mute(user, user2) conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications") @@ -313,7 +310,7 @@ test "see notifications after muting user without notifications", %{conn: conn} assert length(json_response(conn, 200)) == 1 - {:ok, _user_mute} = User.mute(user, user2, false) + {:ok, _user_relationships} = User.mute(user, user2, false) conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications") @@ -336,7 +333,7 @@ test "see notifications after muting user with notifications and with_muted para assert length(json_response(conn, 200)) == 1 - {:ok, _user_mute} = User.mute(user, user2) + {:ok, _user_relationships} = User.mute(user, user2) conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"}) diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index e8a1054ac..5fbe947ba 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -1108,7 +1108,7 @@ test "does not return users who have favorited the status but are blocked", %{ activity: activity } do other_user = insert(:user) - {:ok, _user_block} = User.block(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user) {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) @@ -1205,7 +1205,7 @@ test "does not return users who have reblogged the status but are blocked", %{ activity: activity } do other_user = insert(:user) - {:ok, _user_block} = User.block(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user) {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 524529d1f..dc17cc963 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -194,7 +194,7 @@ test "doesn't include DMs from blocked users", %{conn: conn} do blocker = insert(:user) blocked = insert(:user) user = insert(:user) - {:ok, _user_block} = User.block(blocker, blocked) + {:ok, _user_relationship} = User.block(blocker, blocked) {:ok, _blocked_direct} = CommonAPI.post(blocked, %{ diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index f8b74553b..4e5da6b06 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -191,11 +191,8 @@ test "represent a relationship for the following and followed user" do {:ok, user} = User.follow(user, other_user) {:ok, other_user} = User.follow(other_user, user) {:ok, other_user} = User.subscribe(user, other_user) - {:ok, _user_mute} = User.mute(user, other_user, true) - {:ok, user} = CommonAPI.hide_reblogs(user, other_user) - - # Refreshing to reflect embedded ap id relation fields (remove once removed) - user = refresh_record(user) + {:ok, _user_relationships} = User.mute(user, other_user, true) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) expected = %{ id: to_string(other_user.id), @@ -222,8 +219,8 @@ test "represent a relationship for the blocking and blocked user" do {:ok, user} = User.follow(user, other_user) {:ok, other_user} = User.subscribe(user, other_user) - {:ok, _user_block} = User.block(user, other_user) - {:ok, _user_block} = User.block(other_user, user) + {:ok, _user_relationship} = User.block(user, other_user) + {:ok, _user_relationship} = User.block(other_user, user) expected = %{ id: to_string(other_user.id), @@ -294,7 +291,7 @@ test "represent an embedded relationship" do other_user = insert(:user) {:ok, other_user} = User.follow(other_user, user) - {:ok, _user_block} = User.block(other_user, user) + {:ok, _user_relationship} = User.block(other_user, user) {:ok, _} = User.follow(insert(:user), user) expected = %{ diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index 89b146e1e..bdd87a79e 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -183,7 +183,7 @@ test "tells if the message is muted for some reason" do user = insert(:user) other_user = insert(:user) - {:ok, _user_mute} = User.mute(user, other_user) + {:ok, _user_relationships} = User.mute(user, other_user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) status = StatusView.render("show.json", %{activity: activity}) @@ -199,7 +199,7 @@ test "tells if the message is thread muted" do user = insert(:user) other_user = insert(:user) - {:ok, _user_mute} = User.mute(user, other_user) + {:ok, _user_relationships} = User.mute(user, other_user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) status = StatusView.render("show.json", %{activity: activity, for: user}) diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 6461fc4c3..802c22e5d 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -59,7 +59,7 @@ test "it doesn't send notify to the 'user:notification' stream when a user is bl user: user } do blocked = insert(:user) - {:ok, _user_block} = User.block(user, blocked) + {:ok, _user_relationship} = User.block(user, blocked) task = Task.async(fn -> refute_receive {:text, _}, 4_000 end) @@ -259,7 +259,7 @@ test "it sends message if recipients invalid and thread containment is enabled b test "it doesn't send messages involving blocked users" do user = insert(:user) blocked_user = insert(:user) - {:ok, _user_block} = User.block(user, blocked_user) + {:ok, _user_relationship} = User.block(user, blocked_user) task = Task.async(fn -> @@ -301,7 +301,7 @@ test "it doesn't send messages transitively involving blocked users" do "public" => [fake_socket] } - {:ok, _user_block} = User.block(blocker, blockee) + {:ok, _user_relationship} = User.block(blocker, blockee) {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"}) From 555edd01abf62b74c14c30caf82d77a80a93e72c Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 20 Nov 2019 12:19:07 +0300 Subject: [PATCH 14/70] [#1335] User AP ID relations fetching performance optimizations. --- lib/pleroma/notification.ex | 37 ++++++++++++++---- lib/pleroma/user.ex | 11 ++++-- lib/pleroma/web/activity_pub/activity_pub.ex | 40 ++++++++++++++++---- lib/pleroma/web/streamer/worker.ex | 2 +- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 15b3337f4..d4b6eb3b8 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -21,6 +21,8 @@ defmodule Pleroma.Notification do @type t :: %__MODULE__{} + @include_muted_option :with_muted + schema "notifications" do field(:seen, :boolean, default: false) belongs_to(:user, User, type: FlakeId.Ecto.CompatType) @@ -34,7 +36,25 @@ def changeset(%Notification{} = notification, attrs) do |> cast(attrs, [:seen]) end - def for_user_query(user, opts \\ []) do + defp for_user_query_ap_id_opts(user, opts) do + ap_id_relations = + [:block] ++ + if opts[@include_muted_option], do: [], else: [:notification_mute] + + preloaded_ap_ids = User.outgoing_relations_ap_ids(user, ap_id_relations) + + exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts) + + exclude_notification_muted_opts = + Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts) + + {exclude_blocked_opts, exclude_notification_muted_opts} + end + + def for_user_query(user, opts \\ %{}) do + {exclude_blocked_opts, exclude_notification_muted_opts} = + for_user_query_ap_id_opts(user, opts) + Notification |> where(user_id: ^user.id) |> where( @@ -54,13 +74,13 @@ def for_user_query(user, opts \\ []) do ) ) |> preload([n, a, o], activity: {a, object: o}) - |> exclude_muted(user, opts) - |> exclude_blocked(user) + |> exclude_notification_muted(user, exclude_notification_muted_opts) + |> exclude_blocked(user, exclude_blocked_opts) |> exclude_visibility(opts) end - defp exclude_blocked(query, user) do - blocked_ap_ids = User.blocked_users_ap_ids(user) + defp exclude_blocked(query, user, opts) do + blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user) query |> where([n, a], a.actor not in ^blocked_ap_ids) @@ -70,12 +90,13 @@ defp exclude_blocked(query, user) do ) end - defp exclude_muted(query, _, %{with_muted: true}) do + defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do query end - defp exclude_muted(query, user, _opts) do - notification_muted_ap_ids = User.notification_muted_users_ap_ids(user) + defp exclude_notification_muted(query, user, opts) do + notification_muted_ap_ids = + opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user) query |> where([n, a], a.actor not in ^notification_muted_ap_ids) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 615f1b725..4b53dce13 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1158,11 +1158,14 @@ def subscribed_to?(user, %{ap_id: ap_id}) do end @doc """ - Returns map of related AP IDs list by relation type. - E.g. `related_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}` + Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type. + E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}` """ - @spec related_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())} - def related_ap_ids(%User{} = user, relationship_types) when is_list(relationship_types) do + @spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())} + def outgoing_relations_ap_ids(_, []), do: %{} + + def outgoing_relations_ap_ids(%User{} = user, relationship_types) + when is_list(relationship_types) do db_result = user |> assoc(:outgoing_relationships) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 61b7f9280..b24139117 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -884,7 +884,7 @@ defp restrict_reblogs(query, _), do: query defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do - mutes = opts["muted_ap_ids"] || User.muted_users_ap_ids(user) + mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user) query = from([activity] in query, @@ -902,7 +902,7 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do defp restrict_muted(query, _), do: query defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do - blocked_ap_ids = opts["blocked_ap_ids"] || User.blocked_users_ap_ids(user) + blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user) domain_blocks = user.domain_blocks || [] query = @@ -944,8 +944,8 @@ defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) defp restrict_pinned(query, _), do: query - defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do - muted_reblogs = User.reblog_muted_users_ap_ids(user) + defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do + muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user) from( activity in query, @@ -1012,7 +1012,33 @@ defp maybe_order(query, %{order: :asc}) do defp maybe_order(query, _), do: query + defp fetch_activities_query_ap_ids_ops(opts) do + source_user = opts["muting_user"] + ap_id_relations = if source_user, do: [:mute, :reblog_mute], else: [] + + ap_id_relations = + ap_id_relations ++ + if opts["blocking_user"] && opts["blocking_user"] == source_user do + [:block] + else + [] + end + + preloaded_ap_ids = User.outgoing_relations_ap_ids(source_user, ap_id_relations) + + restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts) + restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts) + + restrict_muted_reblogs_opts = + Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts) + + {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} + end + def fetch_activities_query(recipients, opts \\ %{}) do + {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} = + fetch_activities_query_ap_ids_ops(opts) + config = %{ skip_thread_containment: Config.get([:instance, :skip_thread_containment]) } @@ -1032,15 +1058,15 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_type(opts) |> restrict_state(opts) |> restrict_favorited_by(opts) - |> restrict_blocked(opts) - |> restrict_muted(opts) + |> restrict_blocked(restrict_blocked_opts) + |> restrict_muted(restrict_muted_opts) |> restrict_media(opts) |> restrict_visibility(opts) |> restrict_thread_visibility(opts, config) |> restrict_replies(opts) |> restrict_reblogs(opts) |> restrict_pinned(opts) - |> restrict_muted_reblogs(opts) + |> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) |> exclude_visibility(opts) diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex index 07e677d29..a1b445f2f 100644 --- a/lib/pleroma/web/streamer/worker.ex +++ b/lib/pleroma/web/streamer/worker.ex @@ -130,7 +130,7 @@ defp do_stream(%{topic: topic, item: item}) do defp should_send?(%User{} = user, %Activity{} = item) do %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = - User.related_ap_ids(user, [:block, :mute, :reblog_mute]) + User.outgoing_relations_ap_ids(user, [:block, :mute, :reblog_mute]) recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids) recipients = MapSet.new(item.recipients) From de892d2fe1e70054aaf946b4cd11fb39111fe937 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 20 Nov 2019 15:46:11 +0300 Subject: [PATCH 15/70] [#1335] Reorganized users.subscribers as UserRelationship. Added tests for UserRelationship-related functionality. --- lib/pleroma/ecto_enums.ex | 8 +- lib/pleroma/user.ex | 103 +++++++------- lib/pleroma/web/common_api/common_api.ex | 2 +- lib/pleroma/web/common_api/utils.ex | 2 +- .../controllers/account_controller.ex | 4 +- ..._migration_populate_user_relationships.exs | 6 +- test/support/factory.ex | 12 ++ test/user_relationship_test.exs | 130 ++++++++++++++++++ test/user_test.exs | 52 ++++++- test/web/common_api/common_api_test.exs | 2 +- .../mastodon_api/views/account_view_test.exs | 4 +- 11 files changed, 263 insertions(+), 62 deletions(-) create mode 100644 test/user_relationship_test.exs diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex index bad5ec523..b86229312 100644 --- a/lib/pleroma/ecto_enums.ex +++ b/lib/pleroma/ecto_enums.ex @@ -4,4 +4,10 @@ import EctoEnum -defenum(UserRelationshipTypeEnum, block: 1, mute: 2, reblog_mute: 3, notification_mute: 4) +defenum(UserRelationshipTypeEnum, + block: 1, + mute: 2, + reblog_mute: 3, + notification_mute: 4, + inverse_subscription: 5 +) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 4b53dce13..d97c19d38 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -44,10 +44,17 @@ defmodule Pleroma.User do @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/ @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/ + # AP ID user relationships (blocks, mutes etc.) # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]] @user_relationships_config [ - block: [blocker_blocks: :blocked_users, blockee_blocks: :blocker_users], - mute: [muter_mutes: :muted_users, mutee_mutes: :muter_users], + block: [ + blocker_blocks: :blocked_users, + blockee_blocks: :blocker_users + ], + mute: [ + muter_mutes: :muted_users, + mutee_mutes: :muter_users + ], reblog_mute: [ reblog_muter_mutes: :reblog_muted_users, reblog_mutee_mutes: :reblog_muter_users @@ -55,6 +62,11 @@ defmodule Pleroma.User do notification_mute: [ notification_muter_mutes: :notification_muted_users, notification_mutee_mutes: :notification_muter_users + ], + # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target + inverse_subscription: [ + subscribee_subscriptions: :subscriber_users, + subscriber_subscriptions: :subscribee_users ] ] @@ -90,7 +102,6 @@ defmodule Pleroma.User do field(:confirmation_token, :string, default: nil) field(:default_scope, :string, default: "public") field(:domain_blocks, {:array, :string}, default: []) - field(:subscribers, {:array, :string}, default: []) field(:deactivated, :boolean, default: false) field(:no_rich_text, :boolean, default: false) field(:ap_enabled, :boolean, default: false) @@ -167,6 +178,8 @@ defmodule Pleroma.User do field(:muted_reblogs, {:array, :string}, default: []) # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation) field(:muted_notifications, {:array, :string}, default: []) + # `:subscribers` is deprecated (replaced with `subscriber_users` relation) + field(:subscribers, {:array, :string}, default: []) timestamps() end @@ -1046,33 +1059,43 @@ def get_recipients_from_activity(%Activity{recipients: to}) do @spec mute(User.t(), User.t(), boolean()) :: {:ok, list(UserRelationship.t())} | {:error, String.t()} - def mute(muter, %User{} = mutee, notifications? \\ true) do + def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do add_to_mutes(muter, mutee, notifications?) end - def unmute(muter, %User{} = mutee) do + def unmute(%User{} = muter, %User{} = mutee) do remove_from_mutes(muter, mutee) end - def subscribe(subscriber, %{ap_id: ap_id}) do - with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do - deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) + def subscribe(%User{} = subscriber, %User{} = target) do + deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) - if blocks?(subscribed, subscriber) and deny_follow_blocked do - {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"} - else - User.add_to_subscribers(subscribed, subscriber.ap_id) - end + if blocks?(target, subscriber) and deny_follow_blocked do + {:error, "Could not subscribe: #{target.nickname} is blocking you"} + else + # Note: the relationship is inverse: subscriber acts as relationship target + UserRelationship.create_inverse_subscription(target, subscriber) end end - def unsubscribe(unsubscriber, %{ap_id: ap_id}) do + def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do + with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do + subscribe(subscriber, subscribee) + end + end + + def unsubscribe(%User{} = unsubscriber, %User{} = target) do + # Note: the relationship is inverse: subscriber acts as relationship target + UserRelationship.delete_inverse_subscription(target, unsubscriber) + end + + def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do with %User{} = user <- get_cached_by_ap_id(ap_id) do - User.remove_from_subscribers(user, unsubscriber.ap_id) + unsubscribe(unsubscriber, user) end end - def block(blocker, %User{} = blocked) do + def block(%User{} = blocker, %User{} = blocked) do # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213) blocker = if following?(blocker, blocked) do @@ -1089,13 +1112,7 @@ def block(blocker, %User{} = blocked) do nil -> blocked end - blocker = - if subscribed_to?(blocked, blocker) do - {:ok, blocker} = unsubscribe(blocked, blocker) - blocker - else - blocker - end + unsubscribe(blocked, blocker) if following?(blocked, blocker), do: unfollow(blocked, blocker) @@ -1105,16 +1122,16 @@ def block(blocker, %User{} = blocked) do end # helper to handle the block given only an actor's AP id - def block(blocker, %{ap_id: ap_id}) do + def block(%User{} = blocker, %{ap_id: ap_id}) do block(blocker, get_cached_by_ap_id(ap_id)) end - def unblock(blocker, %User{} = blocked) do + def unblock(%User{} = blocker, %User{} = blocked) do remove_from_block(blocker, blocked) end # helper to handle the block given only an actor's AP id - def unblock(blocker, %{ap_id: ap_id}) do + def unblock(%User{} = blocker, %{ap_id: ap_id}) do unblock(blocker, get_cached_by_ap_id(ap_id)) end @@ -1128,7 +1145,7 @@ def mutes_user?(%User{} = user, %User{} = target) do @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean() def muted_notifications?(nil, _), do: false - def muted_notifications?(user, %User{} = target), + def muted_notifications?(%User{} = user, %User{} = target), do: UserRelationship.notification_mute_exists?(user, target) def blocks?(nil, _), do: false @@ -1151,9 +1168,14 @@ def blocks_domain?(%User{} = user, %User{} = target) do def blocks_domain?(_, _), do: false - def subscribed_to?(user, %{ap_id: ap_id}) do + def subscribed_to?(%User{} = user, %User{} = target) do + # Note: the relationship is inverse: subscriber acts as relationship target + UserRelationship.inverse_subscription_exists?(target, user) + end + + def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do with %User{} = target <- get_cached_by_ap_id(ap_id) do - Enum.member?(target.subscribers, user.ap_id) + subscribed_to?(user, target) end end @@ -1183,12 +1205,6 @@ def outgoing_relations_ap_ids(%User{} = user, relationship_types) ) end - @spec subscribers(User.t()) :: [User.t()] - def subscribers(user) do - User.Query.build(%{ap_id: user.subscribers, deactivated: false}) - |> Repo.all() - end - def deactivate_async(user, status \\ true) do BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status}) end @@ -1919,23 +1935,6 @@ def update_email_notifications(user, settings) do |> update_and_set_cache() end - defp set_subscribers(user, subscribers) do - params = %{subscribers: subscribers} - - user - |> cast(params, [:subscribers]) - |> validate_required([:subscribers]) - |> update_and_set_cache() - end - - def add_to_subscribers(user, subscribed) do - set_subscribers(user, Enum.uniq([subscribed | user.subscribers])) - end - - def remove_from_subscribers(user, subscribed) do - set_subscribers(user, List.delete(user.subscribers, subscribed)) - end - defp set_domain_blocks(user, domain_blocks) do params = %{domain_blocks: domain_blocks} diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index afda9ffce..2f3bcfc3c 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -33,7 +33,7 @@ def follow(follower, followed) do def unfollow(follower, unfollowed) do with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed), - {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do + {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do {:ok, follower} end end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 88a5f434a..f77076906 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -492,7 +492,7 @@ def maybe_notify_subscribers( with %User{} = user <- User.get_cached_by_ap_id(actor) do subscriber_ids = user - |> User.subscribers() + |> User.subscriber_users() |> Enum.filter(&Visibility.visible_for_user?(activity, &1)) |> Enum.map(& &1.ap_id) diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index bc2f1017c..773cd9a97 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -144,7 +144,7 @@ def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do @doc "POST /api/v1/pleroma/accounts/:id/subscribe" def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do - with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do + with {:ok, _subscription} <- User.subscribe(user, subscription_target) do render(conn, "relationship.json", user: user, target: subscription_target) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -153,7 +153,7 @@ def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _p @doc "POST /api/v1/pleroma/accounts/:id/unsubscribe" def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do - with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do + with {:ok, _subscription} <- User.unsubscribe(user, subscription_target) do render(conn, "relationship.json", user: user, target: subscription_target) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) diff --git a/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs b/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs index f8dde7626..990e9f3b8 100644 --- a/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs +++ b/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs @@ -8,9 +8,13 @@ defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserRelationships do def up do Enum.each( - [blocks: 1, mutes: 2, muted_reblogs: 3, muted_notifications: 4], + [blocks: 1, mutes: 2, muted_reblogs: 3, muted_notifications: 4, subscribers: 5], fn {field, relationship_type_code} -> migrate(field, relationship_type_code) + + if field == :subscribers do + drop_if_exists(index(:users, [:subscribers])) + end end ) end diff --git a/test/support/factory.ex b/test/support/factory.ex index e3f797f64..c2ceb5836 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -43,6 +43,18 @@ def user_factory do } end + def user_relationship_factory(attrs \\ %{}) do + source = attrs[:source] || insert(:user) + target = attrs[:target] || insert(:user) + relationship_type = attrs[:relationship_type] || :block + + %Pleroma.UserRelationship{ + source_id: source.id, + target_id: target.id, + relationship_type: relationship_type + } + end + def note_factory(attrs \\ %{}) do text = sequence(:text, &"This is :moominmamma: note #{&1}") diff --git a/test/user_relationship_test.exs b/test/user_relationship_test.exs new file mode 100644 index 000000000..437450bc3 --- /dev/null +++ b/test/user_relationship_test.exs @@ -0,0 +1,130 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserRelationshipTest do + alias Pleroma.UserRelationship + + use Pleroma.DataCase + + import Pleroma.Factory + + describe "*_exists?/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "returns false if record doesn't exist", %{users: [user1, user2]} do + refute UserRelationship.block_exists?(user1, user2) + refute UserRelationship.mute_exists?(user1, user2) + refute UserRelationship.notification_mute_exists?(user1, user2) + refute UserRelationship.reblog_mute_exists?(user1, user2) + refute UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "returns true if record exists", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + assert UserRelationship.block_exists?(user1, user2) + assert UserRelationship.mute_exists?(user1, user2) + assert UserRelationship.notification_mute_exists?(user1, user2) + assert UserRelationship.reblog_mute_exists?(user1, user2) + assert UserRelationship.inverse_subscription_exists?(user1, user2) + end + end + + describe "create_*/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + UserRelationship.create_block(user1, user2) + UserRelationship.create_mute(user1, user2) + UserRelationship.create_notification_mute(user1, user2) + UserRelationship.create_reblog_mute(user1, user2) + UserRelationship.create_inverse_subscription(user1, user2) + + assert UserRelationship.block_exists?(user1, user2) + assert UserRelationship.mute_exists?(user1, user2) + assert UserRelationship.notification_mute_exists?(user1, user2) + assert UserRelationship.reblog_mute_exists?(user1, user2) + assert UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "if record already exists, returns it", %{users: [user1, user2]} do + user_block = UserRelationship.create_block(user1, user2) + assert user_block == UserRelationship.create_block(user1, user2) + end + end + + describe "delete_*/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "deletes user relationship record if it exists", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + assert {:ok, %UserRelationship{}} = UserRelationship.delete_block(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_mute(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_notification_mute(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_reblog_mute(user1, user2) + + assert {:ok, %UserRelationship{}} = + UserRelationship.delete_inverse_subscription(user1, user2) + + refute UserRelationship.block_exists?(user1, user2) + refute UserRelationship.mute_exists?(user1, user2) + refute UserRelationship.notification_mute_exists?(user1, user2) + refute UserRelationship.reblog_mute_exists?(user1, user2) + refute UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "if record does not exist, returns {:ok, nil}", %{users: [user1, user2]} do + assert {:ok, nil} = UserRelationship.delete_block(user1, user2) + assert {:ok, nil} = UserRelationship.delete_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_notification_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_reblog_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_inverse_subscription(user1, user2) + end + end +end diff --git a/test/user_test.exs b/test/user_test.exs index b0838e498..ae90edbe9 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -25,6 +25,56 @@ defmodule Pleroma.UserTest do clear_config([:instance, :account_activation_required]) + describe "AP ID user relationships" do + setup do + {:ok, user: insert(:user)} + end + + test "outgoing_relations_ap_ids/1", %{user: user} do + rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription] + + ap_ids_by_rel = + Enum.into( + rel_types, + %{}, + fn rel_type -> + rel_records = + insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type}) + + ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end) + {rel_type, Enum.sort(ap_ids)} + end + ) + + assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user)) + assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user)) + assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:notification_mute] == + Enum.sort(User.notification_muted_users_ap_ids(user)) + + assert ap_ids_by_rel[:notification_mute] == + Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user)) + + assert ap_ids_by_rel[:reblog_mute] == + Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user)) + + assert ap_ids_by_rel[:inverse_subscription] == + Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id)) + + outgoing_relations_ap_ids = User.outgoing_relations_ap_ids(user, rel_types) + + assert ap_ids_by_rel == + Enum.into(outgoing_relations_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end) + end + end + describe "when tags are nil" do test "tagging a user" do user = insert(:user, %{tags: nil}) @@ -785,7 +835,7 @@ test "blocks tear down blocked->blocker subscription relationships" do blocker = insert(:user) blocked = insert(:user) - {:ok, blocker} = User.subscribe(blocked, blocker) + {:ok, _subscription} = User.subscribe(blocked, blocker) assert User.subscribed_to?(blocked, blocker) refute User.subscribed_to?(blocker, blocked) diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 667ca89b9..b5d6d4055 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -526,7 +526,7 @@ test "remove a reblog mute", %{muter: muter, muted: muted} do test "also unsubscribes a user" do [follower, followed] = insert_pair(:user) {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) - {:ok, followed} = User.subscribe(follower, followed) + {:ok, _subscription} = User.subscribe(follower, followed) assert User.subscribed_to?(follower, followed) diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 4e5da6b06..53cd26a69 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -190,7 +190,7 @@ test "represent a relationship for the following and followed user" do {:ok, user} = User.follow(user, other_user) {:ok, other_user} = User.follow(other_user, user) - {:ok, other_user} = User.subscribe(user, other_user) + {:ok, _subscription} = User.subscribe(user, other_user) {:ok, _user_relationships} = User.mute(user, other_user, true) {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) @@ -218,7 +218,7 @@ test "represent a relationship for the blocking and blocked user" do other_user = insert(:user) {:ok, user} = User.follow(user, other_user) - {:ok, other_user} = User.subscribe(user, other_user) + {:ok, _subscription} = User.subscribe(user, other_user) {:ok, _user_relationship} = User.block(user, other_user) {:ok, _user_relationship} = User.block(other_user, user) From 81f1c6bc4725f6d97d42ff89f24328ae5b505a2d Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 20 Nov 2019 15:57:51 +0300 Subject: [PATCH 16/70] [#1335] Added CHANGELOG.md entry. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd835a3d..3a259ef63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - OStatus: Extract RSS functionality - Deprecated `User.Info` embedded schema (fields moved to `User`) - Store status data inside Flag activity +- Deprecated (reorganized as `UserRelationship` entity) User fields with user AP IDs (`blocks`, `mutes`, `muted_reblogs`, `muted_notifications`, `subscribers`).
API Changes From 1d6970baf1fd09c357740a380351adf44f247279 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Fri, 22 Nov 2019 14:17:12 +0300 Subject: [PATCH 17/70] fix --- benchmarks/load_testing/fetcher.ex | 36 ++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex index 776105d34..a45a71d4a 100644 --- a/benchmarks/load_testing/fetcher.ex +++ b/benchmarks/load_testing/fetcher.ex @@ -57,9 +57,6 @@ def query_timelines(user) do Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities( mastodon_federated_timeline_params ) - end, - "User favourites timeline" => fn -> - Pleroma.Web.ActivityPub.ActivityPub.fetch_favourites(user) end }) @@ -77,8 +74,6 @@ def query_timelines(user) do mastodon_federated_timeline_params ) - user_favourites = Pleroma.Web.ActivityPub.ActivityPub.fetch_favourites(user) - Benchee.run(%{ "Rendering home timeline" => fn -> Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ @@ -102,10 +97,33 @@ def query_timelines(user) do }) end, "Rendering favorites timeline" => fn -> - Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ - activities: user_favourites, - for: user, - as: :activity}) + conn = Phoenix.ConnTest.build_conn(:get, "http://localhost:4001/api/v1/favourites", nil) + Pleroma.Web.MastodonAPI.StatusController.favourites( + %Plug.Conn{conn | + assigns: %{user: user}, + query_params: %{"limit" => "0"}, + body_params: %{}, + cookies: %{}, + params: %{}, + path_params: %{}, + private: %{ + Pleroma.Web.Router => {[], %{}}, + phoenix_router: Pleroma.Web.Router, + phoenix_action: :favourites, + phoenix_controller: Pleroma.Web.MastodonAPI.StatusController, + phoenix_endpoint: Pleroma.Web.Endpoint, + phoenix_format: "json", + phoenix_layout: {Pleroma.Web.LayoutView, "app.html"}, + phoenix_recycled: true, + + phoenix_view: Pleroma.Web.MastodonAPI.StatusView, + plug_session: %{"user_id" => user.id}, + plug_session_fetch: :done, + plug_session_info: :write, + plug_skip_csrf_protection: true + } + }, + %{}) end, }) end From 1636cc5b7e2ad324c828c993d5fb39ac9cdb40cc Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 23 Nov 2019 14:06:19 +0300 Subject: [PATCH 18/70] Removed users.info and remaining usages. --- benchmarks/load_testing/generator.ex | 1 - lib/pleroma/user.ex | 5 +--- .../20191123103423_remove_info_from_users.exs | 9 +++++++ test/support/factory.ex | 1 - test/user_test.exs | 25 +++---------------- 5 files changed, 14 insertions(+), 27 deletions(-) create mode 100644 priv/repo/migrations/20191123103423_remove_info_from_users.exs diff --git a/benchmarks/load_testing/generator.ex b/benchmarks/load_testing/generator.ex index b4432bdb7..4505d192f 100644 --- a/benchmarks/load_testing/generator.ex +++ b/benchmarks/load_testing/generator.ex @@ -31,7 +31,6 @@ defp generate_user_data(i) do password_hash: "$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg", bio: "Tester Number #{i}", - info: %{}, local: remote } diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index fcb1d5143..e4656af3d 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -119,8 +119,6 @@ defmodule Pleroma.User do has_many(:registrations, Registration) has_many(:deliveries, Delivery) - field(:info, :map, default: %{}) - timestamps() end @@ -244,7 +242,6 @@ def remote_user_creation(params) do params = params - |> Map.put(:info, params[:info] || %{}) |> truncate_if_exists(:name, name_limit) |> truncate_if_exists(:bio, bio_limit) |> truncate_fields_param() @@ -1226,7 +1223,7 @@ def external_users_query do def external_users(opts \\ []) do query = external_users_query() - |> select([u], struct(u, [:id, :ap_id, :info])) + |> select([u], struct(u, [:id, :ap_id])) query = if opts[:max_id], diff --git a/priv/repo/migrations/20191123103423_remove_info_from_users.exs b/priv/repo/migrations/20191123103423_remove_info_from_users.exs new file mode 100644 index 000000000..b251255ea --- /dev/null +++ b/priv/repo/migrations/20191123103423_remove_info_from_users.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.RemoveInfoFromUsers do + use Ecto.Migration + + def change do + alter table(:users) do + remove(:info, :map, default: %{}) + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index e3f797f64..bb8a64e72 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -31,7 +31,6 @@ def user_factory do nickname: sequence(:nickname, &"nick#{&1}"), password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), bio: sequence(:bio, &"Tester Number #{&1}"), - info: %{}, last_digest_emailed_at: NaiveDateTime.utc_now() } diff --git a/test/user_test.exs b/test/user_test.exs index 8fdb6b25f..e07ddd097 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -347,18 +347,6 @@ test "it sets the password_hash and ap_id" do assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" end - - test "it ensures info is not nil" do - changeset = User.register_changeset(%User{}, @full_user_data) - - assert changeset.valid? - - {:ok, user} = - changeset - |> Repo.insert() - - refute is_nil(user.info) - end end describe "user registration, with :account_activation_required" do @@ -412,8 +400,7 @@ test "gets an existing user by ap_id" do :user, local: false, nickname: "admin@mastodon.example.org", - ap_id: ap_id, - info: %{} + ap_id: ap_id ) {:ok, fetched_user} = User.get_or_fetch(ap_id) @@ -474,8 +461,7 @@ test "updates an existing user, if stale" do local: false, nickname: "admin@mastodon.example.org", ap_id: "http://mastodon.example.org/users/admin", - last_refreshed_at: a_week_ago, - info: %{} + last_refreshed_at: a_week_ago ) assert orig_user.last_refreshed_at == a_week_ago @@ -516,7 +502,6 @@ test "returns an ap_followers link for a user" do name: "Someone", nickname: "a@b.de", ap_id: "http...", - info: %{some: "info"}, avatar: %{some: "avatar"} } @@ -1121,8 +1106,7 @@ test "with an overly long bio" do ap_id: user.ap_id, name: user.name, nickname: user.nickname, - bio: String.duplicate("h", current_max_length + 1), - info: %{} + bio: String.duplicate("h", current_max_length + 1) } assert {:ok, %User{}} = User.insert_or_update_user(data) @@ -1135,8 +1119,7 @@ test "with an overly long display name" do data = %{ ap_id: user.ap_id, name: String.duplicate("h", current_max_length + 1), - nickname: user.nickname, - info: %{} + nickname: user.nickname } assert {:ok, %User{}} = User.insert_or_update_user(data) From 2b341627da5d592bdedc66a331409f5228ab28cf Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Mon, 25 Nov 2019 00:04:29 +0900 Subject: [PATCH 19/70] Admin API: Render whole status in grouped reports --- CHANGELOG.md | 1 + lib/pleroma/web/activity_pub/utils.ex | 23 +++++++++++++++---- .../web/admin_api/views/report_view.ex | 8 ++++++- .../admin_api/admin_api_controller_test.exs | 16 ++++--------- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd835a3d..443e5f3c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: `pleroma.thread_muted` to the Status entity - Mastodon API: Mark the direct conversation as read for the author when they send a new direct message - Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload. +- Admin API: Render whole status in grouped reports
### Added diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index c45662359..277ca3c7c 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -822,20 +822,33 @@ def parse_report_group(activity) do reports = get_reports_by_status_id(activity["id"]) max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"])) actors = Enum.map(reports, & &1.user_actor) + {deleted, status} = get_status_data(activity) %{ date: max_date.data["published"], account: activity["actor"], - status: %{ - id: activity["id"], - content: activity["content"], - published: activity["published"] - }, + status: status, + status_deleted: deleted, actors: Enum.uniq(actors), reports: reports } end + defp get_status_data(activity) do + case Activity.get_by_ap_id(activity["id"]) do + %Activity{} = act -> + {false, act} + + _ -> + {true, + %{ + id: activity["id"], + content: activity["content"], + published: activity["published"] + }} + end + end + def get_reports_by_status_id(ap_id) do from(a in Activity, where: fragment("(?)->>'type' = 'Flag'", a.data), diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index ca88595c7..0ba94def9 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -45,10 +45,16 @@ def render("show.json", %{report: report, user: user, account: account, statuses def render("index_grouped.json", %{groups: groups}) do reports = Enum.map(groups, fn group -> + status = + if group[:status_deleted], + do: group[:status], + else: StatusView.render("show.json", %{activity: group[:status]}) + %{ date: group[:date], account: group[:account], - status: group[:status], + status: status, + status_deleted: status_deleted, actors: Enum.map(group[:actors], &merge_account_views/1), reports: group[:reports] diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 3a4c4d65c..ea1b4c48c 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -15,6 +15,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MediaProxy import Pleroma.Factory @@ -1616,14 +1617,11 @@ test "returns reports grouped by status", %{ assert length(response["reports"]) == 3 - first_group = - Enum.find(response["reports"], &(&1["status"]["id"] == first_status.data["id"])) + first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id)) - second_group = - Enum.find(response["reports"], &(&1["status"]["id"] == second_status.data["id"])) + second_group = Enum.find(response["reports"], &(&1["status"]["id"] == second_status.id)) - third_group = - Enum.find(response["reports"], &(&1["status"]["id"] == third_status.data["id"])) + third_group = Enum.find(response["reports"], &(&1["status"]["id"] == third_status.id)) assert length(first_group["reports"]) == 3 assert length(second_group["reports"]) == 2 @@ -1634,11 +1632,7 @@ test "returns reports grouped by status", %{ NaiveDateTime.from_iso8601!(act.data["published"]) end).data["published"] - assert first_group["status"] == %{ - "id" => first_status.data["id"], - "content" => first_status.object.data["content"], - "published" => first_status.object.data["published"] - } + assert first_group["status"] == StatusView.render("show.json", %{activity: first_status}) assert first_group["account"]["id"] == target_user.id From 40059c9b1e404412da103aaf74333bec364d3099 Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Mon, 25 Nov 2019 00:05:52 +0900 Subject: [PATCH 20/70] Typo --- lib/pleroma/web/admin_api/views/report_view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 0ba94def9..45ce75272 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -54,7 +54,7 @@ def render("index_grouped.json", %{groups: groups}) do date: group[:date], account: group[:account], status: status, - status_deleted: status_deleted, + status_deleted: group[:status_deleted], actors: Enum.map(group[:actors], &merge_account_views/1), reports: group[:reports] From 1364d303f8e4dcd4d9f7913d4755c58b0f4b87ef Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Mon, 25 Nov 2019 01:39:35 +0900 Subject: [PATCH 21/70] AdminAPI: Fix grouped reports for closed/resolved reports --- lib/pleroma/web/activity_pub/utils.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 277ca3c7c..9e460b604 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -852,7 +852,8 @@ defp get_status_data(activity) do def get_reports_by_status_id(ap_id) do from(a in Activity, where: fragment("(?)->>'type' = 'Flag'", a.data), - where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]) + where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]), + or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id]) ) |> Activity.with_preloaded_user_actor() |> Repo.all() From b3b4e5ca805780f278b6239973d6d497b1697fbd Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Fri, 22 Nov 2019 13:35:21 +0900 Subject: [PATCH 22/70] AdminAPI: Grouped reports old/new fix If some status received reports both in the "new" format and "old" format it was considered reports on two different statuses (in the context of grouped reports) --- CHANGELOG.md | 1 + lib/pleroma/web/activity_pub/utils.ex | 78 ++++++++++++++------------- test/web/activity_pub/utils_test.exs | 43 --------------- 3 files changed, 41 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 443e5f3c3..60512c6b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`) - Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname` +- AdminAPI: If some status received reports both in the "new" format and "old" format it was considered reports on two different statuses (in the context of grouped reports) ## [1.1.6] - 2019-11-19 diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 9e460b604..718e3328d 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -788,36 +788,6 @@ def get_reports(params, page, page_size) do ActivityPub.fetch_activities([], params, :offset) end - @spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{ - required(:groups) => [ - %{ - required(:date) => String.t(), - required(:account) => %{}, - required(:status) => %{}, - required(:actors) => [%User{}], - required(:reports) => [%Activity{}] - } - ], - required(:total) => integer - } - def get_reports_grouped_by_status(groups) do - parsed_groups = - groups - |> Enum.map(fn entry -> - activity = - case Jason.decode(entry.activity) do - {:ok, activity} -> activity - _ -> build_flag_object(entry.activity) - end - - parse_report_group(activity) - end) - - %{ - groups: parsed_groups - } - end - def parse_report_group(activity) do reports = get_reports_by_status_id(activity["id"]) max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"])) @@ -859,6 +829,32 @@ def get_reports_by_status_id(ap_id) do |> Repo.all() end + @spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{ + required(:groups) => [ + %{ + required(:date) => String.t(), + required(:account) => %{}, + required(:status) => %{}, + required(:actors) => [%User{}], + required(:reports) => [%Activity{}] + } + ], + required(:total) => integer + } + def get_reports_grouped_by_status(activity_ids) do + parsed_groups = + activity_ids + |> Enum.map(fn id -> + id + |> build_flag_object() + |> parse_report_group() + end) + + %{ + groups: parsed_groups + } + end + @spec get_reported_activities() :: [ %{ required(:activity) => String.t(), @@ -866,17 +862,23 @@ def get_reports_by_status_id(ap_id) do } ] def get_reported_activities do - from(a in Activity, - where: fragment("(?)->>'type' = 'Flag'", a.data), + reported_activities_query = + from(a in Activity, + where: fragment("(?)->>'type' = 'Flag'", a.data), + select: %{ + activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data) + }, + group_by: fragment("activity") + ) + + from(a in subquery(reported_activities_query), + distinct: true, select: %{ - date: fragment("max(?->>'published') date", a.data), - activity: - fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data) - }, - group_by: fragment("activity"), - order_by: fragment("date DESC") + id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity) + } ) |> Repo.all() + |> Enum.map(& &1.id) end def update_report_state(%Activity{} = activity, state) diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs index 1feb076ba..586eb1d2f 100644 --- a/test/web/activity_pub/utils_test.exs +++ b/test/web/activity_pub/utils_test.exs @@ -636,47 +636,4 @@ test "removes actor from announcements" do assert updated_object.data["announcement_count"] == 1 end end - - describe "get_reports_grouped_by_status/1" do - setup do - [reporter, target_user] = insert_pair(:user) - first_status = insert(:note_activity, user: target_user) - second_status = insert(:note_activity, user: target_user) - - CommonAPI.report(reporter, %{ - "account_id" => target_user.id, - "comment" => "I feel offended", - "status_ids" => [first_status.id] - }) - - CommonAPI.report(reporter, %{ - "account_id" => target_user.id, - "comment" => "I feel offended2", - "status_ids" => [second_status.id] - }) - - data = [%{activity: first_status.data["id"]}, %{activity: second_status.data["id"]}] - - {:ok, - %{ - first_status: first_status, - second_status: second_status, - data: data - }} - end - - test "works for deprecated reports format", %{ - first_status: first_status, - second_status: second_status, - data: data - } do - groups = Utils.get_reports_grouped_by_status(data).groups - - first_group = Enum.find(groups, &(&1.status.id == first_status.data["id"])) - second_group = Enum.find(groups, &(&1.status.id == second_status.data["id"])) - - assert first_group.status.id == first_status.data["id"] - assert second_group.status.id == second_status.data["id"] - end - end end From 5869a43fe722c878fca1d87816cd1437f2bb49fe Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Tue, 26 Nov 2019 01:06:54 +0900 Subject: [PATCH 23/70] Fix tests --- test/support/helpers.ex | 17 +++++++++++++++++ .../admin_api/admin_api_controller_test.exs | 19 +++++++------------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/test/support/helpers.ex b/test/support/helpers.ex index ce39dd9d8..af2b2eddf 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -75,6 +75,23 @@ def render_json(view, template, assigns) do |> Poison.decode!() end + def stringify_keys(nil), do: nil + + def stringify_keys(key) when key in [true, false], do: key + def stringify_keys(key) when is_atom(key), do: Atom.to_string(key) + + def stringify_keys(map) when is_map(map) do + map + |> Enum.map(fn {k, v} -> {stringify_keys(k), stringify_keys(v)} end) + |> Enum.into(%{}) + end + + def stringify_keys([head | rest] = list) when is_list(list) do + [stringify_keys(head) | stringify_keys(rest)] + end + + def stringify_keys(key), do: key + defmacro guards_config(config_path) do quote do initial_setting = Pleroma.Config.get(config_path) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 12dba7773..c6ff1a065 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1654,9 +1654,10 @@ test "returns reports grouped by status", %{ NaiveDateTime.from_iso8601!(act.data["published"]) end).data["published"] - assert first_group["status"] == StatusView.render("show.json", %{activity: first_status}) + assert first_group["status"] == + stringify_keys(StatusView.render("show.json", %{activity: first_status})) - assert first_group["account"]["id"] == target_user.id + assert(first_group["account"]["id"] == target_user.id) assert length(first_group["actors"]) == 1 assert hd(first_group["actors"])["id"] == reporter.id @@ -1669,11 +1670,8 @@ test "returns reports grouped by status", %{ NaiveDateTime.from_iso8601!(act.data["published"]) end).data["published"] - assert second_group["status"] == %{ - "id" => second_status.data["id"], - "content" => second_status.object.data["content"], - "published" => second_status.object.data["published"] - } + assert second_group["status"] == + stringify_keys(StatusView.render("show.json", %{activity: second_status})) assert second_group["account"]["id"] == target_user.id @@ -1688,11 +1686,8 @@ test "returns reports grouped by status", %{ NaiveDateTime.from_iso8601!(act.data["published"]) end).data["published"] - assert third_group["status"] == %{ - "id" => third_status.data["id"], - "content" => third_status.object.data["content"], - "published" => third_status.object.data["published"] - } + assert third_group["status"] == + stringify_keys(StatusView.render("show.json", %{activity: third_status})) assert third_group["account"]["id"] == target_user.id From f01881e82a48b4177f943c73338d013010719040 Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Thu, 11 Jul 2019 16:33:20 -0400 Subject: [PATCH 24/70] Add federated test infrastructure --- test/federation/federation_test.exs | 43 ++++++ test/support/cluster.ex | 218 ++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 test/federation/federation_test.exs create mode 100644 test/support/cluster.ex diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs new file mode 100644 index 000000000..7400d2abc --- /dev/null +++ b/test/federation/federation_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Integration.FederationTest do + use Pleroma.DataCase + + import Pleroma.Cluster + + @federated1 :"federated1@127.0.0.1" + + describe "federated cluster primitives" do + test "within/2 captures local bindings and executes block on remote node" do + captured_binding = :captured + + result = + within @federated1 do + user = Pleroma.Factory.insert(:user) + {captured_binding, node(), user} + end + + assert {:captured, @federated1, user} = result + refute Pleroma.User.get_by_id(user.id) + assert user.id == within(@federated1, do: Pleroma.User.get_by_id(user.id)).id + end + + test "runs webserver on customized port" do + {nickname, url, url_404} = + within @federated1 do + import Pleroma.Web.Router.Helpers + user = Pleroma.Factory.insert(:user) + user_url = mastodon_api_url(Pleroma.Web.Endpoint, :user, user) + url_404 = mastodon_api_url(Pleroma.Web.Endpoint, :user, "not-exists") + + {user.nickname, user_url, url_404} + end + + assert {:ok, {{_, 200, _}, _headers, body}} = :httpc.request(~c"#{url}") + assert %{"acct" => ^nickname} = Jason.decode!(body) + assert {:ok, {{_, 404, _}, _headers, _body}} = :httpc.request(~c"#{url_404}") + end + end +end diff --git a/test/support/cluster.ex b/test/support/cluster.ex new file mode 100644 index 000000000..deb37f361 --- /dev/null +++ b/test/support/cluster.ex @@ -0,0 +1,218 @@ +defmodule Pleroma.Cluster do + @moduledoc """ + Facilities for managing a cluster of slave VM's for federated testing. + + ## Spawning the federated cluster + + `spawn_cluster/1` spawns a map of slave nodes that are started + within the running VM. During startup, the slave node is sent all configuration + from the parent node, as well as all code. After receiving configuration and + code, the slave then starts all applications currently running on the parent. + The configuration passed to `spawn_cluster/1` overrides any parent application + configuration for the provided OTP app and key. This is useful for customizing + the Ecto database, Phoenix webserver ports, etc. + + For example, to start a single federated VM named ":federated1", with the + Pleroma Endpoint running on port 4123, and with a database named + "pleroma_test1", you would run: + + endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint) + repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo) + + Pleroma.Cluster.spawn_cluster(%{ + :"federated1@127.0.0.1" => [ + {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")}, + {:pleroma, Pleroma.Web.Endpoint, + Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)} + ] + }) + + *Note*: application configuration for a given key is not merged, + so any customization requires first fetching the existing values + and merging yourself by providing the merged configuration, + such as above with the endpoint config and repo config. + + ## Executing code within a remote node + + Use the `within/2` macro to execute code within the context of a remote + federated node. The code block captures all local variable bindings from + the parent's context and returns the result of the expression after executing + it on the remote node. For example: + + import Pleroma.Cluster + + parent_value = 123 + + result = + within :"federated1@127.0.0.1" do + {node(), parent_value} + end + + assert result == {:"federated1@127.0.0.1, 123} + + *Note*: while local bindings are captured and available within the block, + other parent contexts like required, aliased, or imported modules are not + in scope. Those will need to be reimported/aliases/required within the block + as `within/2` is a remote procedure call. + """ + + @extra_apps Pleroma.Mixfile.application()[:extra_applications] + + @doc """ + Spawns the default Pleroma federated cluster. + + Values before may be customized as needed for the test suite. + """ + def spawn_default_cluster do + endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint) + repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo) + + spawn_cluster(%{ + :"federated1@127.0.0.1" => [ + {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")}, + {:pleroma, Pleroma.Web.Endpoint, + Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)} + ], + :"federated2@127.0.0.1" => [ + {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")}, + {:pleroma, Pleroma.Web.Endpoint, + Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)} + ] + }) + end + + @doc """ + Spawns a configured map of federated nodes. + + See `Pleroma.Cluster` module documentation for details. + """ + def spawn_cluster(node_configs) do + # Turn node into a distributed node with the given long name + :net_kernel.start([:"primary@127.0.0.1"]) + + # Allow spawned nodes to fetch all code from this node + {:ok, _} = :erl_boot_server.start([]) + allow_boot("127.0.0.1") + + silence_logger_warnings(fn -> + node_configs + |> Enum.map(&Task.async(fn -> start_slave(&1) end)) + |> Enum.map(&Task.await(&1, 60_000)) + end) + end + + @doc """ + Executes block of code again remote node. + + See `Pleroma.Cluster` module documentation for details. + """ + defmacro within(node, do: block) do + quote do + rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [ + unquote(Macro.escape(block)), + binding() + ]) + end + end + + @doc false + def eval_quoted(block, binding) do + {result, _binding} = Code.eval_quoted(block, binding, __ENV__) + result + end + + defp start_slave({node_host, override_configs}) do + log(node_host, "booting federated VM") + {:ok, node} = :slave.start(~c"127.0.0.1", node_name(node_host), vm_args()) + add_code_paths(node) + load_apps_and_transfer_configuration(node, override_configs) + ensure_apps_started(node) + {:ok, node} + end + + def rpc(node, module, function, args) do + :rpc.block_call(node, module, function, args) + end + + defp vm_args do + ~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}" + end + + defp allow_boot(host) do + {:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}") + :ok = :erl_boot_server.add_slave(ipv4) + end + + defp add_code_paths(node) do + rpc(node, :code, :add_paths, [:code.get_path()]) + end + + defp load_apps_and_transfer_configuration(node, override_configs) do + Enum.each(Application.loaded_applications(), fn {app_name, _, _} -> + app_name + |> Application.get_all_env() + |> Enum.each(fn {key, primary_config} -> + rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]]) + end) + end) + + Enum.each(override_configs, fn {app_name, key, val} -> + rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]]) + end) + end + + defp log(node, msg), do: IO.puts("[#{node}] #{msg}") + + defp ensure_apps_started(node) do + loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end) + app_names = @extra_apps ++ (loaded_names -- @extra_apps) + + rpc(node, Application, :ensure_all_started, [:mix]) + rpc(node, Mix, :env, [Mix.env()]) + rpc(node, __MODULE__, :prepare_database, []) + + log(node, "starting application") + + Enum.reduce(app_names, MapSet.new(), fn app, loaded -> + if Enum.member?(loaded, app) do + loaded + else + {:ok, started} = rpc(node, Application, :ensure_all_started, [app]) + MapSet.union(loaded, MapSet.new(started)) + end + end) + end + + @doc false + def prepare_database do + log(node(), "preparing database") + repo_config = Application.get_env(:pleroma, Pleroma.Repo) + repo_config[:adapter].storage_down(repo_config) + repo_config[:adapter].storage_up(repo_config) + + {:ok, _, _} = + Ecto.Migrator.with_repo(Pleroma.Repo, fn repo -> + Ecto.Migrator.run(repo, :up, log: false, all: true) + end) + + Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) + {:ok, _} = Application.ensure_all_started(:ex_machina) + end + + defp silence_logger_warnings(func) do + prev_level = Logger.level() + Logger.configure(level: :error) + res = func.() + Logger.configure(level: prev_level) + + res + end + + defp node_name(node_host) do + node_host + |> to_string() + |> String.split("@") + |> Enum.at(0) + |> String.to_atom() + end +end From 717d246491f65024957546fbbbaa7209d92d96d1 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 22 Jul 2019 16:17:18 +0200 Subject: [PATCH 25/70] Tests: Don't run federated tests by default --- test/federation/federation_test.exs | 8 ++++++-- test/test_helper.exs | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs index 7400d2abc..8b8e8c870 100644 --- a/test/federation/federation_test.exs +++ b/test/federation/federation_test.exs @@ -4,11 +4,15 @@ defmodule Pleroma.Integration.FederationTest do use Pleroma.DataCase - + @moduletag :federated import Pleroma.Cluster - @federated1 :"federated1@127.0.0.1" + setup_all do + Pleroma.Cluster.spawn_default_cluster() + :ok + end + @federated1 :"federated1@127.0.0.1" describe "federated cluster primitives" do test "within/2 captures local bindings and executes block on remote node" do captured_binding = :captured diff --git a/test/test_helper.exs b/test/test_helper.exs index c8dbee010..b80c2683e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -2,8 +2,7 @@ # Copyright © 2017-2019 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: [] -ExUnit.start(exclude: os_exclude) +ExUnit.start(exclude: [:federated]) Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client) {:ok, _} = Application.ensure_all_started(:ex_machina) From 74aa42998f23f49df1ea43564645ecbea3007c19 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 22 Jul 2019 16:18:37 +0200 Subject: [PATCH 26/70] CI: Add testing stage for federated tests. --- .gitlab-ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ab62c8827..78b560cac 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -55,6 +55,18 @@ unit-testing: - mix ecto.migrate - mix coveralls --preload-modules +federated-testing: + stage: test + services: + - name: lainsoykaf/postgres-with-rum + alias: postgres + command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] + script: + - mix deps.get + - mix ecto.create + - mix ecto.migrate + - mix test --trace --only federated + unit-testing-rum: stage: test services: From bbec9bb727baf6c76656b977fd8fbdd63aaaae07 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 22 Jul 2019 16:32:50 +0200 Subject: [PATCH 27/70] Tests: Run the daemon --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 78b560cac..66fa806f8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,6 +65,7 @@ federated-testing: - mix deps.get - mix ecto.create - mix ecto.migrate + - epmd -daemon - mix test --trace --only federated unit-testing-rum: From bd1984f054b075c04f2098afc5fe326fbc610c10 Mon Sep 17 00:00:00 2001 From: stwf Date: Tue, 26 Nov 2019 15:24:34 -0500 Subject: [PATCH 28/70] update tests and Oban --- mix.exs | 2 +- mix.lock | 8 ++++---- test/federation/federation_test.exs | 4 ++-- test/test_helper.exs | 4 +++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/mix.exs b/mix.exs index 60c7fe3f6..eb2e54e4a 100644 --- a/mix.exs +++ b/mix.exs @@ -102,7 +102,7 @@ defp deps do {:phoenix_ecto, "~> 4.0"}, {:ecto_sql, "~> 3.2"}, {:postgrex, ">= 0.13.5"}, - {:oban, "~> 0.8.1"}, + {:oban, "~> 0.12.0"}, {:quantum, "~> 2.3"}, {:gettext, "~> 0.15"}, {:comeonin, "~> 4.1.1"}, diff --git a/mix.lock b/mix.lock index 3a664287c..0d41c4ad5 100644 --- a/mix.lock +++ b/mix.lock @@ -23,8 +23,8 @@ "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "3.2.3", "51274df79862845b388733fddcf6f107d0c8c86e27abe7131fa98f8d30761bda", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, - "ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, + "ecto": {:hex, :ecto, "3.2.5", "76c864b77948a479e18e69cc1d0f0f4ee7cced1148ffe6a093ff91eba644f0b5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "ecto_sql": {:hex, :ecto_sql, "3.2.2", "d10845bc147b9f61ef485cbf0973c0a337237199bd9bd30dd9542db00aadc26b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0 or ~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, @@ -67,7 +67,7 @@ "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, - "oban": {:hex, :oban, "0.8.1", "4bbf62eb1829f856d69aeb5069ac7036afe07db8221a17de2a9169cc7a58a318", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, + "oban": {:hex, :oban, "0.12.0", "5477d5ab4a5a201c0b6c89764040ebfc5d2c71c488a36f378016ce5990838f0f", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.4.10", "619e4a545505f562cd294df52294372d012823f4fd9d34a6657a8b242898c255", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, @@ -97,7 +97,7 @@ "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"}, "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, - "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, + "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"}, "tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs index 8b8e8c870..45800568a 100644 --- a/test/federation/federation_test.exs +++ b/test/federation/federation_test.exs @@ -33,8 +33,8 @@ test "runs webserver on customized port" do within @federated1 do import Pleroma.Web.Router.Helpers user = Pleroma.Factory.insert(:user) - user_url = mastodon_api_url(Pleroma.Web.Endpoint, :user, user) - url_404 = mastodon_api_url(Pleroma.Web.Endpoint, :user, "not-exists") + user_url = account_url(Pleroma.Web.Endpoint, :show, user) + url_404 = account_url(Pleroma.Web.Endpoint, :show, "not-exists") {user.nickname, user_url, url_404} end diff --git a/test/test_helper.exs b/test/test_helper.exs index b80c2683e..241ad1f94 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -2,7 +2,9 @@ # Copyright © 2017-2019 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -ExUnit.start(exclude: [:federated]) +os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: [] +ExUnit.start(exclude: [:federated | os_exclude]) + Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client) {:ok, _} = Application.ensure_all_started(:ex_machina) From 1fc28a4b441fdc0de8b2b43e2b045da38ee50b0a Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 26 Nov 2019 18:48:56 +0700 Subject: [PATCH 29/70] 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 5135656f579954cf786011b539934c7150e0d0bc Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Wed, 27 Nov 2019 22:54:12 +0900 Subject: [PATCH 30/70] Handle reopened reports with deleted statuses --- lib/pleroma/web/activity_pub/utils.ex | 50 ++++++++------- .../web/admin_api/views/report_view.ex | 11 ++-- .../admin_api/admin_api_controller_test.exs | 64 ++++++++++++++++++- 3 files changed, 93 insertions(+), 32 deletions(-) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 962f02a05..d91abf7b3 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -722,16 +722,22 @@ defp build_flag_object(act) when is_map(act) or is_binary(act) do act when is_binary(act) -> act end - activity = Activity.get_by_ap_id_with_object(id) - actor = User.get_by_ap_id(activity.object.data["actor"]) + case Activity.get_by_ap_id_with_object(id) do + %Activity{} = activity -> + %{ + "type" => "Note", + "id" => activity.data["id"], + "content" => activity.object.data["content"], + "published" => activity.object.data["published"], + "actor" => + AccountView.render("show.json", %{ + user: User.get_by_ap_id(activity.object.data["actor"]) + }) + } - %{ - "type" => "Note", - "id" => activity.data["id"], - "content" => activity.object.data["content"], - "published" => activity.object.data["published"], - "actor" => AccountView.render("show.json", %{user: actor}) - } + _ -> + %{"id" => id, "deleted" => true} + end end defp build_flag_object(_), do: [] @@ -792,30 +798,27 @@ def parse_report_group(activity) do reports = get_reports_by_status_id(activity["id"]) max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"])) actors = Enum.map(reports, & &1.user_actor) - {deleted, status} = get_status_data(activity) + status = get_status_data(activity) %{ date: max_date.data["published"], account: activity["actor"], status: status, - status_deleted: deleted, actors: Enum.uniq(actors), reports: reports } end - defp get_status_data(activity) do - case Activity.get_by_ap_id(activity["id"]) do - %Activity{} = act -> - {false, act} + defp get_status_data(status) do + case status["deleted"] do + true -> + %{ + "id" => status["id"], + "deleted" => true + } _ -> - {true, - %{ - id: activity["id"], - content: activity["content"], - published: activity["published"] - }} + Activity.get_by_ap_id(status["id"]) end end @@ -829,7 +832,7 @@ def get_reports_by_status_id(ap_id) do |> Repo.all() end - @spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{ + @spec get_reports_grouped_by_status([String.t()]) :: %{ required(:groups) => [ %{ required(:date) => String.t(), @@ -838,8 +841,7 @@ def get_reports_by_status_id(ap_id) do required(:actors) => [%User{}], required(:reports) => [%Activity{}] } - ], - required(:total) => integer + ] } def get_reports_grouped_by_status(activity_ids) do parsed_groups = diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 45ce75272..13602efd9 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do use Pleroma.Web, :view + alias Pleroma.Activity alias Pleroma.HTML alias Pleroma.User alias Pleroma.Web.AdminAPI.Report @@ -46,15 +47,15 @@ def render("index_grouped.json", %{groups: groups}) do reports = Enum.map(groups, fn group -> status = - if group[:status_deleted], - do: group[:status], - else: StatusView.render("show.json", %{activity: group[:status]}) + case group.status do + %Activity{} = activity -> StatusView.render("show.json", %{activity: activity}) + _ -> group.status + end %{ date: group[:date], account: group[:account], - status: status, - status_deleted: group[:status_deleted], + status: Map.put_new(status, "deleted", false), actors: Enum.map(group[:actors], &merge_account_views/1), reports: group[:reports] diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index c6ff1a065..a69fadcdc 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1613,6 +1613,7 @@ test "returns 403 when requested by anonymous" do first_status: Activity.get_by_ap_id_with_object(first_status.data["id"]), second_status: Activity.get_by_ap_id_with_object(second_status.data["id"]), third_status: Activity.get_by_ap_id_with_object(third_status.data["id"]), + first_report: first_report, first_status_reports: [first_report, second_report, third_report], second_status_reports: [first_report, second_report], third_status_reports: [first_report], @@ -1655,7 +1656,11 @@ test "returns reports grouped by status", %{ end).data["published"] assert first_group["status"] == - stringify_keys(StatusView.render("show.json", %{activity: first_status})) + Map.put( + stringify_keys(StatusView.render("show.json", %{activity: first_status})), + "deleted", + false + ) assert(first_group["account"]["id"] == target_user.id) @@ -1671,7 +1676,11 @@ test "returns reports grouped by status", %{ end).data["published"] assert second_group["status"] == - stringify_keys(StatusView.render("show.json", %{activity: second_status})) + Map.put( + stringify_keys(StatusView.render("show.json", %{activity: second_status})), + "deleted", + false + ) assert second_group["account"]["id"] == target_user.id @@ -1687,7 +1696,11 @@ test "returns reports grouped by status", %{ end).data["published"] assert third_group["status"] == - stringify_keys(StatusView.render("show.json", %{activity: third_status})) + Map.put( + stringify_keys(StatusView.render("show.json", %{activity: third_status})), + "deleted", + false + ) assert third_group["account"]["id"] == target_user.id @@ -1697,6 +1710,51 @@ test "returns reports grouped by status", %{ assert Enum.map(third_group["reports"], & &1["id"]) -- Enum.map(third_status_reports, & &1.id) == [] end + + test "reopened report renders status data", %{ + conn: conn, + first_report: first_report, + first_status: first_status + } do + {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved") + + response = + conn + |> get("/api/pleroma/admin/grouped_reports") + |> json_response(:ok) + + first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id)) + + assert first_group["status"] == + Map.put( + stringify_keys(StatusView.render("show.json", %{activity: first_status})), + "deleted", + false + ) + end + + test "reopened report does not render status data if status has been deleted", %{ + conn: conn, + first_report: first_report, + first_status: first_status, + target_user: target_user + } do + {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved") + {:ok, _} = CommonAPI.delete(first_status.id, target_user) + + refute Activity.get_by_ap_id(first_status.id) + + response = + conn + |> get("/api/pleroma/admin/grouped_reports") + |> json_response(:ok) + + assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["status"][ + "deleted" + ] == true + + assert length(Enum.filter(response["reports"], &(&1["status"]["deleted"] == false))) == 2 + end end describe "POST /api/pleroma/admin/reports/:id/respond" do From fcabcab4430b0aa075243bf98630d67c79f3ef9b Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Thu, 28 Nov 2019 00:09:00 +0900 Subject: [PATCH 31/70] Fetch account from report, not from status (it might be deleted) --- lib/pleroma/web/activity_pub/utils.ex | 9 ++++++++- .../web/admin_api/admin_api_controller.ex | 4 ++-- .../admin_api/admin_api_controller_test.exs | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index d91abf7b3..2ca805c09 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -798,11 +798,18 @@ def parse_report_group(activity) do reports = get_reports_by_status_id(activity["id"]) max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"])) actors = Enum.map(reports, & &1.user_actor) + [%{data: %{"object" => [account_id | _]}} | _] = reports + + account = + AccountView.render("show.json", %{ + user: User.get_by_ap_id(account_id) + }) + status = get_status_data(activity) %{ date: max_date.data["published"], - account: activity["actor"], + account: account, status: status, actors: Enum.uniq(actors), reports: reports diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 24fdc3c82..b003d1f35 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -647,11 +647,11 @@ def list_reports(conn, params) do end def list_grouped_reports(conn, _params) do - reports = Utils.get_reported_activities() + statuses = Utils.get_reported_activities() conn |> put_view(ReportView) - |> render("index_grouped.json", Utils.get_reports_grouped_by_status(reports)) + |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses)) end def report_show(conn, %{"id" => id}) do diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index a69fadcdc..108baad91 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1755,6 +1755,25 @@ test "reopened report does not render status data if status has been deleted", % assert length(Enum.filter(response["reports"], &(&1["status"]["deleted"] == false))) == 2 end + + test "account not empty if status was deleted", %{ + conn: conn, + first_report: first_report, + first_status: first_status, + target_user: target_user + } do + {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved") + {:ok, _} = CommonAPI.delete(first_status.id, target_user) + + refute Activity.get_by_ap_id(first_status.id) + + response = + conn + |> get("/api/pleroma/admin/grouped_reports") + |> json_response(:ok) + + assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["account"] + end end describe "POST /api/pleroma/admin/reports/:id/respond" do From c4b468a95bd22dae16d89024016b2590e38d23ae Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 28 Nov 2019 16:44:48 +0700 Subject: [PATCH 32/70] Cleanup tests output --- test/web/activity_pub/transmogrifier_test.exs | 5 +++++ test/web/admin_api/admin_api_controller_test.exs | 1 + test/web/streamer/streamer_test.exs | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 0bdd514e9..b31c411dc 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -39,6 +39,7 @@ test "it ignores an incoming notice if we already have it" do assert activity == returned_activity end + @tag capture_log: true test "it fetches replied-to activities if we don't have them" do data = File.read!("test/fixtures/mastodon-post-activity.json") @@ -533,6 +534,7 @@ test "it works for incoming announces with an inlined activity" do assert object.data["content"] == "this is a private toot" end + @tag capture_log: true test "it rejects incoming announces with an inlined activity from another origin" do data = File.read!("test/fixtures/bogus-mastodon-announce.json") @@ -814,6 +816,7 @@ test "it fails for incoming deletes with spoofed origin" do assert Activity.get_by_id(activity.id) end + @tag capture_log: true test "it works for incoming user deletes" do %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") @@ -1749,6 +1752,7 @@ test "returns object with inReplyToAtomUri when denied incoming reply", %{data: assert modified_object["inReplyToAtomUri"] == "" end + @tag capture_log: true test "returns modified object when allowed incoming reply", %{data: data} do object_with_reply = Map.put( @@ -1868,6 +1872,7 @@ test "returns nil when cannot normalize object" do end) =~ "Unsupported URI scheme" end + @tag capture_log: true test "returns {:ok, %Object{}} for success case" do assert {:ok, %Object{}} = Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873") diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index bb2ca6a62..32577afee 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1923,6 +1923,7 @@ test "with settings in db", %{conn: conn} do Pleroma.Config.put([:instance, :dynamic_configuration], true) end + @tag capture_log: true test "create new config setting in db", %{conn: conn} do conn = post(conn, "/api/pleroma/admin/config", %{ diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 80a7541b2..8265f18dd 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -15,7 +15,7 @@ defmodule Pleroma.Web.StreamerTest do alias Pleroma.Web.Streamer.StreamerSocket alias Pleroma.Web.Streamer.Worker - @moduletag needs_streamer: true + @moduletag needs_streamer: true, capture_log: true clear_config_all([:instance, :skip_thread_containment]) describe "user streams" do From 9a2d38107ccaf8730a51ac052c73153d385a5cb6 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 28 Nov 2019 15:14:11 +0100 Subject: [PATCH 33/70] CI: Make benchmarks manually triggered. --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ab62c8827..c4cde5cb9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,6 +31,7 @@ build: benchmark: stage: benchmark + when: manual variables: MIX_ENV: benchmark services: From f0bdbe3f61e3f3278f76d31dad447b982e2e2571 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 28 Nov 2019 17:01:43 +0100 Subject: [PATCH 34/70] Migrations: Set users.following_count to NOT NULL Also set following_count for local users to the correct value and for remote users to 0. --- ...1128153944_fix_missing_following_count.exs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 priv/repo/migrations/20191128153944_fix_missing_following_count.exs diff --git a/priv/repo/migrations/20191128153944_fix_missing_following_count.exs b/priv/repo/migrations/20191128153944_fix_missing_following_count.exs new file mode 100644 index 000000000..3236de7a4 --- /dev/null +++ b/priv/repo/migrations/20191128153944_fix_missing_following_count.exs @@ -0,0 +1,53 @@ +defmodule Pleroma.Repo.Migrations.FixMissingFollowingCount do + use Ecto.Migration + + def up do + """ + UPDATE + users + SET + following_count = sub.count + FROM + ( + SELECT + users.id AS sub_id + ,COUNT (following_relationships.id) + FROM + following_relationships + ,users + WHERE + users.id = following_relationships.follower_id + AND following_relationships.state = 'accept' + GROUP BY + users.id + ) AS sub + WHERE + users.id = sub.sub_id + AND users.local = TRUE + ; + """ + |> execute() + + """ + UPDATE + users + SET + following_count = 0 + WHERE + following_count IS NULL + """ + |> execute() + + execute("ALTER TABLE users + ALTER COLUMN following_count SET DEFAULT 0, + ALTER COLUMN following_count SET NOT NULL + ") + end + + def down do + execute("ALTER TABLE users + ALTER COLUMN following_count DROP DEFAULT, + ALTER COLUMN following_count DROP NOT NULL + ") + end +end From 798aa0c3350ba24b962c54bd1b9e5a14a70a76c0 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 28 Nov 2019 23:08:13 +0700 Subject: [PATCH 35/70] Use PosgreSQL v12 in the federated tests --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 66fa806f8..1a5df7f7e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -58,7 +58,7 @@ unit-testing: federated-testing: stage: test services: - - name: lainsoykaf/postgres-with-rum + - name: minibikini/postgres-with-rum:12 alias: postgres command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] script: From a52da55eb9c6bbf8a08bf1d90d59a48dc25f5907 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Mon, 28 Oct 2019 12:47:23 +0300 Subject: [PATCH 36/70] added User.NotificationSetting struct --- lib/pleroma/notification.ex | 8 +-- lib/pleroma/user.ex | 26 +++------- lib/pleroma/user/notification_setting.ex | 49 +++++++++++++++++++ test/notification_test.exs | 20 ++++++-- test/support/builders/user_builder.ex | 3 +- test/support/factory.ex | 3 +- test/user/notification_setting_test.exs | 40 +++++++++++++++ test/user_search_test.exs | 1 + .../mastodon_api/views/account_view_test.exs | 8 +-- test/web/twitter_api/util_controller_test.exs | 30 ++++++++++-- 10 files changed, 146 insertions(+), 42 deletions(-) create mode 100644 lib/pleroma/user/notification_setting.ex create mode 100644 test/user/notification_setting_test.exs diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index b7ecf51e4..acb635fdc 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -314,7 +314,7 @@ def skip?(:self, activity, user) do def skip?( :followers, activity, - %{notification_settings: %{"followers" => false}} = user + %{notification_settings: %{followers: false}} = user ) do actor = activity.data["actor"] follower = User.get_cached_by_ap_id(actor) @@ -324,14 +324,14 @@ def skip?( def skip?( :non_followers, activity, - %{notification_settings: %{"non_followers" => false}} = user + %{notification_settings: %{non_followers: false}} = user ) do actor = activity.data["actor"] follower = User.get_cached_by_ap_id(actor) !User.following?(follower, user) end - def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do + def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user) do actor = activity.data["actor"] followed = User.get_cached_by_ap_id(actor) User.following?(user, followed) @@ -340,7 +340,7 @@ def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = def skip?( :non_follows, activity, - %{notification_settings: %{"non_follows" => false}} = user + %{notification_settings: %{non_follows: false}} = user ) do actor = activity.data["actor"] followed = User.get_cached_by_ap_id(actor) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index b18a4c6a5..94fca2a9f 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -105,13 +105,10 @@ defmodule Pleroma.User do field(:invisible, :boolean, default: false) field(:skip_thread_containment, :boolean, default: false) - field(:notification_settings, :map, - default: %{ - "followers" => true, - "follows" => true, - "non_follows" => true, - "non_followers" => true - } + embeds_one( + :notification_settings, + Pleroma.User.NotificationSetting, + on_replace: :update ) has_many(:notifications, Notification) @@ -1095,20 +1092,9 @@ def deactivate(%User{} = user, status) do end def update_notification_settings(%User{} = user, settings) do - settings = - settings - |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end) - |> Map.new() - - notification_settings = - user.notification_settings - |> Map.merge(settings) - |> Map.take(["followers", "follows", "non_follows", "non_followers"]) - - params = %{notification_settings: notification_settings} - user - |> cast(params, [:notification_settings]) + |> cast(%{notification_settings: settings}, []) + |> cast_embed(:notification_settings) |> validate_required([:notification_settings]) |> update_and_set_cache() end diff --git a/lib/pleroma/user/notification_setting.ex b/lib/pleroma/user/notification_setting.ex new file mode 100644 index 000000000..64100c0e6 --- /dev/null +++ b/lib/pleroma/user/notification_setting.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.NotificationSetting do + use Ecto.Schema + import Ecto.Changeset + + @derive Jason.Encoder + @primary_key false + + @privacy_options %{ + name_and_message: "name_and_message", + name_only: "name_only", + no_name_or_message: "no_name_or_message" + } + + embedded_schema do + field(:followers, :boolean, default: true) + field(:follows, :boolean, default: true) + field(:non_follows, :boolean, default: true) + field(:non_followers, :boolean, default: true) + field(:privacy_option, :string, default: @privacy_options.name_and_message) + end + + def changeset(schema, params) do + schema + |> cast(prepare_attrs(params), [ + :followers, + :follows, + :non_follows, + :non_followers, + :privacy_option + ]) + |> validate_inclusion(:privacy_option, Map.values(@privacy_options)) + end + + defp prepare_attrs(params) do + Enum.reduce(params, %{}, fn + {k, v}, acc + when k in ["followers", "follows", "non_follows", "non_followers"] and + is_binary(v) -> + Map.put(acc, k, String.downcase(v)) + + {k, v}, acc -> + Map.put(acc, k, v) + end) + end +end diff --git a/test/notification_test.exs b/test/notification_test.exs index f8d429223..e7c031c8f 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -136,7 +136,10 @@ test "it creates a notification for an activity from a muted thread" do test "it disables notifications from followers" do follower = insert(:user) - followed = insert(:user, notification_settings: %{"followers" => false}) + + followed = + insert(:user, notification_settings: %Pleroma.User.NotificationSetting{followers: false}) + User.follow(follower, followed) {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) refute Notification.create_notification(activity, followed) @@ -144,13 +147,20 @@ test "it disables notifications from followers" do test "it disables notifications from non-followers" do follower = insert(:user) - followed = insert(:user, notification_settings: %{"non_followers" => false}) + + followed = + insert(:user, + notification_settings: %Pleroma.User.NotificationSetting{non_followers: false} + ) + {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) refute Notification.create_notification(activity, followed) end test "it disables notifications from people the user follows" do - follower = insert(:user, notification_settings: %{"follows" => false}) + follower = + insert(:user, notification_settings: %Pleroma.User.NotificationSetting{follows: false}) + followed = insert(:user) User.follow(follower, followed) follower = Repo.get(User, follower.id) @@ -159,7 +169,9 @@ test "it disables notifications from people the user follows" do end test "it disables notifications from people the user does not follow" do - follower = insert(:user, notification_settings: %{"non_follows" => false}) + follower = + insert(:user, notification_settings: %Pleroma.User.NotificationSetting{non_follows: false}) + followed = insert(:user) {:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"}) refute Notification.create_notification(activity, follower) diff --git a/test/support/builders/user_builder.ex b/test/support/builders/user_builder.ex index 6da16f71a..fcfea666f 100644 --- a/test/support/builders/user_builder.ex +++ b/test/support/builders/user_builder.ex @@ -10,7 +10,8 @@ def build(data \\ %{}) do password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), bio: "A tester.", ap_id: "some id", - last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + notification_settings: %Pleroma.User.NotificationSetting{} } Map.merge(user, data) diff --git a/test/support/factory.ex b/test/support/factory.ex index e3f797f64..4bd82ebd6 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -32,7 +32,8 @@ def user_factory do password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), bio: sequence(:bio, &"Tester Number #{&1}"), info: %{}, - last_digest_emailed_at: NaiveDateTime.utc_now() + last_digest_emailed_at: NaiveDateTime.utc_now(), + notification_settings: %Pleroma.User.NotificationSetting{} } %{ diff --git a/test/user/notification_setting_test.exs b/test/user/notification_setting_test.exs new file mode 100644 index 000000000..d1f766eb3 --- /dev/null +++ b/test/user/notification_setting_test.exs @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.NotificationSettingTest do + use Pleroma.DataCase + + alias Pleroma.User.NotificationSetting + + describe "changeset/2" do + test "sets valid privacy option" do + changeset = + NotificationSetting.changeset( + %NotificationSetting{}, + %{"privacy_option" => "name_only"} + ) + + assert %Ecto.Changeset{valid?: true} = changeset + end + + test "returns invalid changeset when privacy option is incorrect" do + changeset = + NotificationSetting.changeset( + %NotificationSetting{}, + %{"privacy_option" => "full_content"} + ) + + assert %Ecto.Changeset{valid?: false} = changeset + + assert [ + privacy_option: + {"is invalid", + [ + validation: :inclusion, + enum: ["name_and_message", "name_only", "no_name_or_message"] + ]} + ] = changeset.errors + end + end +end diff --git a/test/user_search_test.exs b/test/user_search_test.exs index 98841dbbd..821858476 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -174,6 +174,7 @@ test "works with URIs" do |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) |> Map.put(:last_digest_emailed_at, nil) + |> Map.put(:notification_settings, nil) assert user == expected end diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index d147079ab..7feff560c 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -92,13 +92,7 @@ test "Represent a user account" do test "Represent the user account for the account owner" do user = insert(:user) - notification_settings = %{ - "followers" => true, - "follows" => true, - "non_follows" => true, - "non_followers" => true - } - + notification_settings = %Pleroma.User.NotificationSetting{} privacy = user.default_scope assert %{ diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index f0211f59c..f1557c193 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -159,11 +159,31 @@ test "it updates notification settings", %{conn: conn} do user = Repo.get(User, user.id) - assert %{ - "followers" => false, - "follows" => true, - "non_follows" => true, - "non_followers" => true + assert %Pleroma.User.NotificationSetting{ + followers: false, + follows: true, + non_follows: true, + non_followers: true, + privacy_option: "name_and_message" + } == user.notification_settings + end + + test "it update notificatin privacy option", %{conn: conn} do + user = insert(:user) + + conn + |> assign(:user, user) + |> put("/api/pleroma/notification_settings", %{"privacy_option" => "name_only"}) + |> json_response(:ok) + + user = refresh_record(user) + + assert %Pleroma.User.NotificationSetting{ + followers: true, + follows: true, + non_follows: true, + non_followers: true, + privacy_option: "name_only" } == user.notification_settings end end From 04a8ffbe84c6d40709860e75fffa0330a2db690f Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 29 Oct 2019 21:33:17 +0300 Subject: [PATCH 37/70] added privacy option to push notifications --- lib/pleroma/user/notification_setting.ex | 13 +---- lib/pleroma/web/push/impl.ex | 27 +++++++++-- lib/pleroma/workers/web_pusher_worker.ex | 2 +- test/user/notification_setting_test.exs | 21 +-------- test/web/push/impl_test.exs | 47 +++++++++++++++++++ test/web/twitter_api/util_controller_test.exs | 6 +-- 6 files changed, 76 insertions(+), 40 deletions(-) diff --git a/lib/pleroma/user/notification_setting.ex b/lib/pleroma/user/notification_setting.ex index 64100c0e6..f0899613e 100644 --- a/lib/pleroma/user/notification_setting.ex +++ b/lib/pleroma/user/notification_setting.ex @@ -9,18 +9,12 @@ defmodule Pleroma.User.NotificationSetting do @derive Jason.Encoder @primary_key false - @privacy_options %{ - name_and_message: "name_and_message", - name_only: "name_only", - no_name_or_message: "no_name_or_message" - } - embedded_schema do field(:followers, :boolean, default: true) field(:follows, :boolean, default: true) field(:non_follows, :boolean, default: true) field(:non_followers, :boolean, default: true) - field(:privacy_option, :string, default: @privacy_options.name_and_message) + field(:privacy_option, :boolean, default: false) end def changeset(schema, params) do @@ -32,14 +26,11 @@ def changeset(schema, params) do :non_followers, :privacy_option ]) - |> validate_inclusion(:privacy_option, Map.values(@privacy_options)) end defp prepare_attrs(params) do Enum.reduce(params, %{}, fn - {k, v}, acc - when k in ["followers", "follows", "non_follows", "non_followers"] and - is_binary(v) -> + {k, v}, acc when is_binary(v) -> Map.put(acc, k, String.downcase(v)) {k, v}, acc -> diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 3de7af708..53f93c1ed 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -22,8 +22,8 @@ defmodule Pleroma.Web.Push.Impl do @spec perform(Notification.t()) :: list(any) | :error def perform( %{ - activity: %{data: %{"type" => activity_type}, id: activity_id} = activity, - user_id: user_id + activity: %{data: %{"type" => activity_type}} = activity, + user: %User{id: user_id} } = notif ) when activity_type in @types do @@ -39,18 +39,17 @@ def perform( for subscription <- fetch_subsriptions(user_id), get_in(subscription.data, ["alerts", type]) do %{ - title: format_title(notif), access_token: subscription.token.token, - body: format_body(notif, actor, object), notification_id: notif.id, notification_type: type, icon: avatar_url, preferred_locale: "en", pleroma: %{ - activity_id: activity_id, + activity_id: notif.activity.id, direct_conversation_id: direct_conversation_id } } + |> Map.merge(build_content(notif, actor, object)) |> Jason.encode!() |> push_message(build_sub(subscription), gcm_api_key, subscription) end @@ -100,6 +99,24 @@ def build_sub(subscription) do } end + def build_content( + %{ + activity: %{data: %{"directMessage" => true}}, + user: %{notification_settings: %{privacy_option: true}} + }, + actor, + _ + ) do + %{title: "New Direct Message", body: "@#{actor.nickname}"} + end + + def build_content(notif, actor, object) do + %{ + title: format_title(notif), + body: format_body(notif, actor, object) + } + end + def format_body( %{activity: %{data: %{"type" => "Create"}}}, actor, diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex index 61b451e3e..a978c4013 100644 --- a/lib/pleroma/workers/web_pusher_worker.ex +++ b/lib/pleroma/workers/web_pusher_worker.ex @@ -13,7 +13,7 @@ def perform(%{"op" => "web_push", "notification_id" => notification_id}, _job) d notification = Notification |> Repo.get(notification_id) - |> Repo.preload([:activity]) + |> Repo.preload([:activity, :user]) Pleroma.Web.Push.Impl.perform(notification) end diff --git a/test/user/notification_setting_test.exs b/test/user/notification_setting_test.exs index d1f766eb3..4744d7b4a 100644 --- a/test/user/notification_setting_test.exs +++ b/test/user/notification_setting_test.exs @@ -12,29 +12,10 @@ test "sets valid privacy option" do changeset = NotificationSetting.changeset( %NotificationSetting{}, - %{"privacy_option" => "name_only"} + %{"privacy_option" => true} ) assert %Ecto.Changeset{valid?: true} = changeset end - - test "returns invalid changeset when privacy option is incorrect" do - changeset = - NotificationSetting.changeset( - %NotificationSetting{}, - %{"privacy_option" => "full_content"} - ) - - assert %Ecto.Changeset{valid?: false} = changeset - - assert [ - privacy_option: - {"is invalid", - [ - validation: :inclusion, - enum: ["name_and_message", "name_only", "no_name_or_message"] - ]} - ] = changeset.errors - end end end diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index 9b554601d..acae7a734 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.Push.ImplTest do use Pleroma.DataCase alias Pleroma.Object + alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.Push.Impl alias Pleroma.Web.Push.Subscription @@ -182,4 +183,50 @@ test "renders title for create activity with direct visibility" do assert Impl.format_title(%{activity: activity}) == "New Direct Message" end + + describe "build_content/3" do + test "returns info content for direct message with enabled privacy option" do + user = insert(:user, nickname: "Bob") + user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true}) + + {:ok, activity} = + CommonAPI.post(user, %{ + "visibility" => "direct", + "status" => " "direct", + "status" => + "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + notif = insert(:notification, user: user2, activity: activity) + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: + "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", + title: "New Direct Message" + } + end + end end diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index f1557c193..5568c479d 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -164,7 +164,7 @@ test "it updates notification settings", %{conn: conn} do follows: true, non_follows: true, non_followers: true, - privacy_option: "name_and_message" + privacy_option: false } == user.notification_settings end @@ -173,7 +173,7 @@ test "it update notificatin privacy option", %{conn: conn} do conn |> assign(:user, user) - |> put("/api/pleroma/notification_settings", %{"privacy_option" => "name_only"}) + |> put("/api/pleroma/notification_settings", %{"privacy_option" => "1"}) |> json_response(:ok) user = refresh_record(user) @@ -183,7 +183,7 @@ test "it update notificatin privacy option", %{conn: conn} do follows: true, non_follows: true, non_followers: true, - privacy_option: "name_only" + privacy_option: true } == user.notification_settings end end From a36607c27e6a50aeca450570f7b8e4c9c0233bb1 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Wed, 30 Oct 2019 22:59:04 +0300 Subject: [PATCH 38/70] add mix task to set\unset privacy option of notification --- .../tasks/pleroma/notification_settings.ex | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 lib/mix/tasks/pleroma/notification_settings.ex diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex new file mode 100644 index 000000000..7d65f0587 --- /dev/null +++ b/lib/mix/tasks/pleroma/notification_settings.ex @@ -0,0 +1,83 @@ +defmodule Mix.Tasks.Pleroma.NotificationSettings do + @shortdoc "Enable&Disable privacy option for push notifications" + @moduledoc """ + Example: + + > mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588" # set false only for parallel588 user + > mix pleroma.notification_settings --privacy-option=true # set true for all users + + """ + + use Mix.Task + import Mix.Pleroma + import Ecto.Query + + def run(args) do + start_pleroma() + + {options, _, _} = + OptionParser.parse( + args, + strict: [ + privacy_option: :boolean, + email_users: :string, + nickname_users: :string + ] + ) + + privacy_option = Keyword.get(options, :privacy_option) + + if not is_nil(privacy_option) do + privacy_option + |> build_query(options) + |> Pleroma.Repo.update_all([]) + end + + shell_info("Done") + end + + defp build_query(privacy_option, options) do + query = + from(u in Pleroma.User, + update: [ + set: [ + notification_settings: + fragment( + "jsonb_set(notification_settings, '{privacy_option}', ?)", + ^privacy_option + ) + ] + ] + ) + + user_emails = + options + |> Keyword.get(:email_users, "") + |> String.split(",") + |> Enum.map(&String.trim(&1)) + |> Enum.reject(&(&1 == "")) + + query = + if length(user_emails) > 0 do + where(query, [u], u.email in ^user_emails) + else + query + end + + user_nicknames = + options + |> Keyword.get(:nickname_users, "") + |> String.split(",") + |> Enum.map(&String.trim(&1)) + |> Enum.reject(&(&1 == "")) + + query = + if length(user_nicknames) > 0 do + where(query, [u], u.nickname in ^user_nicknames) + else + query + end + + query + end +end From a98cda77580ead154dd2cb020c6d4ebc1e719d86 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 29 Nov 2019 15:49:35 +0700 Subject: [PATCH 39/70] Fix Pleroma.HTML.extract_first_external_url/2 --- lib/pleroma/html.ex | 1 + test/html_test.exs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 997e965f0..4acd46253 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -91,6 +91,7 @@ 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/test/html_test.exs b/test/html_test.exs index f0869534c..c918dbe20 100644 --- a/test/html_test.exs +++ b/test/html_test.exs @@ -228,5 +228,16 @@ test "skips microformats hashtags" do assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" end + + test "does not crash when there is an HTML entity in a link" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "\"http://cofe.com/?boomer=ok&foo=bar\""}) + + object = Object.normalize(activity) + + assert {:ok, nil} = HTML.extract_first_external_url(object, object.data["content"]) + end end end From 51111e286b316340b45b4e6a378646bed0cb0a6a Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 29 Nov 2019 18:57:19 +0300 Subject: [PATCH 40/70] [#1427] Initial support for OAuth admin scopes. --- .../web/admin_api/admin_api_controller.ex | 16 ++++----- lib/pleroma/web/oauth/oauth_controller.ex | 10 +++--- lib/pleroma/web/oauth/scopes.ex | 34 ++++++++++++++++--- .../controllers/emoji_api_controller.ex | 2 +- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 8c1318d1b..01cd12c96 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -30,13 +30,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: ["read:accounts"]} + %{scopes: ["admin:read:accounts", "read:accounts"]} when action in [:list_users, :user_show, :right_get, :invites] ) plug( OAuthScopesPlug, - %{scopes: ["write:accounts"]} + %{scopes: ["admin:write:accounts", "write:accounts"]} when action in [ :get_invite_token, :revoke_invite, @@ -58,35 +58,35 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: ["read:reports"]} when action in [:list_reports, :report_show] + %{scopes: ["admin:read:reports", "read:reports"]} when action in [:list_reports, :report_show] ) plug( OAuthScopesPlug, - %{scopes: ["write:reports"]} + %{scopes: ["admin:reports", "write:reports"]} when action in [:report_update_state, :report_respond] ) plug( OAuthScopesPlug, - %{scopes: ["read:statuses"]} when action == :list_user_statuses + %{scopes: ["admin:read:statuses", "read:statuses"]} when action == :list_user_statuses ) plug( OAuthScopesPlug, - %{scopes: ["write:statuses"]} + %{scopes: ["admin:write:statuses", "write:statuses"]} when action in [:status_update, :status_delete] ) plug( OAuthScopesPlug, - %{scopes: ["read"]} + %{scopes: ["admin:read", "read"]} when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log] ) plug( OAuthScopesPlug, - %{scopes: ["write"]} + %{scopes: ["admin:write", "write"]} when action in [:relay_follow, :relay_unfollow, :config_update] ) diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 2aee8cab2..87acdec97 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -222,7 +222,7 @@ def token_exchange( {:user_active, true} <- {:user_active, !user.deactivated}, {:password_reset_pending, false} <- {:password_reset_pending, user.password_reset_pending}, - {:ok, scopes} <- validate_scopes(app, params), + {:ok, scopes} <- validate_scopes(app, params, user), {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, token} <- Token.exchange_token(app, auth) do json(conn, Token.Response.build(user, token)) @@ -471,7 +471,7 @@ defp do_create_authorization( {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, %App{} = app <- Repo.get_by(App, client_id: client_id), true <- redirect_uri in String.split(app.redirect_uris), - {:ok, scopes} <- validate_scopes(app, auth_attrs), + {:ok, scopes} <- validate_scopes(app, auth_attrs, user), {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do Authorization.create_authorization(app, user, scopes) end @@ -487,12 +487,12 @@ defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :re defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), do: put_session(conn, :registration_id, registration_id) - @spec validate_scopes(App.t(), map()) :: + @spec validate_scopes(App.t(), map(), User.t()) :: {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - defp validate_scopes(app, params) do + defp validate_scopes(%App{} = app, params, %User{} = user) do params |> Scopes.fetch_scopes(app.scopes) - |> Scopes.validate(app.scopes) + |> Scopes.validate(app.scopes, user) end def default_redirect_uri(%App{} = app) do diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 48bd14407..0c8796ecb 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -7,6 +7,9 @@ defmodule Pleroma.Web.OAuth.Scopes do Functions for dealing with scopes. """ + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + @doc """ Fetch scopes from request params. @@ -53,15 +56,36 @@ def to_string(scopes), do: Enum.join(scopes, " ") @doc """ Validates scopes. """ - @spec validate(list() | nil, list()) :: + @spec validate(list() | nil, list(), User.t()) :: {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - def validate([], _app_scopes), do: {:error, :missing_scopes} - def validate(nil, _app_scopes), do: {:error, :missing_scopes} + def validate(blank_scopes, _app_scopes, _user) when blank_scopes in [nil, []], + do: {:error, :missing_scopes} - def validate(scopes, app_scopes) do - case Pleroma.Plugs.OAuthScopesPlug.filter_descendants(scopes, app_scopes) do + def validate(scopes, app_scopes, %User{} = user) do + with {:ok, _} <- ensure_scopes_support(scopes, app_scopes), + {:ok, scopes} <- authorize_admin_scopes(scopes, app_scopes, user) do + {:ok, scopes} + end + end + + defp ensure_scopes_support(scopes, app_scopes) do + case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do ^scopes -> {:ok, scopes} _ -> {:error, :unsupported_scopes} end end + + defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do + if user.is_admin || !contains_admin_scopes?(scopes) || !contains_admin_scopes?(app_scopes) do + {:ok, scopes} + else + {:error, :unsupported_scopes} + end + end + + defp contains_admin_scopes?(scopes) do + scopes + |> OAuthScopesPlug.filter_descendants(["admin"]) + |> Enum.any?() + end end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index a474d41d4..1e8c62e3a 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do plug( OAuthScopesPlug, - %{scopes: ["write"]} + %{scopes: ["admin:write", "write"]} when action in [ :create, :delete, From 0326683932800c20dcec2b7d3500bff149cf091b Mon Sep 17 00:00:00 2001 From: kPherox Date: Sat, 30 Nov 2019 04:26:45 +0900 Subject: [PATCH 41/70] hide follower/following for friendica --- lib/pleroma/web/activity_pub/activity_pub.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index f25314ff6..f32d04175 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1233,13 +1233,13 @@ defp maybe_update_follow_information(data) do end end - defp collection_private(data) do - if is_map(data["first"]) and - data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do + defp collection_private(%{"first" => first}) do + if is_map(first) and + first["type"] in ["CollectionPage", "OrderedCollectionPage"] do {:ok, false} else with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <- - Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do + Fetcher.fetch_and_contain_remote_object_from_id(first) do {:ok, false} else {:error, {:ok, %{status: code}}} when code in [401, 403] -> @@ -1254,6 +1254,8 @@ defp collection_private(data) do end end + defp collection_private(_data), do: {:ok, true} + def user_data_from_user_object(data) do with {:ok, data} <- MRF.filter(data), {:ok, data} <- object_to_user_data(data) do From 1915b23e72c5cbed5c794f66eb0a727e719d3c57 Mon Sep 17 00:00:00 2001 From: kPherox Date: Sat, 30 Nov 2019 15:24:08 +0900 Subject: [PATCH 42/70] test missing first field --- .../users_mock/friendica_followers.json | 19 +++++++++++++++++++ .../users_mock/friendica_following.json | 19 +++++++++++++++++++ test/support/http_request_mock.ex | 16 ++++++++++++++++ test/web/activity_pub/activity_pub_test.exs | 15 +++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 test/fixtures/users_mock/friendica_followers.json create mode 100644 test/fixtures/users_mock/friendica_following.json diff --git a/test/fixtures/users_mock/friendica_followers.json b/test/fixtures/users_mock/friendica_followers.json new file mode 100644 index 000000000..7b86b5fe2 --- /dev/null +++ b/test/fixtures/users_mock/friendica_followers.json @@ -0,0 +1,19 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "vcard": "http://www.w3.org/2006/vcard/ns#", + "dfrn": "http://purl.org/macgirvin/dfrn/1.0/", + "diaspora": "https://diasporafoundation.org/ns/", + "litepub": "http://litepub.social/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "directMessage": "litepub:directMessage" + } + ], + "id": "http://localhost:8080/followers/fuser3", + "type": "OrderedCollection", + "totalItems": 296 +} diff --git a/test/fixtures/users_mock/friendica_following.json b/test/fixtures/users_mock/friendica_following.json new file mode 100644 index 000000000..7c526befc --- /dev/null +++ b/test/fixtures/users_mock/friendica_following.json @@ -0,0 +1,19 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "vcard": "http://www.w3.org/2006/vcard/ns#", + "dfrn": "http://purl.org/macgirvin/dfrn/1.0/", + "diaspora": "https://diasporafoundation.org/ns/", + "litepub": "http://litepub.social/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "directMessage": "litepub:directMessage" + } + ], + "id": "http://localhost:8080/following/fuser3", + "type": "OrderedCollection", + "totalItems": 32 +} diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 965335e96..e3a621f49 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1035,6 +1035,22 @@ def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do }} end + def get("http://localhost:8080/followers/fuser3", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/users_mock/friendica_followers.json") + }} + end + + def get("http://localhost:8080/following/fuser3", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/users_mock/friendica_following.json") + }} + end + def get("http://localhost:4001/users/fuser2/followers", _, _, _) do {:ok, %Tesla.Env{ diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index d437ad456..30f5dde0c 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1554,5 +1554,20 @@ test "detects hidden follows" do assert follow_info.hide_followers == false assert follow_info.hide_follows == true end + + test "detects hidden follows/followers for friendica" do + user = + insert(:user, + local: false, + follower_address: "http://localhost:8080/followers/fuser3", + following_address: "http://localhost:8080/following/fuser3" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + assert follow_info.hide_followers == true + assert follow_info.follower_count == 296 + assert follow_info.following_count == 32 + assert follow_info.hide_follows == true + end 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 43/70] 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 356a936ecbddf6bbdfada2ef70264b99e9d9723c Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 2 Dec 2019 18:11:45 +0700 Subject: [PATCH 44/70] Fix `mix pleroma.user list` task --- lib/mix/tasks/pleroma/user.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index bc8eacda8..0adb78fe3 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -373,9 +373,9 @@ def run(["list"]) do users |> Enum.each(fn user -> shell_info( - "#{user.nickname} moderator: #{user.info.is_moderator}, admin: #{user.info.is_admin}, locked: #{ - user.info.locked - }, deactivated: #{user.info.deactivated}" + "#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{ + user.locked + }, deactivated: #{user.deactivated}" ) end) end) From 624e720aa45fd54575d17a2b5e77c67deef6eb86 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 3 Dec 2019 22:13:38 +0700 Subject: [PATCH 45/70] Add `with_move` query param to the notifications API --- lib/pleroma/notification.ex | 9 +++++++ lib/pleroma/web/mastodon_api/mastodon_api.ex | 3 ++- test/notification_test.exs | 20 ++++++++------ test/web/activity_pub/activity_pub_test.exs | 4 +-- .../notification_controller_test.exs | 26 +++++++++++++++++++ .../views/notification_view_test.exs | 8 +++--- 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index f37e7ec67..6b6e1c2e1 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -57,6 +57,7 @@ def for_user_query(user, opts \\ []) do |> exclude_muted(user, opts) |> exclude_blocked(user) |> exclude_visibility(opts) + |> exclude_move(opts) end defp exclude_blocked(query, user) do @@ -81,6 +82,14 @@ defp exclude_muted(query, user, _opts) do |> where([n, a, o, tm], is_nil(tm.user_id)) end + defp exclude_move(query, %{with_move: true}) do + query + end + + defp exclude_move(query, _opts) do + where(query, [n, a], fragment("?->>'type' != 'Move'", a.data)) + end + @valid_visibilities ~w[direct unlisted public private] defp exclude_visibility(query, %{exclude_visibilities: visibility}) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index d875a5788..3e7dc4617 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -73,7 +73,8 @@ defp cast_params(params) do exclude_types: {:array, :string}, exclude_visibilities: {:array, :string}, reblogs: :boolean, - with_muted: :boolean + with_muted: :boolean, + with_move: :boolean } changeset = cast({%{}, param_types}, params, Map.keys(param_types)) diff --git a/test/notification_test.exs b/test/notification_test.exs index dcbffeafe..9fe34d18f 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -643,13 +643,7 @@ test "move activity generates a notification" do 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 [] = Notification.for_user(follower) assert [ %{ @@ -657,7 +651,17 @@ test "move activity generates a notification" do data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} } } - ] = Notification.for_user(other_follower) + ] = Notification.for_user(follower, %{with_move: true}) + + assert [] = Notification.for_user(other_follower) + + assert [ + %{ + activity: %{ + data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} + } + } + ] = Notification.for_user(other_follower, %{with_move: true}) end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 2677b9e36..93eabde85 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1619,10 +1619,10 @@ test "create" do activity = %Activity{activity | object: nil} assert [%Notification{activity: ^activity}] = - Notification.for_user_since(follower, ~N[2019-04-13 11:22:33]) + Notification.for_user(follower, %{with_move: true}) assert [%Notification{activity: ^activity}] = - Notification.for_user_since(follower_move_opted_out, ~N[2019-04-13 11:22:33]) + Notification.for_user(follower_move_opted_out, %{with_move: true}) end test "old user must be in the new user's `also_known_as` list" do diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index fa55a7cf9..349cca682 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -341,6 +341,32 @@ test "see notifications after muting user with notifications and with_muted para assert length(json_response(conn, 200)) == 1 end + test "see move notifications with `with_move` parameter", %{ + conn: conn + } do + old_user = insert(:user) + new_user = insert(:user, also_known_as: [old_user.ap_id]) + follower = insert(:user) + + User.follow(follower, old_user) + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + Pleroma.Tests.ObanHelpers.perform_all() + + conn = + conn + |> assign(:user, follower) + |> get("/api/v1/notifications") + + assert json_response(conn, 200) == [] + + conn = + build_conn() + |> assign(:user, follower) + |> get("/api/v1/notifications", %{"with_move" => "true"}) + + assert length(json_response(conn, 200)) == 1 + end + defp get_notification_id_by_activity(%{id: id}) do Notification |> Repo.get_by(activity_id: id) diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index 80b6d414c..cd36cb538 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -109,22 +109,22 @@ test "Follow notification" do 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]) + old_user = insert(:user) + new_user = insert(:user, also_known_as: [old_user.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) + [notification] = Notification.for_user(follower, %{with_move: true}) 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}), + target: AccountView.render("show.json", %{user: refresh_record(new_user), for: follower}), created_at: Utils.to_masto_date(notification.inserted_at) } From 05fb8d0084553a3ed7d1f10212f49771ce05570f Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 3 Dec 2019 22:31:12 +0700 Subject: [PATCH 46/70] Fix Pleroma.FollowingRelationship.move_following/2 --- lib/pleroma/following_relationship.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index a03c9bd30..0b0219b82 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -121,8 +121,12 @@ def move_following(origin, target) do Pleroma.Web.CommonAPI.follow(following_relationship.follower, target) end) |> case do - [] -> :ok - _ -> move_following(origin, target) + [] -> + User.update_follower_count(origin) + :ok + + _ -> + move_following(origin, target) end end end From aa97fe8f175c3618a04bb45e6aa7c37e26259577 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 4 Dec 2019 12:48:34 +0100 Subject: [PATCH 47/70] ChatChannel: Ignore messages that are too long. --- config/config.exs | 1 + lib/pleroma/application.ex | 2 -- lib/pleroma/web/chat_channel.ex | 2 +- test/support/channel_case.ex | 1 + test/web/chat_channel_test.exs | 37 +++++++++++++++++++++++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 test/web/chat_channel_test.exs diff --git a/config/config.exs b/config/config.exs index b60ffef7d..4624bded2 100644 --- a/config/config.exs +++ b/config/config.exs @@ -225,6 +225,7 @@ notify_email: "noreply@example.com", description: "A Pleroma instance, an alternative fediverse server", limit: 5_000, + chat_limit: 5_000, remote_limit: 100_000, upload_limit: 16_000_000, avatar_upload_limit: 2_000_000, diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9dbd1e26b..57462740c 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -147,8 +147,6 @@ defp oauth_cleanup_child(true), defp oauth_cleanup_child(_), do: [] - defp chat_child(:test, _), do: [] - defp chat_child(_env, true) do [Pleroma.Web.ChatChannel.ChatChannelState] end diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex index 08841a3e8..840414933 100644 --- a/lib/pleroma/web/chat_channel.ex +++ b/lib/pleroma/web/chat_channel.ex @@ -20,7 +20,7 @@ def handle_info(:after_join, socket) do def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}} = socket) do text = String.trim(text) - if String.length(text) > 0 do + if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do author = User.get_cached_by_nickname(user_name) author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author) message = ChatChannelState.add_message(%{text: text, author: author}) diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex index 466d8986f..4a4585844 100644 --- a/test/support/channel_case.ex +++ b/test/support/channel_case.ex @@ -23,6 +23,7 @@ defmodule Pleroma.Web.ChannelCase do quote do # Import conveniences for testing with channels use Phoenix.ChannelTest + use Pleroma.Tests.Helpers # The default endpoint for testing @endpoint Pleroma.Web.Endpoint diff --git a/test/web/chat_channel_test.exs b/test/web/chat_channel_test.exs new file mode 100644 index 000000000..68c24a9f9 --- /dev/null +++ b/test/web/chat_channel_test.exs @@ -0,0 +1,37 @@ +defmodule Pleroma.Web.ChatChannelTest do + use Pleroma.Web.ChannelCase + alias Pleroma.Web.ChatChannel + alias Pleroma.Web.UserSocket + + import Pleroma.Factory + + setup do + user = insert(:user) + + {:ok, _, socket} = + socket(UserSocket, "", %{user_name: user.nickname}) + |> subscribe_and_join(ChatChannel, "chat:public") + + {:ok, socket: socket} + end + + test "it broadcasts a message", %{socket: socket} do + push(socket, "new_msg", %{"text" => "why is tenshi eating a corndog so cute?"}) + assert_broadcast("new_msg", %{text: "why is tenshi eating a corndog so cute?"}) + end + + describe "message lengths" do + clear_config([:instance, :chat_limit]) + + test "it ignores messages of length zero", %{socket: socket} do + push(socket, "new_msg", %{"text" => ""}) + refute_broadcast("new_msg", %{text: ""}) + end + + test "it ignores messages above a certain length", %{socket: socket} do + Pleroma.Config.put([:instance, :chat_limit], 2) + push(socket, "new_msg", %{"text" => "123"}) + refute_broadcast("new_msg", %{text: "123"}) + end + end +end From 21353a8ee900fc5b48d7de5168f0b81e7b15dc97 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 4 Dec 2019 12:49:46 +0100 Subject: [PATCH 48/70] Changelog: Add information about chat limit. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a06ea211e..b162026e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added +- `:chat_limit` option to limit chat characters. - Refreshing poll results for remote polls - Authentication: Added rate limit for password-authorized actions / login existence checks - Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app. From 9487995e8b2172bd720d17d639d5d782d38a45a2 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 4 Dec 2019 12:51:06 +0100 Subject: [PATCH 49/70] Cheat Sheet: Addg chat_limit information. --- docs/configuration/cheatsheet.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index dc2f55229..ef2711e3c 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -12,6 +12,7 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic * `notify_email`: Email used for notifications. * `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``. * `limit`: Posts character limit (CW/Subject included in the counter). +* `chat_limit`: Character limit of the instance chat messages. * `remote_limit`: Hard character limit beyond which remote posts will be dropped. * `upload_limit`: File size limit of uploads (except for avatar, background, banner). * `avatar_upload_limit`: File size limit of user’s profile avatars. From ed7f850ad2c9271268098959d8a86b24b187fa22 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Wed, 4 Dec 2019 16:22:01 +0300 Subject: [PATCH 50/70] 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 = %{ From d3bce89edddf72e2eb78e7c84a93c5bd4b6c8ff8 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 4 Dec 2019 19:41:14 +0300 Subject: [PATCH 51/70] [#1335] Merge issue fix. --- lib/pleroma/user.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index a68524377..b7f50e5ac 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -169,8 +169,6 @@ defmodule Pleroma.User do has_many(incoming_relation_source, through: [incoming_relation, :source]) end - field(:info, :map, default: %{}) - # `:blocks` is deprecated (replaced with `blocked_users` relation) field(:blocks, {:array, :string}, default: []) # `:mutes` is deprecated (replaced with `muted_users` relation) From 9f99640cfc81f28c0b6cdb5f6ef065b3e46f5f23 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 5 Dec 2019 02:50:38 +0300 Subject: [PATCH 52/70] ActivityPub: Fix deletes being exempt from MRF Closes #1461 --- CHANGELOG.md | 1 + lib/pleroma/activity.ex | 5 +++-- lib/pleroma/activity/queries.ex | 8 ++++++++ lib/pleroma/object.ex | 2 +- lib/pleroma/web/activity_pub/activity_pub.ex | 5 +++-- .../activity_pub/activity_pub_controller_test.exs | 2 +- test/web/activity_pub/activity_pub_test.exs | 15 +++++++++++++++ 7 files changed, 32 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a06ea211e..3ec34f452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Report emails now include functional links to profiles of remote user accounts - Not being able to log in to some third-party apps when logged in to MastoFE +- MRF: `Delete` activities being exempt from MRF policies
API Changes diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index f180c1e33..480b261cf 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -241,9 +241,10 @@ def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"]) def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id) def normalize(_), do: nil - def delete_by_ap_id(id) when is_binary(id) do + def delete_all_by_object_ap_id(id) when is_binary(id) do id |> Queries.by_object_id() + |> Queries.exclude_type("Delete") |> select([u], u) |> Repo.delete_all() |> elem(1) @@ -255,7 +256,7 @@ def delete_by_ap_id(id) when is_binary(id) do |> purge_web_resp_cache() end - def delete_by_ap_id(_), do: nil + def delete_all_by_object_ap_id(_), do: nil defp purge_web_resp_cache(%Activity{} = activity) do %{path: path} = URI.parse(activity.data["id"]) diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex index 949f010a8..26bc1099d 100644 --- a/lib/pleroma/activity/queries.ex +++ b/lib/pleroma/activity/queries.ex @@ -64,4 +64,12 @@ def by_type(query \\ Activity, activity_type) do where: fragment("(?)->>'type' = ?", activity.data, ^activity_type) ) end + + @spec exclude_type(query, String.t()) :: query + def exclude_type(query \\ Activity, activity_type) do + from( + activity in query, + where: fragment("(?)->>'type' != ?", activity.data, ^activity_type) + ) + end end diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index b4ed3a9b2..ff0e59241 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -147,7 +147,7 @@ def swap_object_with_tombstone(object) do def delete(%Object{data: %{"id" => id}} = object) do with {:ok, _obj} = swap_object_with_tombstone(object), - deleted_activity = Activity.delete_by_ap_id(id), + deleted_activity = Activity.delete_all_by_object_ap_id(id), {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do {:ok, object, deleted_activity} diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index d6a425d8b..c8160b0cf 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -456,17 +456,18 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ [ user = User.get_cached_by_ap_id(actor) to = (object.data["to"] || []) ++ (object.data["cc"] || []) - with {:ok, object, activity} <- Object.delete(object), + with create_activity <- Activity.get_create_by_object_ap_id(id), data <- %{ "type" => "Delete", "actor" => actor, "object" => id, "to" => to, - "deleted_activity_id" => activity && activity.id + "deleted_activity_id" => create_activity && create_activity.id } |> maybe_put("id", activity_id), {:ok, activity} <- insert(data, local, false), + {:ok, object, _create_activity} <- Object.delete(object), stream_out_participations(object, user), _ <- decrease_replies_count_if_reply(object), {:ok, _actor} <- decrease_note_count_if_public(user, object), diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 1aa73d75c..ba2ce1dd9 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -298,7 +298,7 @@ test "cached purged after activity deletion", %{conn: conn} do assert json_response(conn1, :ok) assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) - Activity.delete_by_ap_id(activity.object.data["id"]) + Activity.delete_all_by_object_ap_id(activity.object.data["id"]) conn2 = conn diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 2677b9e36..23dffb150 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1256,6 +1256,21 @@ test "decreases reply count" do assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) assert object.data["repliesCount"] == 0 end + + test "it passes delete activity through MRF before deleting the object" do + rewrite_policy = Pleroma.Config.get([:instance, :rewrite_policy]) + Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy) + + on_exit(fn -> Pleroma.Config.put([:instance, :rewrite_policy], rewrite_policy) end) + + note = insert(:note_activity) + object = Object.normalize(note) + + {:error, {:reject, _}} = ActivityPub.delete(object) + + assert Activity.get_by_id(note.id) + assert Repo.get(Object, object.id).data["type"] == object.data["type"] + end end describe "timeline post-processing" do From af42c00cfffb2cd8e93857cd1cf2901113c45bd2 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 6 Dec 2019 00:25:44 +0300 Subject: [PATCH 53/70] [#1427] Reworked admin scopes support. Requalified users.is_admin flag as legacy accessor to admin actions in case token lacks admin scope(s). --- CHANGELOG.md | 1 + config/config.exs | 5 +++- config/description.exs | 9 ++++++ lib/mix/tasks/pleroma/user.ex | 9 ++---- lib/pleroma/config.ex | 7 +++++ lib/pleroma/plugs/user_is_admin_plug.ex | 28 ++++++++++++++++--- lib/pleroma/user.ex | 28 ++++++++++++++----- .../web/admin_api/admin_api_controller.ex | 18 ++++++------ lib/pleroma/web/oauth/scopes.ex | 2 +- .../controllers/emoji_api_controller.ex | 2 +- .../admin_api/admin_api_controller_test.exs | 3 +- 11 files changed, 82 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd835a3d..dac61c174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mix task to re-count statuses for all users (`mix pleroma.count_statuses`) - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers. +- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
API Changes diff --git a/config/config.exs b/config/config.exs index bf2b3f6e2..64397484e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -560,7 +560,10 @@ base_path: "/oauth", providers: ueberauth_providers -config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies +config :pleroma, + :auth, + enforce_oauth_admin_scope_usage: false, + oauth_consumer_strategies: oauth_consumer_strategies config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false diff --git a/config/description.exs b/config/description.exs index 70e963399..45e4b43f1 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2094,6 +2094,15 @@ type: :group, description: "Authentication / authorization settings", children: [ + %{ + key: :enforce_oauth_admin_scope_usage, + type: :boolean, + description: + "OAuth admin scope requirement toggle. " <> + "If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <> + "(client app must support admin scopes). If `false` and token doesn't have admin scope(s)," <> + "`is_admin` user flag grants access to admin-specific actions." + }, %{ key: :auth_template, type: :string, diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 4e3b80db3..8c4739221 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -8,7 +8,6 @@ defmodule Mix.Tasks.Pleroma.User do alias Ecto.Changeset alias Pleroma.User alias Pleroma.UserInviteToken - alias Pleroma.Web.OAuth @shortdoc "Manages Pleroma users" @moduledoc File.read!("docs/administration/CLI_tasks/user.md") @@ -354,8 +353,7 @@ def run(["sign_out", nickname]) do start_pleroma() with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do - OAuth.Token.delete_user_tokens(user) - OAuth.Authorization.delete_user_authorizations(user) + User.global_sign_out(user) shell_info("#{nickname} signed out from all apps.") else @@ -375,10 +373,7 @@ defp set_moderator(user, value) do end defp set_admin(user, value) do - {:ok, user} = - user - |> Changeset.change(%{is_admin: value}) - |> User.update_and_set_cache() + {:ok, user} = User.admin_api_update(user, %{is_admin: value}) shell_info("Admin status of #{user.nickname}: #{user.is_admin}") user diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex index fcc039710..cadab2f15 100644 --- a/lib/pleroma/config.ex +++ b/lib/pleroma/config.ex @@ -65,4 +65,11 @@ def delete(key) do def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], []) def oauth_consumer_enabled?, do: oauth_consumer_strategies() != [] + + def enforce_oauth_admin_scope_usage?, do: !!get([:auth, :enforce_oauth_admin_scope_usage]) + + def oauth_admin_scopes(scope) do + ["admin:#{scope}"] ++ + if enforce_oauth_admin_scope_usage?(), do: [], else: [scope] + end end diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index ee808f31f..8814556f1 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -5,19 +5,39 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do import Pleroma.Web.TranslationHelpers import Plug.Conn + alias Pleroma.User + alias Pleroma.Web.OAuth def init(options) do options end - def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do - conn + def call(%Plug.Conn{assigns: %{token: %OAuth.Token{scopes: oauth_scopes} = _token}} = conn, _) do + if OAuth.Scopes.contains_admin_scopes?(oauth_scopes) do + # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. + # Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. + conn + else + fail(conn) + end + end + + unless Pleroma.Config.enforce_oauth_admin_scope_usage?() do + # To do: once AdminFE makes use of "admin" scope, disable the following func definition + # (fail on no admin scope(s) in token even if `is_admin` is true) + def call(%Plug.Conn{assigns: %{user: %User{is_admin: true}}} = conn, _) do + conn + end end def call(conn, _) do + fail(conn) + end + + defp fail(conn) do conn - |> render_error(:forbidden, "User is not admin.") - |> halt + |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") + |> halt() end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index fcb1d5143..d05dccd3d 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1732,13 +1732,27 @@ defp truncate_field(%{"name" => name, "value" => value}) do end def admin_api_update(user, params) do - user - |> cast(params, [ - :is_moderator, - :is_admin, - :show_role - ]) - |> update_and_set_cache() + changeset = + cast(user, params, [ + :is_moderator, + :is_admin, + :show_role + ]) + + with {:ok, updated_user} <- update_and_set_cache(changeset) do + if user.is_admin && !updated_user.is_admin do + # Tokens & authorizations containing any admin scopes must be revoked (revoking all) + global_sign_out(user) + end + + {:ok, updated_user} + end + end + + @doc "Signs user out of all applications" + def global_sign_out(user) do + OAuth.Authorization.delete_user_authorizations(user) + OAuth.Token.delete_user_tokens(user) end def mascot_update(user, url) do diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 01cd12c96..f9ace00d7 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -30,13 +30,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: ["admin:read:accounts", "read:accounts"]} + %{scopes: Pleroma.Config.oauth_admin_scopes("read:accounts")} when action in [:list_users, :user_show, :right_get, :invites] ) plug( OAuthScopesPlug, - %{scopes: ["admin:write:accounts", "write:accounts"]} + %{scopes: Pleroma.Config.oauth_admin_scopes("write:accounts")} when action in [ :get_invite_token, :revoke_invite, @@ -58,35 +58,37 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: ["admin:read:reports", "read:reports"]} when action in [:list_reports, :report_show] + %{scopes: Pleroma.Config.oauth_admin_scopes("read:reports")} + when action in [:list_reports, :report_show] ) plug( OAuthScopesPlug, - %{scopes: ["admin:reports", "write:reports"]} + %{scopes: Pleroma.Config.oauth_admin_scopes("write:reports")} when action in [:report_update_state, :report_respond] ) plug( OAuthScopesPlug, - %{scopes: ["admin:read:statuses", "read:statuses"]} when action == :list_user_statuses + %{scopes: Pleroma.Config.oauth_admin_scopes("read:statuses")} + when action == :list_user_statuses ) plug( OAuthScopesPlug, - %{scopes: ["admin:write:statuses", "write:statuses"]} + %{scopes: Pleroma.Config.oauth_admin_scopes("write:statuses")} when action in [:status_update, :status_delete] ) plug( OAuthScopesPlug, - %{scopes: ["admin:read", "read"]} + %{scopes: Pleroma.Config.oauth_admin_scopes("read")} when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log] ) plug( OAuthScopesPlug, - %{scopes: ["admin:write", "write"]} + %{scopes: Pleroma.Config.oauth_admin_scopes("write")} when action in [:relay_follow, :relay_unfollow, :config_update] ) diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 0c8796ecb..5e04652c2 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -83,7 +83,7 @@ defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do end end - defp contains_admin_scopes?(scopes) do + def contains_admin_scopes?(scopes) do scopes |> OAuthScopesPlug.filter_descendants(["admin"]) |> Enum.any?() diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 1e8c62e3a..6f286032e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do plug( OAuthScopesPlug, - %{scopes: ["admin:write", "write"]} + %{scopes: Pleroma.Config.oauth_admin_scopes("write")} when action in [ :create, :delete, diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 3a4c4d65c..fd179e8c2 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1537,7 +1537,8 @@ test "returns 403 when requested by a non-admin" do |> assign(:user, user) |> get("/api/pleroma/admin/reports") - assert json_response(conn, :forbidden) == %{"error" => "User is not admin."} + assert json_response(conn, :forbidden) == + %{"error" => "User is not an admin or OAuth admin scope is not granted."} end test "returns 403 when requested by anonymous" do From e8cee4d9a0ea13db8d087e42eb17939bb8b11f0b Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 6 Dec 2019 14:25:13 +0100 Subject: [PATCH 54/70] ActivityPub: For user timelines, respects blocks. Unless the timeline belongs to a blocked user. --- lib/pleroma/web/activity_pub/activity_pub.ex | 9 ++++ .../controllers/account_controller.ex | 6 ++- .../controllers/account_controller_test.exs | 44 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index b07a94701..c5bc1ef0d 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -748,6 +748,15 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do |> Map.put("whole_db", true) |> Map.put("pinned_activity_ids", user.pinned_activities) + params = + if User.blocks?(reading_user, user) do + params + else + params + |> Map.put("blocking_user", reading_user) + |> Map.put("muting_user", reading_user) + end + recipients = user_activities_recipients(%{ "godmode" => params["godmode"], diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index d71a14434..d19029cb5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -249,7 +249,11 @@ def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do @doc "GET /api/v1/accounts/:id/statuses" def statuses(%{assigns: %{user: reading_user}} = conn, params) do with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do - params = Map.put(params, "tag", params["tagged"]) + params = + params + |> Map.put("tag", params["tagged"]) + |> Map.delete("godmode") + activities = ActivityPub.fetch_user_activities(user, reading_user, params) conn diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 444693404..fa08ae4df 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -144,6 +144,50 @@ test "returns 404 for internal.fetch actor", %{conn: conn} do end describe "user timelines" do + test "respects blocks", %{conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + user_three = insert(:user) + + User.block(user_one, user_two) + + {:ok, activity} = CommonAPI.post(user_two, %{"status" => "User one sux0rz"}) + {:ok, repeat, _} = CommonAPI.repeat(activity.id, user_three) + + resp = + conn + |> get("/api/v1/accounts/#{user_two.id}/statuses") + + assert [%{"id" => id}] = json_response(resp, 200) + assert id == activity.id + + # Even a blocked user will deliver the full user timeline, there would be + # no point in looking at a blocked users timeline otherwise + resp = + conn + |> assign(:user, user_one) + |> get("/api/v1/accounts/#{user_two.id}/statuses") + + assert [%{"id" => id}] = json_response(resp, 200) + assert id == activity.id + + resp = + conn + |> get("/api/v1/accounts/#{user_three.id}/statuses") + + assert [%{"id" => id}] = json_response(resp, 200) + assert id == repeat.id + + # When viewing a third user's timeline, the blocked users will NOT be + # shown. + resp = + conn + |> assign(:user, user_one) + |> get("/api/v1/accounts/#{user_three.id}/statuses") + + assert [] = json_response(resp, 200) + end + test "gets a users statuses", %{conn: conn} do user_one = insert(:user) user_two = insert(:user) From 93a80ee9155bf5257aa92afaca2fe017f28aabfa Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 6 Dec 2019 16:56:23 +0300 Subject: [PATCH 55/70] [#1427] Bugfix for `enforce_oauth_admin_scope_usage`. Admin API documentation entry. --- docs/API/admin_api.md | 7 +++++++ lib/pleroma/plugs/user_is_admin_plug.ex | 17 ++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 2cac317de..b19793150 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -2,6 +2,13 @@ Authentication is required and the user must be an admin. +Configuration options: + +* `[:auth, :enforce_oauth_admin_scope_usage]` — OAuth admin scope requirement toggle. + If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes). + If `false` and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions. + Note that client app needs to explicitly support admin scopes and request them when obtaining auth token. + ## `GET /api/pleroma/admin/users` ### List users diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index 8814556f1..814029dce 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -6,13 +6,20 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do import Pleroma.Web.TranslationHelpers import Plug.Conn - alias Pleroma.User alias Pleroma.Web.OAuth def init(options) do options end + unless Pleroma.Config.enforce_oauth_admin_scope_usage?() do + # To do: once AdminFE makes use of "admin" scope, disable the following func definition + # (fail on no admin scope(s) in token even if `is_admin` is true) + def call(%Plug.Conn{assigns: %{user: %Pleroma.User{is_admin: true}}} = conn, _) do + conn + end + end + def call(%Plug.Conn{assigns: %{token: %OAuth.Token{scopes: oauth_scopes} = _token}} = conn, _) do if OAuth.Scopes.contains_admin_scopes?(oauth_scopes) do # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. @@ -23,14 +30,6 @@ def call(%Plug.Conn{assigns: %{token: %OAuth.Token{scopes: oauth_scopes} = _toke end end - unless Pleroma.Config.enforce_oauth_admin_scope_usage?() do - # To do: once AdminFE makes use of "admin" scope, disable the following func definition - # (fail on no admin scope(s) in token even if `is_admin` is true) - def call(%Plug.Conn{assigns: %{user: %User{is_admin: true}}} = conn, _) do - conn - end - end - def call(conn, _) do fail(conn) end From 40e1817f707c3c2ef253009c7363cd81b11322a6 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 6 Dec 2019 20:33:47 +0300 Subject: [PATCH 56/70] [#1427] Fixes / improvements of admin scopes support. Added tests. --- lib/pleroma/plugs/oauth_scopes_plug.ex | 9 ++ lib/pleroma/plugs/user_is_admin_plug.ex | 42 +++---- .../web/admin_api/admin_api_controller.ex | 16 +-- .../controllers/emoji_api_controller.ex | 2 +- test/plugs/user_is_admin_plug_test.exs | 104 ++++++++++++++---- .../admin_api/admin_api_controller_test.exs | 47 +++++++- 6 files changed, 162 insertions(+), 58 deletions(-) diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index a3278dbef..3201fb399 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do import Plug.Conn import Pleroma.Web.Gettext + alias Pleroma.Config alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug @behaviour Plug @@ -15,6 +16,14 @@ def init(%{scopes: _} = options), do: options def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do op = options[:op] || :| token = assigns[:token] + + scopes = + if options[:admin] do + Config.oauth_admin_scopes(scopes) + else + scopes + end + matched_scopes = token && filter_descendants(scopes, token.scopes) cond do diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index 814029dce..4a0e43b00 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -12,31 +12,23 @@ def init(options) do options end - unless Pleroma.Config.enforce_oauth_admin_scope_usage?() do - # To do: once AdminFE makes use of "admin" scope, disable the following func definition - # (fail on no admin scope(s) in token even if `is_admin` is true) - def call(%Plug.Conn{assigns: %{user: %Pleroma.User{is_admin: true}}} = conn, _) do - conn + def call(%Plug.Conn{assigns: assigns} = conn, _) do + token = assigns[:token] + user = assigns[:user] + + cond do + token && OAuth.Scopes.contains_admin_scopes?(token.scopes) -> + # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. + # Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. + conn + + user && user.is_admin && !Pleroma.Config.enforce_oauth_admin_scope_usage?() -> + conn + + true -> + conn + |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") + |> halt() end end - - def call(%Plug.Conn{assigns: %{token: %OAuth.Token{scopes: oauth_scopes} = _token}} = conn, _) do - if OAuth.Scopes.contains_admin_scopes?(oauth_scopes) do - # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. - # Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. - conn - else - fail(conn) - end - end - - def call(conn, _) do - fail(conn) - end - - defp fail(conn) do - conn - |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") - |> halt() - end end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 34d147107..0a63f3fe6 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -30,13 +30,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: Pleroma.Config.oauth_admin_scopes("read:accounts")} + %{scopes: ["read:accounts"], admin: true} when action in [:list_users, :user_show, :right_get, :invites] ) plug( OAuthScopesPlug, - %{scopes: Pleroma.Config.oauth_admin_scopes("write:accounts")} + %{scopes: ["write:accounts"], admin: true} when action in [ :get_invite_token, :revoke_invite, @@ -58,37 +58,37 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: Pleroma.Config.oauth_admin_scopes("read:reports")} + %{scopes: ["read:reports"], admin: true} when action in [:list_reports, :report_show] ) plug( OAuthScopesPlug, - %{scopes: Pleroma.Config.oauth_admin_scopes("write:reports")} + %{scopes: ["write:reports"], admin: true} when action in [:report_update_state, :report_respond] ) plug( OAuthScopesPlug, - %{scopes: Pleroma.Config.oauth_admin_scopes("read:statuses")} + %{scopes: ["read:statuses"], admin: true} when action == :list_user_statuses ) plug( OAuthScopesPlug, - %{scopes: Pleroma.Config.oauth_admin_scopes("write:statuses")} + %{scopes: ["write:statuses"], admin: true} when action in [:status_update, :status_delete] ) plug( OAuthScopesPlug, - %{scopes: Pleroma.Config.oauth_admin_scopes("read")} + %{scopes: ["read"], admin: true} when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log] ) plug( OAuthScopesPlug, - %{scopes: Pleroma.Config.oauth_admin_scopes("write")} + %{scopes: ["write"], admin: true} when action in [:relay_follow, :relay_unfollow, :config_update] ) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 6f286032e..69dfa92e3 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do plug( OAuthScopesPlug, - %{scopes: Pleroma.Config.oauth_admin_scopes("write")} + %{scopes: ["write"], admin: true} when action in [ :create, :delete, diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs index 136dcc54e..154c9b195 100644 --- a/test/plugs/user_is_admin_plug_test.exs +++ b/test/plugs/user_is_admin_plug_test.exs @@ -8,36 +8,96 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do alias Pleroma.Plugs.UserIsAdminPlug import Pleroma.Factory - test "accepts a user that is admin" do - user = insert(:user, is_admin: true) + describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + end - conn = - build_conn() - |> assign(:user, user) + test "accepts a user that is admin" do + user = insert(:user, is_admin: true) - ret_conn = - conn - |> UserIsAdminPlug.call(%{}) + conn = assign(build_conn(), :user, user) - assert conn == ret_conn + ret_conn = UserIsAdminPlug.call(conn, %{}) + + assert conn == ret_conn + end + + test "denies a user that isn't admin" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + + test "denies when a user isn't set" do + conn = UserIsAdminPlug.call(build_conn(), %{}) + + assert conn.status == 403 + end end - test "denies a user that isn't admin" do - user = insert(:user) + describe "with [:auth, :enforce_oauth_admin_scope_usage]," do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) + end - conn = - build_conn() - |> assign(:user, user) - |> UserIsAdminPlug.call(%{}) + setup do + admin_user = insert(:user, is_admin: true) + non_admin_user = insert(:user, is_admin: false) + blank_user = nil - assert conn.status == 403 - end + {:ok, %{users: [admin_user, non_admin_user, blank_user]}} + end - test "denies when a user isn't set" do - conn = - build_conn() - |> UserIsAdminPlug.call(%{}) + # Note: in real-life scenarios only users with is_admin flag can possess admin-scoped tokens; + # however, the following test stresses out that is_admin flag is not checked if we got token + test "if token has any of admin scopes, accepts users regardless of is_admin flag", + %{users: users} do + for user <- users do + token = insert(:oauth_token, user: user, scopes: ["admin:something"]) - assert conn.status == 403 + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + |> UserIsAdminPlug.call(%{}) + + ret_conn = UserIsAdminPlug.call(conn, %{}) + + assert conn == ret_conn + end + end + + test "if token lacks admin scopes, denies users regardless of is_admin flag", + %{users: users} do + for user <- users do + token = insert(:oauth_token, user: user) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + end + + test "if token is missing, denies users regardless of is_admin flag", %{users: users} do + for user <- users do + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, nil) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + end end end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index d0131fd90..2fc23ad6c 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -24,6 +24,49 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do :ok end + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + end + + describe "with [:auth, :enforce_oauth_admin_scope_usage]," do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) + end + + test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope" do + user = insert(:user) + admin = insert(:user, is_admin: true) + + good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) + good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) + good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"]) + + bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) + bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) + bad_token3 = nil + + for good_token <- [good_token1, good_token2, good_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, good_token) + |> get("/api/pleroma/admin/users/#{user.nickname}") + + assert json_response(conn, 200) + end + + for bad_token <- [bad_token1, bad_token2, bad_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, bad_token) + |> get("/api/pleroma/admin/users/#{user.nickname}") + + assert json_response(conn, :forbidden) + end + end + end + describe "DELETE /api/pleroma/admin/users" do test "single user" do admin = insert(:user, is_admin: true) @@ -97,7 +140,7 @@ test "Create" do assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] end - test "Cannot create user with exisiting email" do + test "Cannot create user with existing email" do admin = insert(:user, is_admin: true) user = insert(:user) @@ -128,7 +171,7 @@ test "Cannot create user with exisiting email" do ] end - test "Cannot create user with exisiting nickname" do + test "Cannot create user with existing nickname" do admin = insert(:user, is_admin: true) user = insert(:user) From 1770602747ae95d95d12c5601f99ced8699e8947 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 7 Dec 2019 17:49:53 +0300 Subject: [PATCH 57/70] [#1427] Extra check that admin OAuth scope is used by admin. Adjusted tests. --- lib/pleroma/plugs/user_is_admin_plug.ex | 24 ++++++--- lib/pleroma/user.ex | 3 +- test/plugs/user_is_admin_plug_test.exs | 52 +++++++++++++------ .../admin_api/admin_api_controller_test.exs | 15 +++++- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index 4a0e43b00..582fb1f92 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -6,29 +6,37 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do import Pleroma.Web.TranslationHelpers import Plug.Conn + alias Pleroma.User alias Pleroma.Web.OAuth def init(options) do options end - def call(%Plug.Conn{assigns: assigns} = conn, _) do + def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do token = assigns[:token] - user = assigns[:user] cond do + not Pleroma.Config.enforce_oauth_admin_scope_usage?() -> + conn + token && OAuth.Scopes.contains_admin_scopes?(token.scopes) -> # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. # Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. conn - user && user.is_admin && !Pleroma.Config.enforce_oauth_admin_scope_usage?() -> - conn - true -> - conn - |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") - |> halt() + fail(conn) end end + + def call(conn, _) do + fail(conn) + end + + defp fail(conn) do + conn + |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") + |> halt() + end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 7b8222ce1..1006b5bf9 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1736,7 +1736,8 @@ def admin_api_update(user, params) do with {:ok, updated_user} <- update_and_set_cache(changeset) do if user.is_admin && !updated_user.is_admin do - # Tokens & authorizations containing any admin scopes must be revoked (revoking all) + # Tokens & authorizations containing any admin scopes must be revoked (revoking all). + # This is an extra safety measure (tokens' admin scopes won't be accepted for non-admins). global_sign_out(user) end diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs index 154c9b195..bc6fcd73c 100644 --- a/test/plugs/user_is_admin_plug_test.exs +++ b/test/plugs/user_is_admin_plug_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) end - test "accepts a user that is admin" do + test "accepts a user that is an admin" do user = insert(:user, is_admin: true) conn = assign(build_conn(), :user, user) @@ -23,7 +23,7 @@ test "accepts a user that is admin" do assert conn == ret_conn end - test "denies a user that isn't admin" do + test "denies a user that isn't an admin" do user = insert(:user) conn = @@ -54,23 +54,43 @@ test "denies when a user isn't set" do {:ok, %{users: [admin_user, non_admin_user, blank_user]}} end - # Note: in real-life scenarios only users with is_admin flag can possess admin-scoped tokens; - # however, the following test stresses out that is_admin flag is not checked if we got token - test "if token has any of admin scopes, accepts users regardless of is_admin flag", - %{users: users} do - for user <- users do - token = insert(:oauth_token, user: user, scopes: ["admin:something"]) + test "if token has any of admin scopes, accepts a user that is an admin", %{conn: conn} do + user = insert(:user, is_admin: true) + token = insert(:oauth_token, user: user, scopes: ["admin:something"]) - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, token) - |> UserIsAdminPlug.call(%{}) + conn = + conn + |> assign(:user, user) + |> assign(:token, token) - ret_conn = UserIsAdminPlug.call(conn, %{}) + ret_conn = UserIsAdminPlug.call(conn, %{}) - assert conn == ret_conn - end + assert conn == ret_conn + end + + test "if token has any of admin scopes, denies a user that isn't an admin", %{conn: conn} do + user = insert(:user, is_admin: false) + token = insert(:oauth_token, user: user, scopes: ["admin:something"]) + + conn = + conn + |> assign(:user, user) + |> assign(:token, token) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + + test "if token has any of admin scopes, denies when a user isn't set", %{conn: conn} do + token = insert(:oauth_token, scopes: ["admin:something"]) + + conn = + conn + |> assign(:user, nil) + |> assign(:token, token) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 end test "if token lacks admin scopes, denies users regardless of is_admin flag", diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 2fc23ad6c..bcab63cf0 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -36,6 +36,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope" do user = insert(:user) admin = insert(:user, is_admin: true) + url = "/api/pleroma/admin/users/#{user.nickname}" good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) @@ -50,17 +51,27 @@ test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or bro build_conn() |> assign(:user, admin) |> assign(:token, good_token) - |> get("/api/pleroma/admin/users/#{user.nickname}") + |> get(url) assert json_response(conn, 200) end + for good_token <- [good_token1, good_token2, good_token3] do + conn = + build_conn() + |> assign(:user, nil) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + for bad_token <- [bad_token1, bad_token2, bad_token3] do conn = build_conn() |> assign(:user, admin) |> assign(:token, bad_token) - |> get("/api/pleroma/admin/users/#{user.nickname}") + |> get(url) assert json_response(conn, :forbidden) end From a21340caa192b41051abe3b5905fbcb2fde281d7 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 8 Dec 2019 16:36:22 +0300 Subject: [PATCH 58/70] Fix never matching clause `length/1` is only used with lists. --- lib/pleroma/clippy.ex | 1 + lib/pleroma/html.ex | 1 + lib/pleroma/web/activity_pub/transmogrifier.ex | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/clippy.ex b/lib/pleroma/clippy.ex index bd20952a6..6e6121d4e 100644 --- a/lib/pleroma/clippy.ex +++ b/lib/pleroma/clippy.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Clippy do @moduledoc false + # No software is complete until they have a Clippy implementation. # A ballmer peak _may_ be required to change this module. diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 997e965f0..71c53ce0e 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -163,6 +163,7 @@ defmodule Pleroma.HTML.Scrubber.Default do require FastSanitize.Sanitizer.Meta alias FastSanitize.Sanitizer.Meta + # credo:disable-for-previous-line # No idea how to fix this one… diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index ce95fb6ba..ecba27bef 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -387,7 +387,7 @@ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = def handle_incoming(%{"id" => nil}, _options), do: :error def handle_incoming(%{"id" => ""}, _options), do: :error # length of https:// = 8, should validate better, but good enough for now. - def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8), + def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8, do: :error # TODO: validate those with a Ecto scheme From bd3aa8500c64b769a36a75fefa445668dc8da4a6 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 8 Dec 2019 17:36:44 +0300 Subject: [PATCH 59/70] OAuth token cleanup: Get rid of compile-time configuration --- CHANGELOG.md | 1 + lib/pleroma/web/oauth/token/clean_worker.ex | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d00097748..bb1f48a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Report emails now include functional links to profiles of remote user accounts - Not being able to log in to some third-party apps when logged in to MastoFE - MRF: `Delete` activities being exempt from MRF policies +- OTP releases: Not being able to configure OAuth expired token cleanup interval
API Changes diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex index f639f9c6f..3c9c580d5 100644 --- a/lib/pleroma/web/oauth/token/clean_worker.ex +++ b/lib/pleroma/web/oauth/token/clean_worker.ex @@ -11,11 +11,6 @@ defmodule Pleroma.Web.OAuth.Token.CleanWorker do @ten_seconds 10_000 @one_day 86_400_000 - @interval Pleroma.Config.get( - [:oauth2, :clean_expired_tokens_interval], - @one_day - ) - alias Pleroma.Web.OAuth.Token alias Pleroma.Workers.BackgroundWorker @@ -29,8 +24,9 @@ def init(_) do @doc false def handle_info(:perform, state) do BackgroundWorker.enqueue("clean_expired_tokens", %{}) + interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day) - Process.send_after(self(), :perform, @interval) + Process.send_after(self(), :perform, interval) {:noreply, state} end From d6c89068f3c6765b7a3ef63199725b7833c34c3a Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 8 Dec 2019 19:42:40 +0300 Subject: [PATCH 60/70] HTML: Compile Scrubbers on boot This makes it possible to configure their behavior on OTP releases. --- .formatter.exs | 2 +- CHANGELOG.md | 1 + lib/pleroma/application.ex | 1 + lib/pleroma/html.ex | 232 +++------------------------------ priv/scrubbers/default.ex | 93 +++++++++++++ priv/scrubbers/links_only.ex | 27 ++++ priv/scrubbers/media_proxy.ex | 32 +++++ priv/scrubbers/twitter_text.ex | 57 ++++++++ 8 files changed, 231 insertions(+), 214 deletions(-) create mode 100644 priv/scrubbers/default.ex create mode 100644 priv/scrubbers/links_only.ex create mode 100644 priv/scrubbers/media_proxy.ex create mode 100644 priv/scrubbers/twitter_text.ex diff --git a/.formatter.exs b/.formatter.exs index 7fa95a619..5799ac127 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,3 +1,3 @@ [ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs"] + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs", "priv/scrubbers/*.ex"] ] diff --git a/CHANGELOG.md b/CHANGELOG.md index d00097748..3d578caef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Report emails now include functional links to profiles of remote user accounts - Not being able to log in to some third-party apps when logged in to MastoFE - MRF: `Delete` activities being exempt from MRF policies +- OTP releases: Not being able to configure HTML sanitization policy
API Changes diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 57462740c..5b844aa41 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -30,6 +30,7 @@ def user_agent do # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do + Pleroma.HTML.compile_scrubbers() Pleroma.Config.DeprecationWarnings.warn() setup_instrumenters() diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 71c53ce0e..2cae29f35 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -3,6 +3,25 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTML do + # Scrubbers are compiled on boot so they can be configured in OTP releases + # @on_load :compile_scrubbers + + def compile_scrubbers do + dir = Path.join(:code.priv_dir(:pleroma), "scrubbers") + + dir + |> File.ls!() + |> Enum.map(&Path.join(dir, &1)) + |> Kernel.ParallelCompiler.compile() + |> case do + {:error, _errors, _warnings} -> + raise "Compiling scrubbers failed" + + {:ok, _modules, _warnings} -> + :ok + end + end + defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber] defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default] @@ -99,216 +118,3 @@ def extract_first_external_url(object, content) do end) end end - -defmodule Pleroma.HTML.Scrubber.TwitterText do - @moduledoc """ - An HTML scrubbing policy which limits to twitter-style text. Only - paragraphs, breaks and links are allowed through the filter. - """ - - @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - - require FastSanitize.Sanitizer.Meta - alias FastSanitize.Sanitizer.Meta - - Meta.strip_comments() - - # links - Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) - - Meta.allow_tag_with_this_attribute_values(:a, "class", [ - "hashtag", - "u-url", - "mention", - "u-url mention", - "mention u-url" - ]) - - Meta.allow_tag_with_this_attribute_values(:a, "rel", [ - "tag", - "nofollow", - "noopener", - "noreferrer" - ]) - - Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) - - # paragraphs and linebreaks - Meta.allow_tag_with_these_attributes(:br, []) - Meta.allow_tag_with_these_attributes(:p, []) - - # microformats - Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"]) - Meta.allow_tag_with_these_attributes(:span, []) - - # allow inline images for custom emoji - if Pleroma.Config.get([:markup, :allow_inline_images]) do - # restrict img tags to http/https only, because of MediaProxy. - Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) - - Meta.allow_tag_with_these_attributes(:img, [ - "width", - "height", - "class", - "title", - "alt" - ]) - end - - Meta.strip_everything_not_covered() -end - -defmodule Pleroma.HTML.Scrubber.Default do - @doc "The default HTML scrubbing policy: no " - - require FastSanitize.Sanitizer.Meta - alias FastSanitize.Sanitizer.Meta - - # credo:disable-for-previous-line - # No idea how to fix this one… - - @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - - Meta.strip_comments() - - Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) - - Meta.allow_tag_with_this_attribute_values(:a, "class", [ - "hashtag", - "u-url", - "mention", - "u-url mention", - "mention u-url" - ]) - - Meta.allow_tag_with_this_attribute_values(:a, "rel", [ - "tag", - "nofollow", - "noopener", - "noreferrer", - "ugc" - ]) - - Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) - - Meta.allow_tag_with_these_attributes(:abbr, ["title"]) - - Meta.allow_tag_with_these_attributes(:b, []) - Meta.allow_tag_with_these_attributes(:blockquote, []) - Meta.allow_tag_with_these_attributes(:br, []) - Meta.allow_tag_with_these_attributes(:code, []) - Meta.allow_tag_with_these_attributes(:del, []) - Meta.allow_tag_with_these_attributes(:em, []) - Meta.allow_tag_with_these_attributes(:i, []) - Meta.allow_tag_with_these_attributes(:li, []) - Meta.allow_tag_with_these_attributes(:ol, []) - Meta.allow_tag_with_these_attributes(:p, []) - Meta.allow_tag_with_these_attributes(:pre, []) - Meta.allow_tag_with_these_attributes(:strong, []) - Meta.allow_tag_with_these_attributes(:sub, []) - Meta.allow_tag_with_these_attributes(:sup, []) - Meta.allow_tag_with_these_attributes(:u, []) - Meta.allow_tag_with_these_attributes(:ul, []) - - Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"]) - Meta.allow_tag_with_these_attributes(:span, []) - - @allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images]) - - if @allow_inline_images do - # restrict img tags to http/https only, because of MediaProxy. - Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) - - Meta.allow_tag_with_these_attributes(:img, [ - "width", - "height", - "class", - "title", - "alt" - ]) - end - - if Pleroma.Config.get([:markup, :allow_tables]) do - Meta.allow_tag_with_these_attributes(:table, []) - Meta.allow_tag_with_these_attributes(:tbody, []) - Meta.allow_tag_with_these_attributes(:td, []) - Meta.allow_tag_with_these_attributes(:th, []) - Meta.allow_tag_with_these_attributes(:thead, []) - Meta.allow_tag_with_these_attributes(:tr, []) - end - - if Pleroma.Config.get([:markup, :allow_headings]) do - Meta.allow_tag_with_these_attributes(:h1, []) - Meta.allow_tag_with_these_attributes(:h2, []) - Meta.allow_tag_with_these_attributes(:h3, []) - Meta.allow_tag_with_these_attributes(:h4, []) - Meta.allow_tag_with_these_attributes(:h5, []) - end - - if Pleroma.Config.get([:markup, :allow_fonts]) do - Meta.allow_tag_with_these_attributes(:font, ["face"]) - end - - Meta.strip_everything_not_covered() -end - -defmodule Pleroma.HTML.Transform.MediaProxy do - @moduledoc "Transforms inline image URIs to use MediaProxy." - - alias Pleroma.Web.MediaProxy - - def before_scrub(html), do: html - - def scrub_attribute(:img, {"src", "http" <> target}) do - media_url = - ("http" <> target) - |> MediaProxy.url() - - {"src", media_url} - end - - def scrub_attribute(_tag, attribute), do: attribute - - def scrub({:img, attributes, children}) do - attributes = - attributes - |> Enum.map(fn attr -> scrub_attribute(:img, attr) end) - |> Enum.reject(&is_nil(&1)) - - {:img, attributes, children} - end - - def scrub({:comment, _text, _children}), do: "" - - def scrub({tag, attributes, children}), do: {tag, attributes, children} - def scrub({_tag, children}), do: children - def scrub(text), do: text -end - -defmodule Pleroma.HTML.Scrubber.LinksOnly do - @moduledoc """ - An HTML scrubbing policy which limits to links only. - """ - - @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - - require FastSanitize.Sanitizer.Meta - alias FastSanitize.Sanitizer.Meta - - Meta.strip_comments() - - # links - Meta.allow_tag_with_uri_attributes(:a, ["href"], @valid_schemes) - - Meta.allow_tag_with_this_attribute_values(:a, "rel", [ - "tag", - "nofollow", - "noopener", - "noreferrer", - "me", - "ugc" - ]) - - Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) - Meta.strip_everything_not_covered() -end diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex new file mode 100644 index 000000000..ea0480dcd --- /dev/null +++ b/priv/scrubbers/default.ex @@ -0,0 +1,93 @@ +defmodule Pleroma.HTML.Scrubber.Default do + @doc "The default HTML scrubbing policy: no " + + require FastSanitize.Sanitizer.Meta + alias FastSanitize.Sanitizer.Meta + + # credo:disable-for-previous-line + # No idea how to fix this one… + + @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) + + Meta.strip_comments() + + Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) + + Meta.allow_tag_with_this_attribute_values(:a, "class", [ + "hashtag", + "u-url", + "mention", + "u-url mention", + "mention u-url" + ]) + + Meta.allow_tag_with_this_attribute_values(:a, "rel", [ + "tag", + "nofollow", + "noopener", + "noreferrer", + "ugc" + ]) + + Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) + + Meta.allow_tag_with_these_attributes(:abbr, ["title"]) + + Meta.allow_tag_with_these_attributes(:b, []) + Meta.allow_tag_with_these_attributes(:blockquote, []) + Meta.allow_tag_with_these_attributes(:br, []) + Meta.allow_tag_with_these_attributes(:code, []) + Meta.allow_tag_with_these_attributes(:del, []) + Meta.allow_tag_with_these_attributes(:em, []) + Meta.allow_tag_with_these_attributes(:i, []) + Meta.allow_tag_with_these_attributes(:li, []) + Meta.allow_tag_with_these_attributes(:ol, []) + Meta.allow_tag_with_these_attributes(:p, []) + Meta.allow_tag_with_these_attributes(:pre, []) + Meta.allow_tag_with_these_attributes(:strong, []) + Meta.allow_tag_with_these_attributes(:sub, []) + Meta.allow_tag_with_these_attributes(:sup, []) + Meta.allow_tag_with_these_attributes(:u, []) + Meta.allow_tag_with_these_attributes(:ul, []) + + Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"]) + Meta.allow_tag_with_these_attributes(:span, []) + + @allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images]) + + if @allow_inline_images do + # restrict img tags to http/https only, because of MediaProxy. + Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) + + Meta.allow_tag_with_these_attributes(:img, [ + "width", + "height", + "class", + "title", + "alt" + ]) + end + + if Pleroma.Config.get([:markup, :allow_tables]) do + Meta.allow_tag_with_these_attributes(:table, []) + Meta.allow_tag_with_these_attributes(:tbody, []) + Meta.allow_tag_with_these_attributes(:td, []) + Meta.allow_tag_with_these_attributes(:th, []) + Meta.allow_tag_with_these_attributes(:thead, []) + Meta.allow_tag_with_these_attributes(:tr, []) + end + + if Pleroma.Config.get([:markup, :allow_headings]) do + Meta.allow_tag_with_these_attributes(:h1, []) + Meta.allow_tag_with_these_attributes(:h2, []) + Meta.allow_tag_with_these_attributes(:h3, []) + Meta.allow_tag_with_these_attributes(:h4, []) + Meta.allow_tag_with_these_attributes(:h5, []) + end + + if Pleroma.Config.get([:markup, :allow_fonts]) do + Meta.allow_tag_with_these_attributes(:font, ["face"]) + end + + Meta.strip_everything_not_covered() +end diff --git a/priv/scrubbers/links_only.ex b/priv/scrubbers/links_only.ex new file mode 100644 index 000000000..b30a00589 --- /dev/null +++ b/priv/scrubbers/links_only.ex @@ -0,0 +1,27 @@ +defmodule Pleroma.HTML.Scrubber.LinksOnly do + @moduledoc """ + An HTML scrubbing policy which limits to links only. + """ + + @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) + + require FastSanitize.Sanitizer.Meta + alias FastSanitize.Sanitizer.Meta + + Meta.strip_comments() + + # links + Meta.allow_tag_with_uri_attributes(:a, ["href"], @valid_schemes) + + Meta.allow_tag_with_this_attribute_values(:a, "rel", [ + "tag", + "nofollow", + "noopener", + "noreferrer", + "me", + "ugc" + ]) + + Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) + Meta.strip_everything_not_covered() +end diff --git a/priv/scrubbers/media_proxy.ex b/priv/scrubbers/media_proxy.ex new file mode 100644 index 000000000..5dbe57666 --- /dev/null +++ b/priv/scrubbers/media_proxy.ex @@ -0,0 +1,32 @@ +defmodule Pleroma.HTML.Transform.MediaProxy do + @moduledoc "Transforms inline image URIs to use MediaProxy." + + alias Pleroma.Web.MediaProxy + + def before_scrub(html), do: html + + def scrub_attribute(:img, {"src", "http" <> target}) do + media_url = + ("http" <> target) + |> MediaProxy.url() + + {"src", media_url} + end + + def scrub_attribute(_tag, attribute), do: attribute + + def scrub({:img, attributes, children}) do + attributes = + attributes + |> Enum.map(fn attr -> scrub_attribute(:img, attr) end) + |> Enum.reject(&is_nil(&1)) + + {:img, attributes, children} + end + + def scrub({:comment, _text, _children}), do: "" + + def scrub({tag, attributes, children}), do: {tag, attributes, children} + def scrub({_tag, children}), do: children + def scrub(text), do: text +end diff --git a/priv/scrubbers/twitter_text.ex b/priv/scrubbers/twitter_text.ex new file mode 100644 index 000000000..c4e796cad --- /dev/null +++ b/priv/scrubbers/twitter_text.ex @@ -0,0 +1,57 @@ +defmodule Pleroma.HTML.Scrubber.TwitterText do + @moduledoc """ + An HTML scrubbing policy which limits to twitter-style text. Only + paragraphs, breaks and links are allowed through the filter. + """ + + @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) + + require FastSanitize.Sanitizer.Meta + alias FastSanitize.Sanitizer.Meta + + Meta.strip_comments() + + # links + Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) + + Meta.allow_tag_with_this_attribute_values(:a, "class", [ + "hashtag", + "u-url", + "mention", + "u-url mention", + "mention u-url" + ]) + + Meta.allow_tag_with_this_attribute_values(:a, "rel", [ + "tag", + "nofollow", + "noopener", + "noreferrer" + ]) + + Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) + + # paragraphs and linebreaks + Meta.allow_tag_with_these_attributes(:br, []) + Meta.allow_tag_with_these_attributes(:p, []) + + # microformats + Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"]) + Meta.allow_tag_with_these_attributes(:span, []) + + # allow inline images for custom emoji + if Pleroma.Config.get([:markup, :allow_inline_images]) do + # restrict img tags to http/https only, because of MediaProxy. + Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) + + Meta.allow_tag_with_these_attributes(:img, [ + "width", + "height", + "class", + "title", + "alt" + ]) + end + + Meta.strip_everything_not_covered() +end From 3c45ed4f4752bb2dce3ca2e9a153cb278df685fb Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 8 Dec 2019 21:08:25 +0300 Subject: [PATCH 61/70] OTP: Fix runtime upload limit config being ignored Closes #1109 --- lib/pleroma/plugs/parsers_plug.ex | 21 +++++++++++++++++++++ lib/pleroma/web/endpoint.ex | 9 +-------- 2 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 lib/pleroma/plugs/parsers_plug.ex diff --git a/lib/pleroma/plugs/parsers_plug.ex b/lib/pleroma/plugs/parsers_plug.ex new file mode 100644 index 000000000..2e493ce0e --- /dev/null +++ b/lib/pleroma/plugs/parsers_plug.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.Parsers do + @moduledoc "Initializes Plug.Parsers with upload limit set at boot time" + + @behaviour Plug + + def init(_opts) do + Plug.Parsers.init( + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Jason, + length: Pleroma.Config.get([:instance, :upload_limit]), + body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} + ) + end + + defdelegate call(conn, opts), to: Plug.Parsers +end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 49735b5c2..bbea31682 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -61,14 +61,7 @@ defmodule Pleroma.Web.Endpoint do plug(Plug.RequestId) plug(Plug.Logger) - plug( - Plug.Parsers, - parsers: [:urlencoded, :multipart, :json], - pass: ["*/*"], - json_decoder: Jason, - length: Pleroma.Config.get([:instance, :upload_limit]), - body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} - ) + plug(Pleroma.Plugs.Parsers) plug(Plug.MethodOverride) plug(Plug.Head) From dfae1d705f9bf040186874498e25571eaf71f2ac Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Sun, 8 Dec 2019 21:46:20 +0300 Subject: [PATCH 62/70] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d00097748..d94821a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mix task to list all users (`mix pleroma.user list`) - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers. +- User notification settings: Add `privacy_option` option.
API Changes From e260a16c05f0c3a6694cc85b560eba5e373195c8 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Sun, 8 Dec 2019 22:09:15 +0300 Subject: [PATCH 63/70] update docs --- docs/API/pleroma_api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index ad16d027e..acdb5efb4 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -302,6 +302,7 @@ See [Admin-API](admin_api.md) * `follows`: BOOLEAN field, receives notifications from people the user follows * `remote`: BOOLEAN field, receives notifications from people on remote instances * `local`: BOOLEAN field, receives notifications from people on the local instance + * `privacy_option`: BOOLEAN field, set privacy direct messages. exclude contents of a message from push notification when it's true. * Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}` ## `/api/pleroma/healthcheck` From eb9ea8475b19d6a00f690421b193310c09ba40f3 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 9 Dec 2019 04:07:16 +0300 Subject: [PATCH 64/70] docs: add OTP/From source tabs to CLI tasks --- docs/administration/CLI_tasks/config.md | 21 ++- docs/administration/CLI_tasks/database.md | 46 ++++-- docs/administration/CLI_tasks/digest.md | 23 ++- docs/administration/CLI_tasks/emoji.md | 30 +++- .../CLI_tasks/general_cli_task_info.include | 5 + docs/administration/CLI_tasks/instance.md | 11 +- docs/administration/CLI_tasks/relay.md | 29 ++-- docs/administration/CLI_tasks/uploads.md | 11 +- docs/administration/CLI_tasks/user.md | 153 +++++++++++++----- 9 files changed, 242 insertions(+), 87 deletions(-) create mode 100644 docs/administration/CLI_tasks/general_cli_task_info.include diff --git a/docs/administration/CLI_tasks/config.md b/docs/administration/CLI_tasks/config.md index ce19e2402..e9d44b9a4 100644 --- a/docs/administration/CLI_tasks/config.md +++ b/docs/administration/CLI_tasks/config.md @@ -3,17 +3,26 @@ !!! danger This is a Work In Progress, not usable just yet. -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl config` and in case of source installs it's -`mix pleroma.config`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Transfer config from file to DB. -```sh -$PREFIX migrate_to_db +```sh tab="OTP" + ./bin/pleroma_ctl config migrate_to_db ``` +```sh tab="From Source" +mix pleroma.config migrate_to_db +``` + + ## Transfer config from DB to `config/env.exported_from_db.secret.exs` -```sh -$PREFIX migrate_from_db +```sh tab="OTP" + ./bin/pleroma_ctl config migrate_from_db ``` + +```sh tab="From Source" +mix pleroma.config migrate_from_db +``` + diff --git a/docs/administration/CLI_tasks/database.md b/docs/administration/CLI_tasks/database.md index 3011646c8..51c7484ba 100644 --- a/docs/administration/CLI_tasks/database.md +++ b/docs/administration/CLI_tasks/database.md @@ -1,6 +1,6 @@ # Database maintenance tasks -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl database` and in case of source installs it's `mix pleroma.database`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} !!! danger These mix tasks can take a long time to complete. Many of them were written to address specific database issues that happened because of bugs in migrations or other specific scenarios. Do not run these tasks "just in case" if everything is fine your instance. @@ -9,8 +9,12 @@ Every command should be ran with a prefix, in case of OTP releases it is `./bin/ Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration. -```sh -$PREFIX remove_embedded_objects [] +```sh tab="OTP" +./bin/pleroma_ctl database remove_embedded_objects [] +``` + +```sh tab="From Source" +mix pleroma.database remove_embedded_objects [] ``` ### Options @@ -20,11 +24,15 @@ $PREFIX remove_embedded_objects [] This will prune remote posts older than 90 days (configurable with [`config :pleroma, :instance, remote_post_retention_days`](../../configuration/cheatsheet.md#instance)) from the database, they will be refetched from source when accessed. -!!! note - The disk space will only be reclaimed after `VACUUM FULL` +!!! danger + The disk space will only be reclaimed after `VACUUM FULL`. You may run out of disk space during the execution of the task or vacuuming if you don't have about 1/3rds of the database size free. -```sh -$PREFIX pleroma.database prune_objects [] +```sh tab="OTP" +./bin/pleroma_ctl database prune_objects [] +``` + +```sh tab="From Source" +mix pleroma.database prune_objects [] ``` ### Options @@ -34,18 +42,30 @@ $PREFIX pleroma.database prune_objects [] Can be safely re-run -```sh -$PREFIX bump_all_conversations +```sh tab="OTP" +./bin/pleroma_ctl database bump_all_conversations +``` + +```sh tab="From Source" +mix pleroma.database bump_all_conversations ``` ## Remove duplicated items from following and update followers count for all users -```sh -$PREFIX update_users_following_followers_counts +```sh tab="OTP" +./bin/pleroma_ctl database update_users_following_followers_counts +``` + +```sh tab="From Source" +mix pleroma.database update_users_following_followers_counts ``` ## Fix the pre-existing "likes" collections for all objects -```sh -$PREFIX fix_likes_collections +```sh tab="OTP" +./bin/pleroma_ctl database fix_likes_collections +``` + +```sh tab="From Source" +mix pleroma.database fix_likes_collections ``` diff --git a/docs/administration/CLI_tasks/digest.md b/docs/administration/CLI_tasks/digest.md index 547702031..a70f24c06 100644 --- a/docs/administration/CLI_tasks/digest.md +++ b/docs/administration/CLI_tasks/digest.md @@ -1,13 +1,24 @@ # Managing digest emails -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl digest` and in case of source installs it's `mix pleroma.digest`. + +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Send digest email since given date (user registration date by default) ignoring user activity status. -```sh -$PREFIX test [] +```sh tab="OTP" + ./bin/pleroma_ctl digest test [] ``` -Example: -```sh -$PREFIX test donaldtheduck 2019-05-20 +```sh tab="From Source" +mix pleroma.digest test [] ``` + + +Example: +```sh tab="OTP" + ./bin/pleroma_ctl digest test donaldtheduck 2019-05-20 +``` + +```sh tab="From Source" +mix pleroma.digest test donaldtheduck 2019-05-20 +``` + diff --git a/docs/administration/CLI_tasks/emoji.md b/docs/administration/CLI_tasks/emoji.md index eee02f2ef..a3207bc6c 100644 --- a/docs/administration/CLI_tasks/emoji.md +++ b/docs/administration/CLI_tasks/emoji.md @@ -1,28 +1,44 @@ # Managing emoji packs -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl emoji` and in case of source installs it's `mix pleroma.emoji`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Lists emoji packs and metadata specified in the manifest -```sh -$PREFIX ls-packs [] +```sh tab="OTP" +./bin/pleroma_ctl emoji ls-packs [] ``` +```sh tab="From Source" +mix pleroma.emoji ls-packs [] +``` + + ### Options - `-m, --manifest PATH/URL` - path to a custom manifest, it can either be an URL starting with `http`, in that case the manifest will be fetched from that address, or a local path ## Fetch, verify and install the specified packs from the manifest into `STATIC-DIR/emoji/PACK-NAME` -```sh -$PREFIX get-packs [] + +```sh tab="OTP" +./bin/pleroma_ctl emoji get-packs [] +``` + +```sh tab="From Source" +mix pleroma.emoji get-packs [] ``` ### Options - `-m, --manifest PATH/URL` - same as [`ls-packs`](#ls-packs) ## Create a new manifest entry and a file list from the specified remote pack file -```sh -$PREFIX gen-pack PACK-URL + +```sh tab="OTP" +./bin/pleroma_ctl emoji gen-pack PACK-URL ``` + +```sh tab="From Source" +mix pleroma.emoji gen-pack PACK-URL +``` + Currently, only .zip archives are recognized as remote pack files and packs are therefore assumed to be zip archives. This command is intended to run interactively and will first ask you some basic questions about the pack, then download the remote file and generate an SHA256 checksum for it, then generate an emoji file list for you. The manifest entry will either be written to a newly created `index.json` file or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously. diff --git a/docs/administration/CLI_tasks/general_cli_task_info.include b/docs/administration/CLI_tasks/general_cli_task_info.include new file mode 100644 index 000000000..a1ff1da12 --- /dev/null +++ b/docs/administration/CLI_tasks/general_cli_task_info.include @@ -0,0 +1,5 @@ +Every command should be ran as the `pleroma` user from it's home directory. For example if you are superuser, you would have to wrap the command in `su pleroma -s $SHELL -lc "$COMMAND"`. + +??? note "From source note about `MIX_ENV`" + + The `mix` command should be prefixed with the name of environment your Pleroma server is running in, usually it's `MIX_ENV=prod` diff --git a/docs/administration/CLI_tasks/instance.md b/docs/administration/CLI_tasks/instance.md index ab0b68ad0..1a3b268be 100644 --- a/docs/administration/CLI_tasks/instance.md +++ b/docs/administration/CLI_tasks/instance.md @@ -1,12 +1,17 @@ # Managing instance configuration -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl instance` and in case of source installs it's `mix pleroma.instance`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Generate a new configuration file -```sh -$PREFIX gen [] +```sh tab="OTP" + ./bin/pleroma_ctl instance gen [] ``` +```sh tab="From Source" +mix pleroma.instance gen [] +``` + + If any of the options are left unspecified, you will be prompted interactively. ### Options diff --git a/docs/administration/CLI_tasks/relay.md b/docs/administration/CLI_tasks/relay.md index aa44617df..c4f078f4d 100644 --- a/docs/administration/CLI_tasks/relay.md +++ b/docs/administration/CLI_tasks/relay.md @@ -1,30 +1,33 @@ # Managing relays -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl relay` and in case of source installs it's `mix pleroma.relay`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Follow a relay -```sh -$PREFIX follow + +```sh tab="OTP" +./bin/pleroma_ctl relay follow ``` -Example: -```sh -$PREFIX follow https://example.org/relay +```sh tab="From Source" +mix pleroma.relay follow ``` ## Unfollow a remote relay -```sh -$PREFIX unfollow +```sh tab="OTP" +./bin/pleroma_ctl relay unfollow ``` -Example: -```sh -$PREFIX unfollow https://example.org/relay +```sh tab="From Source" +mix pleroma.relay unfollow ``` ## List relay subscriptions -```sh -$PREFIX list +```sh tab="OTP" +./bin/pleroma_ctl relay list +``` + +```sh tab="From Source" +mix pleroma.relay list ``` diff --git a/docs/administration/CLI_tasks/uploads.md b/docs/administration/CLI_tasks/uploads.md index 71800e341..e36c94c38 100644 --- a/docs/administration/CLI_tasks/uploads.md +++ b/docs/administration/CLI_tasks/uploads.md @@ -1,11 +1,16 @@ # Managing uploads -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl uploads` and in case of source installs it's `mix pleroma.uploads`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Migrate uploads from local to remote storage -```sh -$PREFIX migrate_local [] +```sh tab="OTP" + ./bin/pleroma_ctl uploads migrate_local [] ``` + +```sh tab="From Source" +mix pleroma.uploads migrate_local [] +``` + ### Options - `--delete` - delete local uploads after migrating them to the target uploader diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index 96b2d9e6a..da8363131 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -1,12 +1,18 @@ # Managing users -Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl user` and in case of source installs it's `mix pleroma.user`. +{! backend/administration/CLI_tasks/general_cli_task_info.include !} ## Create a user -```sh -$PREFIX new [] + +```sh tab="OTP" +./bin/pleroma_ctl user new [] ``` +```sh tab="From Source" +mix pleroma.user new [] +``` + + ### Options - `--name ` - the user's display name - `--bio ` - the user's bio @@ -16,84 +22,159 @@ $PREFIX new [] - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions ## List local users -```sh -$PREFIX list +```sh tab="OTP" + ./bin/pleroma_ctl user list ``` -## Generate an invite link -```sh -$PREFIX invite [] +```sh tab="From Source" +mix pleroma.user list ``` + +## Generate an invite link +```sh tab="OTP" + ./bin/pleroma_ctl user invite [] +``` + +```sh tab="From Source" +mix pleroma.user invite [] +``` + + ### Options - `--expires-at DATE` - last day on which token is active (e.g. "2019-04-05") - `--max-use NUMBER` - maximum numbers of token uses ## List generated invites -```sh -$PREFIX invites +```sh tab="OTP" + ./bin/pleroma_ctl user invites ``` +```sh tab="From Source" +mix pleroma.user invites +``` + + ## Revoke invite -```sh -$PREFIX revoke_invite +```sh tab="OTP" + ./bin/pleroma_ctl user revoke_invite ``` +```sh tab="From Source" +mix pleroma.user revoke_invite +``` + + ## Delete a user -```sh -$PREFIX rm +```sh tab="OTP" + ./bin/pleroma_ctl user rm ``` +```sh tab="From Source" +mix pleroma.user rm +``` + + ## Delete user's posts and interactions -```sh -$PREFIX delete_activities +```sh tab="OTP" + ./bin/pleroma_ctl user delete_activities ``` +```sh tab="From Source" +mix pleroma.user delete_activities +``` + + ## Sign user out from all applications (delete user's OAuth tokens and authorizations) -```sh -$PREFIX sign_out +```sh tab="OTP" + ./bin/pleroma_ctl user sign_out ``` +```sh tab="From Source" +mix pleroma.user sign_out +``` + + ## Deactivate or activate a user -```sh -$PREFIX toggle_activated +```sh tab="OTP" + ./bin/pleroma_ctl user toggle_activated ``` +```sh tab="From Source" +mix pleroma.user toggle_activated +``` + + ## Unsubscribe local users from a user and deactivate the user -```sh -$PREFIX unsubscribe NICKNAME +```sh tab="OTP" + ./bin/pleroma_ctl user unsubscribe NICKNAME ``` +```sh tab="From Source" +mix pleroma.user unsubscribe NICKNAME +``` + + ## Unsubscribe local users from an instance and deactivate all accounts on it -```sh -$PREFIX unsubscribe_all_from_instance +```sh tab="OTP" + ./bin/pleroma_ctl user unsubscribe_all_from_instance ``` +```sh tab="From Source" +mix pleroma.user unsubscribe_all_from_instance +``` + + ## Create a password reset link for user -```sh -$PREFIX reset_password +```sh tab="OTP" + ./bin/pleroma_ctl user reset_password ``` -## Set the value of the given user's settings -```sh -$PREFIX set [] +```sh tab="From Source" +mix pleroma.user reset_password ``` + + +## Set the value of the given user's settings +```sh tab="OTP" + ./bin/pleroma_ctl user set [] +``` + +```sh tab="From Source" +mix pleroma.user set [] +``` + ### Options - `--locked`/`--no-locked` - whether the user should be locked - `--moderator`/`--no-moderator` - whether the user should be a moderator - `--admin`/`--no-admin` - whether the user should be an admin ## Add tags to a user -```sh -$PREFIX tag +```sh tab="OTP" + ./bin/pleroma_ctl user tag ``` +```sh tab="From Source" +mix pleroma.user tag +``` + + ## Delete tags from a user -```sh -$PREFIX untag +```sh tab="OTP" + ./bin/pleroma_ctl user untag ``` -## Toggle confirmation status of the user -```sh -$PREFIX toggle_confirmed +```sh tab="From Source" +mix pleroma.user untag ``` + + +## Toggle confirmation status of the user +```sh tab="OTP" + ./bin/pleroma_ctl user toggle_confirmed +``` + +```sh tab="From Source" +mix pleroma.user toggle_confirmed +``` + From b4027e71871b4a73f0ee9f0431af7c6208e51f8d Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 9 Dec 2019 14:46:06 +0700 Subject: [PATCH 65/70] Document `with_move` parameter of the notification API --- docs/API/differences_in_mastoapi_responses.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 006d17c1b..566789ec7 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -103,6 +103,7 @@ The `type` value is `move`. Has an additional field: Accepts additional parameters: - `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`. +- `with_move`: boolean, when set to `true` will include Move notifications. `false` by default. ## POST `/api/v1/statuses` From eb7f412b260d1887a3df644cc71f9ef49f3c13eb Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 9 Dec 2019 14:43:57 +0100 Subject: [PATCH 66/70] Changelog: Update with user timeline change info. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d00097748..64da5d379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Mark the direct conversation as read for the author when they send a new direct message - Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload. - Admin API: Render whole status in grouped reports +- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
### Added From 4692919ea68d30fbd1147ad9415a4573e31023be Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 9 Dec 2019 14:15:59 +0000 Subject: [PATCH 67/70] Update pleroma_api.md --- docs/API/pleroma_api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index acdb5efb4..7228d805b 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -302,7 +302,7 @@ See [Admin-API](admin_api.md) * `follows`: BOOLEAN field, receives notifications from people the user follows * `remote`: BOOLEAN field, receives notifications from people on remote instances * `local`: BOOLEAN field, receives notifications from people on the local instance - * `privacy_option`: BOOLEAN field, set privacy direct messages. exclude contents of a message from push notification when it's true. + * `privacy_option`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification. * Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}` ## `/api/pleroma/healthcheck` From f4b7f32d51f9d0bd721befdd33b49d2c52a6e231 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 9 Dec 2019 20:45:04 +0300 Subject: [PATCH 68/70] status search: prefer the status fetched by url over other results --- lib/pleroma/activity/search.ex | 2 +- .../mastodon_api/controllers/search_controller_test.exs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index f847ac238..d30a5a6a5 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -86,7 +86,7 @@ defp maybe_fetch(activities, user, search_query) do {:ok, object} <- Fetcher.fetch_object_from_id(search_query), %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), true <- Visibility.visible_for_user?(activity, user) do - activities ++ [activity] + [activity | activities] else _ -> activities end diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 7953fad62..34deeba47 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -165,15 +165,20 @@ test "search", %{conn: conn} do assert status["id"] == to_string(activity.id) end - test "search fetches remote statuses", %{conn: conn} do + test "search fetches remote statuses and prefers them over other results", %{conn: conn} do capture_log(fn -> + {:ok, %{id: activity_id}} = + CommonAPI.post(insert(:user), %{ + "status" => "check out https://shitposter.club/notice/2827873" + }) + conn = conn |> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"}) assert results = json_response(conn, 200) - [status] = results["statuses"] + [status, %{"id" => ^activity_id}] = results["statuses"] assert status["uri"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" From 9dfaa0b832ddb09f0937c96e5e30b83957f8185f Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Mon, 9 Dec 2019 22:29:44 +0300 Subject: [PATCH 69/70] fix loads config variable with large value from db --- lib/mix/tasks/pleroma/config.ex | 4 +- test/tasks/config_test.exs | 80 +++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 0e21408b2..590c7a914 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -52,7 +52,9 @@ def run(["migrate_from_db", env, delete?]) do |> Enum.each(fn config -> IO.write( file, - "config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n" + "config :#{config.group}, #{config.key}, #{ + inspect(Config.from_binary(config.value), limit: :infinity) + }\r\n\r\n" ) if delete? do diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index 9cd47380c..fab9d6e9a 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -63,4 +63,84 @@ test "settings are migrated to file and deleted from db", %{temp_file: temp_file assert file =~ "config :pleroma, :setting_first," assert file =~ "config :pleroma, :setting_second," end + + test "load a settings with large values and pass to file", %{temp_file: temp_file} do + Config.create(%{ + group: "pleroma", + key: ":instance", + value: [ + name: "Pleroma", + email: "example@example.com", + notify_email: "noreply@example.com", + description: "A Pleroma instance, an alternative fediverse server", + limit: 5_000, + chat_limit: 5_000, + remote_limit: 100_000, + upload_limit: 16_000_000, + avatar_upload_limit: 2_000_000, + background_upload_limit: 4_000_000, + banner_upload_limit: 4_000_000, + poll_limits: %{ + max_options: 20, + max_option_chars: 200, + min_expiration: 0, + max_expiration: 365 * 24 * 60 * 60 + }, + registrations_open: true, + federating: true, + federation_incoming_replies_max_depth: 100, + federation_reachability_timeout_days: 7, + federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher], + allow_relay: true, + rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, + public: true, + quarantined_instances: [], + managed_config: true, + static_dir: "instance/static/", + allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], + mrf_transparency: true, + mrf_transparency_exclusions: [], + autofollowed_nicknames: [], + max_pinned_statuses: 1, + no_attachment_links: true, + welcome_user_nickname: nil, + welcome_message: nil, + max_report_comment_size: 1000, + safe_dm_mentions: false, + healthcheck: false, + remote_post_retention_days: 90, + skip_thread_containment: true, + limit_to_local_content: :unauthenticated, + dynamic_configuration: false, + user_bio_length: 5000, + user_name_length: 100, + max_account_fields: 10, + max_remote_account_fields: 20, + account_field_name_length: 512, + account_field_value_length: 2048, + external_user_synchronization: true, + extended_nickname_format: true, + multi_factor_authentication: [ + totp: [ + # digits 6 or 8 + digits: 6, + period: 30 + ], + backup_codes: [ + number: 2, + length: 6 + ] + ] + ] + }) + + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "temp", "true"]) + + assert Repo.all(Config) == [] + assert File.exists?(temp_file) + {:ok, file} = File.read(temp_file) + + assert file == + "use Mix.Config\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n mrf_transparency: true,\n mrf_transparency_exclusions: [],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n no_attachment_links: true,\n welcome_user_nickname: nil,\n welcome_message: nil,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n dynamic_configuration: false,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" + end end From 701815e64c35160d29e418724c29cbe2d8b4024d Mon Sep 17 00:00:00 2001 From: Hakaba Hitoyo Date: Tue, 10 Dec 2019 13:19:26 +0000 Subject: [PATCH 70/70] [ActivityPub] Configurable ActivityPub actor type --- CHANGELOG.md | 4 ++++ docs/API/differences_in_mastoapi_responses.md | 4 ++++ lib/pleroma/user.ex | 4 ++++ lib/pleroma/web/activity_pub/activity_pub.ex | 2 ++ lib/pleroma/web/activity_pub/views/user_view.ex | 2 +- .../web/mastodon_api/controllers/account_controller.ex | 1 + lib/pleroma/web/mastodon_api/views/account_view.ex | 5 +++-- .../20191123030554_add_activitypub_actor_type.exs | 9 +++++++++ test/web/mastodon_api/views/account_view_test.exs | 9 +++++++-- 9 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 priv/repo/migrations/20191123030554_add_activitypub_actor_type.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 847dbe902..4b1fdcdce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers. - User notification settings: Add `privacy_option` option. +- User settings: Add _This account is a_ option.
API Changes @@ -78,6 +79,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Pleroma API: Add Emoji reactions - Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance - Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users +- ActivityPub: Configurable `type` field of the actors. +- Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field. +- Mastodon API: `/api/v1/update_credentials` accepts `actor_type` field.
### Fixed diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 566789ec7..7f5d7681d 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -66,6 +66,8 @@ Has these additional fields under the `pleroma` object: - `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown - `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API +- `discoverable`: boolean, true when the user allows discovery of the account in search results and other services. +- `actor_type`: string, the type of this account. ## Conversations @@ -146,6 +148,8 @@ Additional parameters can be added to the JSON body/Form data: - `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. +- `discoverable` - if true, discovery of this account in search results and other services is allowed. +- `actor_type` - the type of this account. ### Pleroma Settings Store Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about. diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index e2afc6de8..694f1f110 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -127,6 +127,7 @@ defmodule Pleroma.User do field(:invisible, :boolean, default: false) field(:allow_following_move, :boolean, default: true) field(:skip_thread_containment, :boolean, default: false) + field(:actor_type, :string, default: "Person") field(:also_known_as, {:array, :string}, default: []) embeds_one( @@ -346,6 +347,7 @@ def remote_user_creation(params) do :following_count, :discoverable, :invisible, + :actor_type, :also_known_as ] ) @@ -396,6 +398,7 @@ def update_changeset(struct, params \\ %{}) do :raw_fields, :pleroma_settings_store, :discoverable, + :actor_type, :also_known_as ] ) @@ -438,6 +441,7 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do :discoverable, :hide_followers_count, :hide_follows_count, + :actor_type, :also_known_as ] ) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 1e2cc2e2b..6571102a9 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1217,6 +1217,7 @@ defp object_to_user_data(data) do data = Transmogrifier.maybe_fix_user_object(data) discoverable = data["discoverable"] || false invisible = data["invisible"] || false + actor_type = data["type"] || "Person" user_data = %{ ap_id: data["id"], @@ -1232,6 +1233,7 @@ defp object_to_user_data(data) do follower_address: data["followers"], following_address: data["following"], bio: data["summary"], + actor_type: actor_type, also_known_as: Map.get(data, "alsoKnownAs", []) } diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index cf08045c9..9059aa634 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -91,7 +91,7 @@ def render("user.json", %{user: user}) do %{ "id" => user.ap_id, - "type" => "Person", + "type" => user.actor_type, "following" => "#{user.ap_id}/following", "followers" => "#{user.ap_id}/followers", "inbox" => "#{user.ap_id}/inbox", diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index d19029cb5..38d14256f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -188,6 +188,7 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do {:ok, Map.merge(user.pleroma_settings_store, value)} end) |> add_if_present(params, "default_scope", :default_scope) + |> add_if_present(params, "actor_type", :actor_type) emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 546cc0ed5..a5420f480 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -86,7 +86,7 @@ defp do_render("show.json", %{user: user} = opts) do 0 end - bot = (user.source_data["type"] || "Person") in ["Application", "Service"] + bot = user.actor_type in ["Application", "Service"] emojis = (user.source_data["tag"] || []) @@ -137,7 +137,8 @@ defp do_render("show.json", %{user: user} = opts) do sensitive: false, fields: user.raw_fields, pleroma: %{ - discoverable: user.discoverable + discoverable: user.discoverable, + actor_type: user.actor_type } }, diff --git a/priv/repo/migrations/20191123030554_add_activitypub_actor_type.exs b/priv/repo/migrations/20191123030554_add_activitypub_actor_type.exs new file mode 100644 index 000000000..76d3b32c4 --- /dev/null +++ b/priv/repo/migrations/20191123030554_add_activitypub_actor_type.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddActivitypubActorType do + use Ecto.Migration + + def change do + alter table("users") do + add(:actor_type, :string, null: false, default: "Person") + end + end +end diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 5e297d129..2107bb85c 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -66,6 +66,7 @@ test "Represent a user account" do note: "valid html", sensitive: false, pleroma: %{ + actor_type: "Person", discoverable: false }, fields: [] @@ -106,7 +107,8 @@ test "Represent a Service(bot) account" do insert(:user, %{ follower_count: 3, note_count: 5, - source_data: %{"type" => "Service"}, + source_data: %{}, + actor_type: "Service", nickname: "shp@shitposter.club", inserted_at: ~N[2017-08-15 15:47:06.597036] }) @@ -134,6 +136,7 @@ test "Represent a Service(bot) account" do note: user.bio, sensitive: false, pleroma: %{ + actor_type: "Service", discoverable: false }, fields: [] @@ -278,7 +281,8 @@ test "represent an embedded relationship" do insert(:user, %{ follower_count: 0, note_count: 5, - source_data: %{"type" => "Service"}, + source_data: %{}, + actor_type: "Service", nickname: "shp@shitposter.club", inserted_at: ~N[2017-08-15 15:47:06.597036] }) @@ -311,6 +315,7 @@ test "represent an embedded relationship" do note: user.bio, sensitive: false, pleroma: %{ + actor_type: "Service", discoverable: false }, fields: []