diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87eb7b48f..a1fa22398 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,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.
@@ -506,7 +507,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
diff --git a/config/config.exs b/config/config.exs
index 23ada389b..8d1e17b42 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -391,6 +391,11 @@
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
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index d085f0f44..8f2c4347e 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -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
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index ef5a09a93..f2fec3ff6 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -92,7 +92,9 @@ def pipeline_filter(%{} = message, meta) 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
index 000000000..def0c437c
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
@@ -0,0 +1,116 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 0b1be8c51..62024c58c 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -64,22 +64,16 @@ defp check_media_nsfw(
%{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
diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
index 5739cee63..528093ac0 100644
--- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
@@ -28,20 +28,11 @@ defp process_tag(
"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(
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 0a701334f..8c7d6a747 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -40,7 +40,6 @@ def fix_object(object, options \\ []) do
|> fix_in_reply_to(options)
|> fix_emoji()
|> fix_tag()
- |> set_sensitive()
|> fix_content_map()
|> fix_addressing()
|> fix_summary()
@@ -741,7 +740,6 @@ def replies(_), 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 @@ def set_conversation(object) 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
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 73f1b0931..8668b600e 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -179,7 +179,7 @@ defp context(draft) 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
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 9587dfa25..4e6a3feb0 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -217,7 +217,6 @@ def make_content_html(%ActivityDraft{} = draft) 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 @@ defp get_content_type(content_type) 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
index 000000000..13415bb79
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# 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
diff --git a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
index f48e5b39b..5c0aff26e 100644
--- a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
@@ -75,10 +75,7 @@ test "has a matching host" 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 @@ test "match with wildcard domain" 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
diff --git a/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs
index 66e98b7ee..faaadff79 100644
--- a/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs
@@ -114,7 +114,7 @@ test "Mark as sensitive on presence of attachments" 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}
diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs
index 7c1eef7e0..61d308b97 100644
--- a/test/pleroma/web/activity_pub/mrf_test.exs
+++ b/test/pleroma/web/activity_pub/mrf_test.exs
@@ -68,7 +68,12 @@ test "it works as expected with noop policy" 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 @@ test "it works as expected with mock policy" 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
}
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index 211e535a5..4c3fcb44a 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -153,15 +153,6 @@ test "it turns mentions into tags" 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)