Restrict statuses that contain user's irreversible filters
authorSergey Suprunenko <suprunenko.s@gmail.com>
Sat, 16 Nov 2019 21:54:13 +0000 (22:54 +0100)
committerAlexander Strizhakov <alex.strizhakov@gmail.com>
Mon, 6 Jul 2020 06:28:21 +0000 (09:28 +0300)
lib/pleroma/filter.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
test/filter_test.exs
test/support/factory.ex
test/web/activity_pub/activity_pub_test.exs

index 4d61b36502911679f4a8e9146af4ff761bdab726..91884c6b38b17de25374320db52e001f612a4eb4 100644 (file)
@@ -34,10 +34,18 @@ defmodule Pleroma.Filter do
     Repo.one(query)
   end
 
-  def get_filters(%User{id: user_id} = _user) do
+  def get_active(query) do
+    from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now())
+  end
+
+  def get_irreversible(query) do
+    from(f in query, where: f.hide)
+  end
+
+  def get_by_user(query, %User{id: user_id} = _user) do
     query =
       from(
-        f in Pleroma.Filter,
+        f in query,
         where: f.user_id == ^user_id,
         order_by: [desc: :id]
       )
@@ -95,4 +103,34 @@ defmodule Pleroma.Filter do
     |> validate_required([:phrase, :context])
     |> Repo.update()
   end
+
+  def compose_regex(user_or_filters, format \\ :postgres)
+
+  def compose_regex(%User{} = user, format) do
+    __MODULE__
+    |> get_active()
+    |> get_irreversible()
+    |> get_by_user(user)
+    |> compose_regex(format)
+  end
+
+  def compose_regex([_ | _] = filters, format) do
+    phrases =
+      filters
+      |> Enum.map(& &1.phrase)
+      |> Enum.join("|")
+
+    case format do
+      :postgres ->
+        "\\y(#{phrases})\\y"
+
+      :re ->
+        ~r/\b#{phrases}\b/i
+
+      _ ->
+        nil
+    end
+  end
+
+  def compose_regex(_, _), do: nil
 end
index 94117202c25a003d6db769b2be48c159e9a17d56..31353c86650b24fa84519184c6c28d225175b22c 100644 (file)
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   alias Pleroma.Constants
   alias Pleroma.Conversation
   alias Pleroma.Conversation.Participation
+  alias Pleroma.Filter
   alias Pleroma.Maps
   alias Pleroma.Notification
   alias Pleroma.Object
@@ -961,6 +962,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_instance(query, _), do: query
 
+  defp restrict_filtered(query, %{user: %User{} = user}) do
+    case Filter.compose_regex(user) do
+      nil ->
+        query
+
+      regex ->
+        from([activity, object] in query,
+          where:
+            fragment("not(?->>'content' ~* ?)", object.data, ^regex) or
+              activity.actor == ^user.ap_id
+        )
+    end
+  end
+
+  defp restrict_filtered(query, %{blocking_user: %User{} = user}) do
+    restrict_filtered(query, %{user: user})
+  end
+
+  defp restrict_filtered(query, _), do: query
+
   defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
 
   defp exclude_poll_votes(query, _) do
@@ -1099,6 +1120,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> restrict_muted_reblogs(restrict_muted_reblogs_opts)
     |> restrict_instance(opts)
     |> restrict_announce_object_actor(opts)
+    |> restrict_filtered(opts)
     |> Activity.restrict_deactivated_users()
     |> exclude_poll_votes(opts)
     |> exclude_chat_messages(opts)
index abbf0ce02e9c0f50085f682ecca6e4e19f231e79..db1ff31898e2d400f6fa1b478b98c8b51071a092 100644 (file)
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
 
   @doc "GET /api/v1/filters"
   def index(%{assigns: %{user: user}} = conn, _) do
-    filters = Filter.get_filters(user)
+    filters = Filter.get_by_user(Filter, user)
 
     render(conn, "index.json", filters: filters)
   end
