Merge branch '1364-no-pushes-from-blocked-domains-users' into 'develop'
authorlain <lain@soykaf.club>
Thu, 30 Apr 2020 10:29:46 +0000 (10:29 +0000)
committerlain <lain@soykaf.club>
Thu, 30 Apr 2020 10:29:46 +0000 (10:29 +0000)
[#1364] [FIX] Disabled notifications on activities from blocked domains (unless actors are followed)

See merge request pleroma/pleroma!2367

CHANGELOG.md
lib/pleroma/following_relationship.ex
lib/pleroma/notification.ex
test/notification_test.exs

index ccc6a5bd4d3f2a9ad3026e09ec1e5b70d04db1f7..c0f1bcf5708352c24e3662b6a0939a89d1d2efea 100644 (file)
@@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Support pagination in conversations API
 - **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again
 - Fix follower/blocks import when nicknames starts with @
+- Filtering of push notifications on activities from blocked domains
 
 ## [unreleased-patch]
 ### Fixed
index 9ccf4049571f4303f8e952874dd4e940572f2eff..3a3082e728037e265bc56a55883f8df7f5e36fe9 100644 (file)
@@ -10,11 +10,12 @@ defmodule Pleroma.FollowingRelationship do
 
   alias Ecto.Changeset
   alias FlakeId.Ecto.CompatType
+  alias Pleroma.FollowingRelationship.State
   alias Pleroma.Repo
   alias Pleroma.User
 
   schema "following_relationships" do
-    field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending)
+    field(:state, State, default: :follow_pending)
 
     belongs_to(:follower, User, type: CompatType)
     belongs_to(:following, User, type: CompatType)
@@ -22,6 +23,11 @@ defmodule Pleroma.FollowingRelationship do
     timestamps()
   end
 
+  @doc "Returns underlying integer code for state atom"
+  def state_int_code(state_atom), do: State.__enum_map__() |> Keyword.fetch!(state_atom)
+
+  def accept_state_code, do: state_int_code(:follow_accept)
+
   def changeset(%__MODULE__{} = following_relationship, attrs) do
     following_relationship
     |> cast(attrs, [:state])
@@ -82,6 +88,29 @@ defmodule Pleroma.FollowingRelationship do
     |> Repo.aggregate(:count, :id)
   end
 
+  def followers_query(%User{} = user) do
+    __MODULE__
+    |> join(:inner, [r], u in User, on: r.follower_id == u.id)
+    |> where([r], r.following_id == ^user.id)
+    |> where([r], r.state == ^:follow_accept)
+  end
+
+  def followers_ap_ids(%User{} = user, from_ap_ids \\ nil) do
+    query =
+      user
+      |> followers_query()
+      |> select([r, u], u.ap_id)
+
+    query =
+      if from_ap_ids do
+        where(query, [r, u], u.ap_id in ^from_ap_ids)
+      else
+        query
+      end
+
+    Repo.all(query)
+  end
+
   def following_count(%User{id: nil}), do: 0
 
   def following_count(%User{} = user) do
@@ -105,12 +134,16 @@ defmodule Pleroma.FollowingRelationship do
     |> Repo.exists?()
   end
 
+  def following_query(%User{} = user) do
+    __MODULE__
+    |> join(:inner, [r], u in User, on: r.following_id == u.id)
+    |> where([r], r.follower_id == ^user.id)
+    |> where([r], r.state == ^:follow_accept)
+  end
+
   def following(%User{} = user) do
     following =
-      __MODULE__
-      |> join(:inner, [r], u in User, on: r.following_id == u.id)
-      |> where([r], r.follower_id == ^user.id)
-      |> where([r], r.state == ^:follow_accept)
+      following_query(user)
       |> select([r, u], u.follower_address)
       |> Repo.all()
 
@@ -171,6 +204,30 @@ defmodule Pleroma.FollowingRelationship do
     end)
   end
 
+  @doc """
+  For a query with joined activity,
+  keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
+  """
+  def keep_following_or_not_domain_blocked(query, user) do
+    where(
+      query,
+      [_, activity],
+      fragment(
+        # "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
+        """
+        NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR
+          ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
+            ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
+        """,
+        activity.actor,
+        ^user.domain_blocks,
+        activity.actor,
+        ^User.binary_id(user.id),
+        ^accept_state_code()
+      )
+    )
+  end
+
   defp validate_not_self_relationship(%Changeset{} = changeset) do
     changeset
     |> validate_follower_id_following_id_inequality()
index 73e19bf970cb3edc5c10fd36fc9e08dbcc059b57..aaa67525366c0aca362970525eb366e2403ba696 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Notification do
   use Ecto.Schema
 
   alias Pleroma.Activity
+  alias Pleroma.FollowingRelationship
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Pagination
@@ -81,15 +82,13 @@ defmodule Pleroma.Notification do
     |> exclude_visibility(opts)
   end
 
