Merge branch 'feature/mrf-anti-link-spam' into 'develop'
implement anti link spam MRF See merge request pleroma/pleroma!1307
This commit is contained in:
commit
55742d978d
4 changed files with 190 additions and 0 deletions
|
@ -62,6 +62,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- MRF: Support for running subchains.
|
- MRF: Support for running subchains.
|
||||||
- Configuration: `skip_thread_containment` option
|
- Configuration: `skip_thread_containment` option
|
||||||
- Configuration: `rate_limit` option. See `Pleroma.Plugs.RateLimiter` documentation for details.
|
- 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
|
### Changed
|
||||||
- **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
|
- **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
|
||||||
|
|
|
@ -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.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.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.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.
|
* `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.
|
* `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``
|
* `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json``
|
||||||
|
|
48
lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
Normal file
48
lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
Normal file
|
@ -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
|
140
test/web/activity_pub/mrf/anti_link_spam_policy_test.exs
Normal file
140
test/web/activity_pub/mrf/anti_link_spam_policy_test.exs
Normal file
|
@ -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
|
Loading…
Reference in a new issue