index 63a30c736d0c6aed72cb29eb54c68e580cda6d40..061a95ad0619a4e046e86a7c06eca47671bc4bad 100644 (file)
@@ -126,7 +126,7 @@ defmodule Pleroma.FilterTest do
 
     {:ok, filter_one} = Pleroma.Filter.create(query_one)
     {:ok, filter_two} = Pleroma.Filter.create(query_two)
-    filters = Pleroma.Filter.get_filters(user)
+    filters = Pleroma.Filter.get_by_user(Pleroma.Filter, user)
     assert filter_one in filters
     assert filter_two in filters
   end
index af580021c9ba46ecd16867ee047ca84ddd05909b..635d83650b9fec2525ce305dcaeb85cf9f0ab96a 100644 (file)
@@ -428,4 +428,12 @@ defmodule Pleroma.Factory do
       user: build(:user)
     }
   end
+
+  def filter_factory do
+    %Pleroma.Filter{
+      user: build(:user),
+      filter_id: sequence(:filter_id, & &1),
+      phrase: "cofe"
+    }
+  end
 end
index 575e0c5db0b6b19b3acebc3d85500b6b365c00b5..4968403dc0fea27b7a97b11639736259ea1b4af9 100644 (file)
@@ -785,6 +785,75 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     assert activity == expected_activity
   end
 
+  describe "irreversible filters" do
+    setup do
+      user = insert(:user)
+      user_two = insert(:user)
+
+      insert(:filter, user: user_two, phrase: "cofe", hide: true)
+      insert(:filter, user: user_two, phrase: "ok boomer", hide: true)
+      insert(:filter, user: user_two, phrase: "test", hide: false)
+
+      params = %{
+        "type" => ["Create", "Announce"],
+        "user" => user_two
+      }
+
+      {:ok, %{user: user, user_two: user_two, params: params}}
+    end
+
+    test "it returns statuses if they don't contain exact filter words", %{
+      user: user,
+      params: params
+    } do
+      {:ok, _} = CommonAPI.post(user, %{"status" => "hey"})
+      {:ok, _} = CommonAPI.post(user, %{"status" => "got cofefe?"})
+      {:ok, _} = CommonAPI.post(user, %{"status" => "I am not a boomer"})
+      {:ok, _} = CommonAPI.post(user, %{"status" => "ok boomers"})
+      {:ok, _} = CommonAPI.post(user, %{"status" => "ccofee is not a word"})
+      {:ok, _} = CommonAPI.post(user, %{"status" => "this is a test"})
+
+      activities = ActivityPub.fetch_activities([], params)
+
+      assert Enum.count(activities) == 6
+    end
+
+    test "it does not filter user's own statuses", %{user_two: user_two, params: params} do
+      {:ok, _} = CommonAPI.post(user_two, %{"status" => "Give me some cofe!"})
+      {:ok, _} = CommonAPI.post(user_two, %{"status" => "ok boomer"})
+
+      activities = ActivityPub.fetch_activities([], params)
+
+      assert Enum.count(activities) == 2
+    end
+
+    test "it excludes statuses with filter words", %{user: user, params: params} do
+      {:ok, _} = CommonAPI.post(user, %{"status" => "Give me some cofe!"})
+      {:ok, _} = CommonAPI.post(user, %{"status" => "ok boomer"})
+      {:ok, _} = CommonAPI.post(user, %{"status" => "is it a cOfE?"})
+      {:ok, _} = CommonAPI.post(user, %{"status" => "cofe is all I need"})
+      {:ok, _} = CommonAPI.post(user, %{"status" => "— ok BOOMER\n"})
+
+      activities = ActivityPub.fetch_activities([], params)
+
+      assert Enum.empty?(activities)
+    end
+
+    test "it returns all statuses if user does not have any filters" do
+      another_user = insert(:user)
+      {:ok, _} = CommonAPI.post(another_user, %{"status" => "got cofe?"})
+      {:ok, _} = CommonAPI.post(another_user, %{"status" => "test!"})
+
+      activities =
+        ActivityPub.fetch_activities([], %{
+          "type" => ["Create", "Announce"],
+          "user" => another_user
+        })
+
+      assert Enum.count(activities) == 2
+    end
+  end
+
   describe "public fetch activities" do
     test "doesn't retrieve unlisted activities" do
       user = insert(:user)