+  # Excludes blocked users and non-followed domain-blocked users
   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)
-    |> where(
-      [n, a],
-      fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
-    )
+    |> FollowingRelationship.keep_following_or_not_domain_blocked(user)
   end
 
   defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
@@ -330,10 +329,11 @@ defmodule Pleroma.Notification do
 
   @doc """
   Returns a tuple with 2 elements:
-    {enabled notification receivers, currently disabled receivers (blocking / [thread] muting)}
+    {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
 
   NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
   """
+  @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
   def get_notified_from_activity(activity, local_only \\ true)
 
   def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
@@ -346,17 +346,14 @@ defmodule Pleroma.Notification do
       |> Utils.maybe_notify_followers(activity)
       |> Enum.uniq()
 
-    # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs
+    potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only)
+
     notification_enabled_ap_ids =
       potential_receiver_ap_ids
+      |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
       |> exclude_relationship_restricted_ap_ids(activity)
       |> exclude_thread_muter_ap_ids(activity)
 
-    potential_receivers =
-      potential_receiver_ap_ids
-      |> Enum.uniq()
-      |> User.get_users_from_set(local_only)
-
     notification_enabled_users =
       Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
 
@@ -365,6 +362,38 @@ defmodule Pleroma.Notification do
 
   def get_notified_from_activity(_, _local_only), do: {[], []}
 
+  @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
+  def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
+
+  def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
+
+  def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
+    activity_actor_domain = activity.actor && URI.parse(activity.actor).host
+
+    users =
+      ap_ids
+      |> Enum.map(fn ap_id ->
+        Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
+          User.get_cached_by_ap_id(ap_id)
+      end)
+      |> Enum.filter(& &1)
+
+    domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
+
+    domain_blocker_follower_ap_ids =
+      if Enum.any?(domain_blocker_ap_ids) do
+        activity
+        |> Activity.user_actor()
+        |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
+      else
+        []
+      end
+
+    ap_ids
+    |> Kernel.--(domain_blocker_ap_ids)
+    |> Kernel.++(domain_blocker_follower_ap_ids)
+  end
+
   @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
   def exclude_relationship_restricted_ap_ids([], _activity), do: []
 
index a7f53e3194f589b027c06d6003b8138e1f1075f3..6ad824c57e1a4e39db0f75bfca20194ba541052d 100644 (file)
@@ -669,6 +669,37 @@ defmodule Pleroma.NotificationTest do
       assert [other_user] == disabled_receivers
       refute other_user in enabled_receivers
     end
+
+    test "it returns non-following domain-blocking recipient in disabled recipients list" do
+      blocked_domain = "blocked.domain"
+      user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"})
+      other_user = insert(:user)
+
+      {:ok, other_user} = User.block_domain(other_user, blocked_domain)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}!"})
+
+      {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
+
+      assert [] == enabled_receivers
+      assert [other_user] == disabled_receivers
+    end
+
+    test "it returns following domain-blocking recipient in enabled recipients list" do
+      blocked_domain = "blocked.domain"
+      user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"})
+      other_user = insert(:user)
+
+      {:ok, other_user} = User.block_domain(other_user, blocked_domain)
+      {:ok, other_user} = User.follow(other_user, user)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}!"})
+
+      {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
+
+      assert [other_user] == enabled_receivers
+      assert [] == disabled_receivers
+    end
   end
 
   describe "notification lifecycle" do
@@ -931,7 +962,7 @@ defmodule Pleroma.NotificationTest do
       assert Notification.for_user(user) == []
     end
 
-    test "it doesn't return notifications for blocked domain" do
+    test "it doesn't return notifications for domain-blocked non-followed user" do
       user = insert(:user)
       blocked = insert(:user, ap_id: "http://some-domain.com")
       {:ok, user} = User.block_domain(user, "some-domain.com")
@@ -941,6 +972,18 @@ defmodule Pleroma.NotificationTest do
       assert Notification.for_user(user) == []
     end
 
+    test "it returns notifications for domain-blocked but followed user" do
+      user = insert(:user)
+      blocked = insert(:user, ap_id: "http://some-domain.com")
+
+      {:ok, user} = User.block_domain(user, "some-domain.com")
+      {:ok, _} = User.follow(user, blocked)
+
+      {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
+
+      assert length(Notification.for_user(user)) == 1
+    end
+
     test "it doesn't return notifications for muted thread" do
       user = insert(:user)
       another_user = insert(:user)
@@ -971,7 +1014,8 @@ defmodule Pleroma.NotificationTest do
       assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
     end
 
-    test "it doesn't return notifications from a domain-blocked user when with_muted is set" do
+    test "when with_muted is set, " <>
+           "it doesn't return notifications from a domain-blocked non-followed user" do
       user = insert(:user)
       blocked = insert(:user, ap_id: "http://some-domain.com")
       {:ok, user} = User.block_domain(user, "some-domain.com")