Merge branch 'feature/mrf-anti-link-spam' into 'develop'
authorkaniini <nenolod@gmail.com>
Fri, 21 Jun 2019 22:56:54 +0000 (22:56 +0000)
committerkaniini <nenolod@gmail.com>
Fri, 21 Jun 2019 22:56:54 +0000 (22:56 +0000)
implement anti link spam MRF

See merge request pleroma/pleroma!1307

CHANGELOG.md
docs/config.md
lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex [new file with mode: 0644]
test/web/activity_pub/mrf/anti_link_spam_policy_test.exs [new file with mode: 0644]

index 5b7e5c9a14cd5af3ceace47975d001e4ea6aca5e..0dc8b547d755bbefe05498c6e5028b01aafecd48 100644 (file)
@@ -62,6 +62,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - MRF: Support for running subchains.
 - Configuration: `skip_thread_containment` option
 - Configuration: `rate_limit` option. See `Pleroma.Plugs.RateLimiter` documentation for details.
+- MRF: Support for filtering out likely spam messages by rejecting posts from new users that contain links.
 
 ### Changed
 - **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
index ed8e465c696044ff9b3a85966dc5ee992187d96b..b751935456e1334c833a982d8b050f025e5b028f 100644 (file)
@@ -90,6 +90,7 @@ config :pleroma, Pleroma.Emails.Mailer,
   * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (see ``:mrf_subchain`` section)
   * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See ``:mrf_rejectnonpublic`` section)
   * `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
+  * `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.
 * `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
 * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
 * `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json``
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
new file mode 100644 (file)
index 0000000..2da3eac
--- /dev/null
@@ -0,0 +1,48 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
+  alias Pleroma.User
+
+  require Logger
+
+  # has the user successfully posted before?
+  defp old_user?(%User{} = u) do
+    u.info.note_count > 0 || u.info.follower_count > 0
+  end
+
+  # does the post contain links?
+  defp contains_links?(%{"content" => content} = _object) do
+    content
+    |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"],a.zrl")
+    |> Floki.attribute("a", "href")
+    |> length() > 0
+  end
+
+  defp contains_links?(_), do: false
+
+  def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
+    with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor),
+         {:contains_links, true} <- {:contains_links, contains_links?(object)},
+         {:old_user, true} <- {:old_user, old_user?(u)} do
+      {:ok, message}
+    else
+      {:contains_links, false} ->
+        {:ok, message}
+
+      {:old_user, false} ->
+        {:reject, nil}
+
+      {:error, _} ->
+        {:reject, nil}
+
+      e ->
+        Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}")
+        {:reject, nil}
+    end
+  end
+
+  # in all other cases, pass through
+  def filter(message), do: {:ok, message}
+end
diff --git a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs
new file mode 100644 (file)
index 0000000..284c133
--- /dev/null
@@ -0,0 +1,140 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
+  use Pleroma.DataCase
+  import Pleroma.Factory
+
+  alias Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy
+
+  @linkless_message %{
+    "type" => "Create",
+    "object" => %{
+      "content" => "hi world!"
+    }
+  }
+
+  @linkful_message %{
+    "type" => "Create",
+    "object" => %{
+      "content" => "<a href='https://example.com'>hi world!</a>"
+    }
+  }
+
+  @response_message %{
+    "type" => "Create",
+    "object" => %{
+      "name" => "yes",
+      "type" => "Answer"
+    }
+  }
+
+  describe "with new user" do
+    test "it allows posts without links" do
+      user = insert(:user)
+
+      assert user.info.note_count == 0
+
+      message =
+        @linkless_message
+        |> Map.put("actor", user.ap_id)
+
+      {:ok, _message} = AntiLinkSpamPolicy.filter(message)
+    end
+
+    test "it disallows posts with links" do
+      user = insert(:user)
+
+      assert user.info.note_count == 0
+
+      message =
+        @linkful_message
+        |> Map.put("actor", user.ap_id)
+
+      {:reject, _} = AntiLinkSpamPolicy.filter(message)
+    end
+  end
+
+  describe "with old user" do
+    test "it allows posts without links" do
+      user = insert(:user, info: %{note_count: 1})
+
+      assert user.info.note_count == 1
+
+      message =
+        @linkless_message
+        |> Map.put("actor", user.ap_id)
+
+      {:ok, _message} = AntiLinkSpamPolicy.filter(message)
+    end
+
+    test "it allows posts with links" do
+      user = insert(:user, info: %{note_count: 1})
+
+      assert user.info.note_count == 1
+
+      message =
+        @linkful_message
+        |> Map.put("actor", user.ap_id)
+
+      {:ok, _message} = AntiLinkSpamPolicy.filter(message)
+    end
+  end
+
+  describe "with followed new user" do
+    test "it allows posts without links" do
+      user = insert(:user, info: %{follower_count: 1})
+
+      assert user.info.follower_count == 1
+
+      message =
+        @linkless_message
+        |> Map.put("actor", user.ap_id)
+
+      {:ok, _message} = AntiLinkSpamPolicy.filter(message)
+    end
+
+    test "it allows posts with links" do
+      user = insert(:user, info: %{follower_count: 1})
+
+      assert user.info.follower_count == 1
+
+      message =
+        @linkful_message
+        |> Map.put("actor", user.ap_id)
+
+      {:ok, _message} = AntiLinkSpamPolicy.filter(message)
+    end
+  end
+
+  describe "with unknown actors" do
+    test "it rejects posts without links" do
+      message =
+        @linkless_message
+        |> Map.put("actor", "http://invalid.actor")
+
+      {:reject, _} = AntiLinkSpamPolicy.filter(message)
+    end
+
+    test "it rejects posts with links" do
+      message =
+        @linkful_message
+        |> Map.put("actor", "http://invalid.actor")
+
+      {:reject, _} = AntiLinkSpamPolicy.filter(message)
+    end
+  end
+
+  describe "with contentless-objects" do
+    test "it does not reject them or error out" do
+      user = insert(:user, info: %{note_count: 1})
+
+      message =
+        @response_message
+        |> Map.put("actor", user.ap_id)
+
+      {:ok, _message} = AntiLinkSpamPolicy.filter(message)
+    end
+  end
+end