Remove sensitive-property setting #nsfw, create HashtagPolicy
authorHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Mon, 28 Dec 2020 22:21:53 +0000 (23:21 +0100)
committerHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Sat, 27 Feb 2021 20:26:17 +0000 (21:26 +0100)
15 files changed:
CHANGELOG.md
config/config.exs
docs/configuration/cheatsheet.md
lib/pleroma/web/activity_pub/mrf.ex
lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/mrf/simple_policy.ex
lib/pleroma/web/activity_pub/mrf/tag_policy.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/common_api/activity_draft.ex
lib/pleroma/web/common_api/utils.ex
test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs [new file with mode: 0644]
test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
test/pleroma/web/activity_pub/mrf/tag_policy_test.exs
test/pleroma/web/activity_pub/mrf_test.exs
test/pleroma/web/activity_pub/transmogrifier_test.exs

index a7b5f6ac055257bd57524952e5084e340687d65e..52fdcb93201484ebf02b1308d5c8892780721272 100644 (file)
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 - **Breaking**: Changed `mix pleroma.user toggle_confirmed` to `mix pleroma.user confirm`
 - **Breaking**: Changed `mix pleroma.user toggle_activated` to `mix pleroma.user activate/deactivate`
+- **Breaking:** NSFW hashtag is no longer added on sensitive posts
 - Polls now always return a `voters_count`, even if they are single-choice.
 - Admin Emails: The ap id is used as the user link in emails now.
 - Improved registration workflow for email confirmation and account approval modes.
@@ -489,7 +490,6 @@ switched to a new configuration mechanism, however it was not officially removed
 - Static-FE: Fix remote posts not being sanitized
 
 ### Fixed
-=======
 - Rate limiter crashes when there is no explicitly specified ip in the config
 - 500 errors when no `Accept` header is present if Static-FE is enabled
 - Instance panel not being updated immediately due to wrong `Cache-Control` headers
index c371c397ce966bf46f7f498f03a741d04996c82a..97e440fee2cf091185546ddab566261b8f54ad2a 100644 (file)
@@ -391,6 +391,11 @@ config :pleroma, :mrf_keyword,
   federated_timeline_removal: [],
   replace: []
 
+config :pleroma, :mrf_hashtag,
+  sensitive: ["nsfw"],
+  reject: [],
+  federated_timeline_removal: []
+
 config :pleroma, :mrf_subchain, match_actor: %{}
 
 config :pleroma, :mrf_activity_expiration, days: 365
