MRF NoEmptyPolicy: Deny posts from local users if there is no content or only mentions.
authorMark Felder <feld@feld.me>
Mon, 8 Feb 2021 21:32:47 +0000 (15:32 -0600)
committerMark Felder <feld@feld.me>
Mon, 8 Feb 2021 21:32:47 +0000 (15:32 -0600)
Helps prevent accidental button mashes from submitting incomplete posts

CHANGELOG.md
lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex [new file with mode: 0644]
test/pleroma/web/activity_pub/mrf/no_empty_policy_test.exs [new file with mode: 0644]

index 1dbdb3f4e04175052b9396185b8718742a49afbc..d4acbc9a587ccadd7a9a9fa53ef5f9e235c24446 100644 (file)
@@ -49,6 +49,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Ability to set ActivityPub aliases for follower migration.
 - Configurable background job limits for RichMedia (link previews) and MediaProxyWarmingPolicy
 - Ability to define custom HTTP headers per each frontend
+- MRF (`NoEmptyPolicy`): New MRF Policy which will deny empty statuses or statuses of only mentions from being created by local users
 
 <details>
   <summary>API Changes</summary>
diff --git a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex
new file mode 100644 (file)
index 0000000..32bb1b6
--- /dev/null
@@ -0,0 +1,61 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
+  @moduledoc "Filter local activities which have no content"
+  @behaviour Pleroma.Web.ActivityPub.MRF
+
+  alias Pleroma.Web
+
+  @impl true
+  def filter(%{"actor" => actor} = object) do
+    with true <- is_local?(actor),
+         true <- is_note?(object),
+         false <- has_attachment?(object),
+         true <- only_mentions?(object) do
+      {:reject, "[NoEmptyPolicy]"}
+    else
+      _ ->
+        {:ok, object}
+    end
+  end
+
+  def filter(object), do: {:ok, object}
+
+  defp is_local?(actor) do
+    if actor |> String.starts_with?("#{Web.base_url()}") do
+      true
+    else
+      false
+    end
+  end
+
+  defp has_attachment?(%{
+         "type" => "Create",
+         "object" => %{"type" => "Note", "attachment" => attachments}
+       })
+       when length(attachments) > 0,
+       do: true
+
+  defp has_attachment?(_), do: false
+
+  defp only_mentions?(%{"type" => "Create", "object" => %{"type" => "Note", "source" => source}}) do
+    non_mentions =
+      source |> String.split() |> Enum.filter(&(not String.starts_with?(&1, "@"))) |> length
+
+    if non_mentions > 0 do
+      false
+    else
+      true
+    end
+  end
+
+  defp only_mentions?(_), do: false
+
+  defp is_note?(%{"type" => "Create", "object" => %{"type" => "Note"}}), do: true
+  defp is_note?(_), do: false
+
+  @impl true
+  def describe, do: {:ok, %{}}
+end
diff --git a/test/pleroma/web/activity_pub/mrf/no_empty_policy_test.exs b/test/pleroma/web/activity_pub/mrf/no_empty_policy_test.exs
new file mode 100644 (file)
index 0000000..fbcf684
--- /dev/null
@@ -0,0 +1,154 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicyTest do
+  use Pleroma.DataCase
+  alias Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy
+
+  setup_all do: clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy])
+
+  test "Notes with content are exempt" do
+    message = %{
+      "actor" => "http://localhost:4001/users/testuser",
+      "cc" => ["http://localhost:4001/users/testuser/followers"],
+      "object" => %{
+        "actor" => "http://localhost:4001/users/testuser",
+        "attachment" => [],
+        "cc" => ["http://localhost:4001/users/testuser/followers"],
+        "source" => "this is a test post",
+        "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+        "type" => "Note"
+      },
+      "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+      "type" => "Create"
+    }
+
+    assert NoEmptyPolicy.filter(message) == {:ok, message}
+  end
+
+  test "Polls are exempt" do
+    message = %{
+      "actor" => "http://localhost:4001/users/testuser",
+      "cc" => ["http://localhost:4001/users/testuser/followers"],
+      "object" => %{
+        "actor" => "http://localhost:4001/users/testuser",
+        "attachment" => [],
+        "cc" => ["http://localhost:4001/users/testuser/followers"],
+        "oneOf" => [
+          %{
+            "name" => "chocolate",
+            "replies" => %{"totalItems" => 0, "type" => "Collection"},
+            "type" => "Note"
+          },
+          %{
+            "name" => "vanilla",
+            "replies" => %{"totalItems" => 0, "type" => "Collection"},
+            "type" => "Note"
+          }
+        ],
+        "source" => "@user2",
+        "to" => [
+          "https://www.w3.org/ns/activitystreams#Public",
+          "http://localhost:4001/users/user2"
+        ],
+        "type" => "Question"
+      },
+      "to" => [
+        "https://www.w3.org/ns/activitystreams#Public",
+        "http://localhost:4001/users/user2"
+      ],
+      "type" => "Create"
+    }
+
+    assert NoEmptyPolicy.filter(message) == {:ok, message}
+  end
+
+  test "Notes with attachments are exempt" do
+    message = %{
+      "actor" => "http://localhost:4001/users/testuser",
+      "cc" => ["http://localhost:4001/users/testuser/followers"],
+      "object" => %{
+        "actor" => "http://localhost:4001/users/testuser",
+        "attachment" => [
+          %{
+            "actor" => "http://localhost:4001/users/testuser",
+            "mediaType" => "image/png",
+            "name" => "",
+            "type" => "Document",
+            "url" => [
+              %{
+                "href" =>
+                  "http://localhost:4001/media/68ba231cf12e1382ce458f1979969f8ed5cc07ba198a02e653464abaf39bdb90.png",
+                "mediaType" => "image/png",
+                "type" => "Link"
+              }
+            ]
+          }
+        ],
+        "cc" => ["http://localhost:4001/users/testuser/followers"],
+        "source" => "@user2",
+        "to" => [
+          "https://www.w3.org/ns/activitystreams#Public",
+          "http://localhost:4001/users/user2"
+        ],
+        "type" => "Note"
+      },
+      "to" => [
+        "https://www.w3.org/ns/activitystreams#Public",
+        "http://localhost:4001/users/user2"
+      ],
+      "type" => "Create"
+    }
+
+    assert NoEmptyPolicy.filter(message) == {:ok, message}
+  end
+
+  test "Notes with only mentions are denied" do
+    message = %{
+      "actor" => "http://localhost:4001/users/testuser",
+      "cc" => ["http://localhost:4001/users/testuser/followers"],
+      "object" => %{
+        "actor" => "http://localhost:4001/users/testuser",
+        "attachment" => [],
+        "cc" => ["http://localhost:4001/users/testuser/followers"],
+        "source" => "@user2",
+        "to" => [
+          "https://www.w3.org/ns/activitystreams#Public",
+          "http://localhost:4001/users/user2"
+        ],
+        "type" => "Note"
+      },
+      "to" => [
+        "https://www.w3.org/ns/activitystreams#Public",
+        "http://localhost:4001/users/user2"
+      ],
+      "type" => "Create"
+    }
+
+    assert NoEmptyPolicy.filter(message) == {:reject, "[NoEmptyPolicy]"}
+  end
+
+  test "Notes with no content are denied" do
+    message = %{
+      "actor" => "http://localhost:4001/users/testuser",
+      "cc" => ["http://localhost:4001/users/testuser/followers"],
+      "object" => %{
+        "actor" => "http://localhost:4001/users/testuser",
+        "attachment" => [],
+        "cc" => ["http://localhost:4001/users/testuser/followers"],
+        "source" => "",
+        "to" => [
+          "https://www.w3.org/ns/activitystreams#Public"
+        ],
+        "type" => "Note"
+      },
+      "to" => [
+        "https://www.w3.org/ns/activitystreams#Public"
+      ],
+      "type" => "Create"
+    }
+
+    assert NoEmptyPolicy.filter(message) == {:reject, "[NoEmptyPolicy]"}
+  end
+end