index 6a1031f15077869b78893c656d43f1f451c072c4..f3eee3e67c1cf84d127b8dacc1d3cee619cb28a6 100644 (file)
@@ -210,6 +210,16 @@ config :pleroma, :mrf_user_allowlist, %{
 
 * `days`: Default global expiration time for all local Create activities (in days)
 
+#### :mrf_hashtag
+
+* `sensitive`: List of hashtags to mark activities as sensitive (default: `nsfw`)
+* `federated_timeline_removal`: List of hashtags to remove activities from the federated timeline (aka TWNK)
+* `reject`: List of hashtags to reject activities from
+
+Notes:
+- The hashtags in the configuration do not have a leading `#`.
+- This MRF Policy is always enabled, if you want to disable it you have to set empty lists
+
 ### :activitypub
 * `unfollow_blocked`: Whether blocks result in people getting unfollowed
 * `outgoing_blocks`: Whether to federate blocks to other instances
index ef5a09a937be2386feaadbc80c48c26345d8715a..f2fec3ff61fa37a45149fa1de14b09b8bd376b51 100644 (file)
@@ -92,7 +92,9 @@ defmodule Pleroma.Web.ActivityPub.MRF do
   end
 
   def get_policies do
-    Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
+    Pleroma.Config.get([:mrf, :policies], [])
+    |> get_policies()
+    |> Enum.concat([Pleroma.Web.ActivityPub.MRF.HashtagPolicy])
   end
 
   defp get_policies(policy) when is_atom(policy), do: [policy]
diff --git a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
new file mode 100644 (file)
index 0000000..def0c43
--- /dev/null
@@ -0,0 +1,116 @@
+# 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.HashtagPolicy do
+  require Pleroma.Constants
+
+  alias Pleroma.Config
+  alias Pleroma.Object
+
+  @moduledoc """
+  Reject, TWKN-remove or Set-Sensitive messsages with specific hashtags (without the leading #)
+
+  Note: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.
+  """
+
+  @behaviour Pleroma.Web.ActivityPub.MRF
+
+  defp check_reject(message, hashtags) do
+    if Enum.any?(Config.get([:mrf_hashtag, :reject]), fn match -> match in hashtags end) do
+      {:reject, "[HashtagPolicy] Matches with rejected keyword"}
+    else
+      {:ok, message}
+    end
+  end
+
+  defp check_ftl_removal(%{"to" => to} = message, hashtags) do
+    if Pleroma.Constants.as_public() in to and
+         Enum.any?(Config.get([:mrf_hashtag, :federated_timeline_removal]), fn match ->
+           match in hashtags
+         end) do
+      to = List.delete(to, Pleroma.Constants.as_public())
+      cc = [Pleroma.Constants.as_public() | message["cc"] || []]
+
+      message =
+        message
+        |> Map.put("to", to)
+        |> Map.put("cc", cc)
+        |> Kernel.put_in(["object", "to"], to)
+        |> Kernel.put_in(["object", "cc"], cc)
+
+      {:ok, message}
+    else
+      {:ok, message}
+    end
+  end
+
+  defp check_ftl_removal(message, _hashtags), do: {:ok, message}
+
+  defp check_sensitive(message, hashtags) do
+    if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do
+      {:ok, Kernel.put_in(message, ["object", "sensitive"], true)}
+    else
+      {:ok, message}
+    end
+  end
+
+  @impl true
+  def filter(%{"type" => "Create", "object" => object} = message) do
+    hashtags = Object.hashtags(%Object{data: object})
+
+    if hashtags != [] do
+      with {:ok, message} <- check_reject(message, hashtags),
+           {:ok, message} <- check_ftl_removal(message, hashtags),
+           {:ok, message} <- check_sensitive(message, hashtags) do
+        {:ok, message}
+      end
+    else
+      {:ok, message}
+    end
+  end
+
+  @impl true
+  def filter(message), do: {:ok, message}
+
+  @impl true
+  def describe do
+    mrf_hashtag =
+      Config.get(:mrf_hashtag)
+      |> Enum.into(%{})
+
+    {:ok, %{mrf_hashtag: mrf_hashtag}}
+  end
+
+  @impl true
+  def config_description do
+    %{
+      key: :mrf_hashtag,
+      related_policy: "Pleroma.Web.ActivityPub.MRF.HashtagPolicy",
+      label: "MRF Hashtag",
+      description: @moduledoc,
+      children: [
+        %{
+          key: :reject,
+          type: {:list, :string},
+          description: "A list of hashtags which result in message being rejected.",
+          suggestions: ["foo"]
+        },
+        %{
+          key: :federated_timeline_removal,
+          type: {:list, :string},
+          description:
+            "A list of hashtags which result in message being removed from federated timelines (a.k.a unlisted).",
+          suggestions: ["foo"]
+        },
+        %{
+          key: :sensitive,
+          type: {:list, :string},
+          description:
+            "A list of hashtags which result in message being set as sensitive (a.k.a NSFW/R-18)",
+          suggestions: ["nsfw", "r18"]
+        }
+      ]
+    }
+  end
+end
index 0b1be8c517bcecc24d0e7cb105dc6966a686709c..62024c58c90929f9073ef72f00db9e2d0953ac5b 100644 (file)
@@ -64,22 +64,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
          %{host: actor_host} = _actor_info,
          %{
            "type" => "Create",
-           "object" => child_object
+           "object" => %{} = _child_object
          } = object
-       )
-       when is_map(child_object) do
+       ) do
     media_nsfw =
       Config.get([:mrf_simple, :media_nsfw])
       |> MRF.subdomains_regex()
 
     object =
       if MRF.subdomain_match?(media_nsfw, actor_host) do
-        child_object =
-          child_object
-          |> Map.put("tag", (child_object["tag"] || []) ++ ["nsfw"])
-          |> Map.put("sensitive", true)
-
-        Map.put(object, "object", child_object)
+        Kernel.put_in(object, ["object", "sensitive"], true)
       else
         object
       end
index 5739cee6319a3c0c2423a5406fb2b751222a0cd0..528093ac00e4ae78ee741246a6662f3583768c54 100644 (file)
@@ -28,20 +28,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
          "mrf_tag:media-force-nsfw",
          %{
            "type" => "Create",
-           "object" => %{"attachment" => child_attachment} = object
+           "object" => %{"attachment" => child_attachment}
          } = message
        )
        when length(child_attachment) > 0 do
-    tags = (object["tag"] || []) ++ ["nsfw"]
-
-    object =
-      object
-      |> Map.put("tag", tags)
-      |> Map.put("sensitive", true)
-
-    message = Map.put(message, "object", object)
-
-    {:ok, message}
+    {:ok, Kernel.put_in(message, ["object", "sensitive"], true)}
   end
 
   defp process_tag(
index 0a701334f00d08f7f57e2a8ac963e2e526414c2e..8c7d6a7478c28ada25aeb7baf55b9fed5c9d2471 100644 (file)
@@ -40,7 +40,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     |> fix_in_reply_to(options)
     |> fix_emoji()
     |> fix_tag()
-    |> set_sensitive()
     |> fix_content_map()
     |> fix_addressing()
     |> fix_summary()
@@ -741,7 +740,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   # Prepares the object of an outgoing create activity.
   def prepare_object(object) do
     object
-    |> set_sensitive
     |> add_hashtags
     |> add_mention_tags
     |> add_emoji_tags
@@ -932,15 +930,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     Map.put(object, "conversation", object["context"])
   end
 
-  def set_sensitive(%{"sensitive" => _} = object) do
-    object
-  end
-
-  def set_sensitive(object) do
-    tags = object["tag"] || []
-    Map.put(object, "sensitive", "nsfw" in tags)
-  end
-
   def set_type(%{"type" => "Answer"} = object) do
     Map.put(object, "type", "Note")
   end
index fb059c27cb61295c0b821616cdbaf2233d1382c1..da726a690209fd60a3fdb49f91c5362898dc5fd4 100644 (file)
@@ -179,7 +179,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
   end
 
   defp sensitive(draft) do
-    sensitive = draft.params[:sensitive] || Enum.member?(draft.tags, {"#nsfw", "nsfw"})
+    sensitive = draft.params[:sensitive]
     %__MODULE__{draft | sensitive: sensitive}
   end
 
index 9587dfa2511aed24214f528ef263f95de76dfa34..4e6a3feb04143ca7888029dafbf8f9b825ca4b17 100644 (file)
@@ -217,7 +217,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
     draft.status
     |> format_input(content_type, options)
     |> maybe_add_attachments(draft.attachments, attachment_links)
-    |> maybe_add_nsfw_tag(draft.params)
   end
 
   defp get_content_type(content_type) do
@@ -228,13 +227,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
     end
   end
 
-  defp maybe_add_nsfw_tag({text, mentions, tags}, %{"sensitive" => sensitive})
-       when sensitive in [true, "True", "true", "1"] do
-    {text, mentions, [{"#nsfw", "nsfw"} | tags]}
-  end
-
-  defp maybe_add_nsfw_tag(data, _), do: data
-
   def make_context(_, %Participation{} = participation) do
     Repo.preload(participation, :conversation).conversation.ap_id
   end
diff --git a/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs
new file mode 100644 (file)
index 0000000..13415bb
--- /dev/null
@@ -0,0 +1,31 @@
+# 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.HashtagPolicyTest do
+  use Oban.Testing, repo: Pleroma.Repo
+  use Pleroma.DataCase
+
+  alias Pleroma.Web.ActivityPub.Transmogrifier
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  test "it sets the sensitive property with relevant hashtags" do
+    user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"})
+    {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+
+    assert modified["object"]["sensitive"]
+  end
+
+  test "it doesn't sets the sensitive property with irrelevant hashtags" do
+    user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(user, %{status: "#cofe hey"})
+    {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+
+    refute modified["object"]["sensitive"]
+  end
+end
index f48e5b39bcebb22d59939c5a089fb83445aeb6aa..5c0aff26eb3bb6f1f714613cda67f8ea4b87d337 100644 (file)
@@ -75,10 +75,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
       local_message = build_local_message()
 
       assert SimplePolicy.filter(media_message) ==
-               {:ok,
-                media_message
-                |> put_in(["object", "tag"], ["foo", "nsfw"])
-                |> put_in(["object", "sensitive"], true)}
+               {:ok, put_in(media_message, ["object", "sensitive"], true)}
 
       assert SimplePolicy.filter(local_message) == {:ok, local_message}
     end
@@ -89,10 +86,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
       local_message = build_local_message()
 
       assert SimplePolicy.filter(media_message) ==
-               {:ok,
-                media_message
-                |> put_in(["object", "tag"], ["foo", "nsfw"])
-                |> put_in(["object", "sensitive"], true)}
+               {:ok, put_in(media_message, ["object", "sensitive"], true)}
 
       assert SimplePolicy.filter(local_message) == {:ok, local_message}
     end
index 66e98b7ee28eaec28f965c85d183d4f2a9c6f896..faaadff798fd706d7cee3c70a863fa7c7063f4af 100644 (file)
@@ -114,7 +114,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do
       except_message = %{
         "actor" => actor.ap_id,
         "type" => "Create",
-        "object" => %{"tag" => ["test", "nsfw"], "attachment" => ["file1"], "sensitive" => true}
+        "object" => %{"tag" => ["test"], "attachment" => ["file1"], "sensitive" => true}
       }
 
       assert TagPolicy.filter(message) == {:ok, except_message}
index 7c1eef7e0847fed73a82d138e26c89302dbd5ee5..61d308b978a20d16d7e8dd5905e2dcc81391a13a 100644 (file)
@@ -68,7 +68,12 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
       clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy])
 
       expected = %{
-        mrf_policies: ["NoOpPolicy"],
+        mrf_policies: ["NoOpPolicy", "HashtagPolicy"],
+        mrf_hashtag: %{
+          federated_timeline_removal: [],
+          reject: [],
+          sensitive: ["nsfw"]
+        },
         exclusions: false
       }
 
@@ -79,8 +84,13 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
       clear_config([:mrf, :policies], [MRFModuleMock])
 
       expected = %{
-        mrf_policies: ["MRFModuleMock"],
+        mrf_policies: ["MRFModuleMock", "HashtagPolicy"],
         mrf_module_mock: "some config data",
+        mrf_hashtag: %{
+          federated_timeline_removal: [],
+          reject: [],
+          sensitive: ["nsfw"]
+        },
         exclusions: false
       }
 
index 7c97fa8f880a3faf92aa130d1fee2a3865c1cb01..a7894a8d0ff8e1a2d39969b58485ef7267173778 100644 (file)
@@ -153,15 +153,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       end
     end
 
-    test "it adds the sensitive property" do
-      user = insert(:user)
-
-      {:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"})
-      {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
-
-      assert modified["object"]["sensitive"]
-    end
-
     test "it adds the json-ld context and the conversation property" do
       user = insert(:user)