Remove sensitive-property setting #nsfw, create HashtagPolicy

This commit is contained in:
Haelwenn (lanodan) Monnier 2020-12-28 23:21:53 +01:00
parent 998437d4a4
commit 3bc7d12271
No known key found for this signature in database
GPG Key ID: D5B7A8E43C997DEE
15 changed files with 187 additions and 62 deletions

View 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_confirmed` to `mix pleroma.user confirm`
- **Breaking**: Changed `mix pleroma.user toggle_activated` to `mix pleroma.user activate/deactivate` - **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. - 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. - Admin Emails: The ap id is used as the user link in emails now.
- Improved registration workflow for email confirmation and account approval modes. - 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 - Static-FE: Fix remote posts not being sanitized
### Fixed ### Fixed
=======
- Rate limiter crashes when there is no explicitly specified ip in the config - 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 - 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 - Instance panel not being updated immediately due to wrong `Cache-Control` headers

View File

@ -391,6 +391,11 @@
federated_timeline_removal: [], federated_timeline_removal: [],
replace: [] replace: []
config :pleroma, :mrf_hashtag,
sensitive: ["nsfw"],
reject: [],
federated_timeline_removal: []
config :pleroma, :mrf_subchain, match_actor: %{} config :pleroma, :mrf_subchain, match_actor: %{}
config :pleroma, :mrf_activity_expiration, days: 365 config :pleroma, :mrf_activity_expiration, days: 365

View File

@ -210,6 +210,16 @@ config :pleroma, :mrf_user_allowlist, %{
* `days`: Default global expiration time for all local Create activities (in days) * `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 ### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed * `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances * `outgoing_blocks`: Whether to federate blocks to other instances

View File

@ -92,7 +92,9 @@ def pipeline_filter(%{} = message, meta) do
end end
def get_policies do 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 end
defp get_policies(policy) when is_atom(policy), do: [policy] defp get_policies(policy) when is_atom(policy), do: [policy]

View File

@ -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

View File

@ -64,22 +64,16 @@ defp check_media_nsfw(
%{host: actor_host} = _actor_info, %{host: actor_host} = _actor_info,
%{ %{
"type" => "Create", "type" => "Create",
"object" => child_object "object" => %{} = _child_object
} = object } = object
) ) do
when is_map(child_object) do
media_nsfw = media_nsfw =
Config.get([:mrf_simple, :media_nsfw]) Config.get([:mrf_simple, :media_nsfw])
|> MRF.subdomains_regex() |> MRF.subdomains_regex()
object = object =
if MRF.subdomain_match?(media_nsfw, actor_host) do if MRF.subdomain_match?(media_nsfw, actor_host) do
child_object = Kernel.put_in(object, ["object", "sensitive"], true)
child_object
|> Map.put("tag", (child_object["tag"] || []) ++ ["nsfw"])
|> Map.put("sensitive", true)
Map.put(object, "object", child_object)
else else
object object
end end

View File

@ -28,20 +28,11 @@ defp process_tag(
"mrf_tag:media-force-nsfw", "mrf_tag:media-force-nsfw",
%{ %{
"type" => "Create", "type" => "Create",
"object" => %{"attachment" => child_attachment} = object "object" => %{"attachment" => child_attachment}
} = message } = message
) )
when length(child_attachment) > 0 do when length(child_attachment) > 0 do
tags = (object["tag"] || []) ++ ["nsfw"] {:ok, Kernel.put_in(message, ["object", "sensitive"], true)}
object =
object
|> Map.put("tag", tags)
|> Map.put("sensitive", true)
message = Map.put(message, "object", object)
{:ok, message}
end end
defp process_tag( defp process_tag(

View File

@ -40,7 +40,6 @@ def fix_object(object, options \\ []) do
|> fix_in_reply_to(options) |> fix_in_reply_to(options)
|> fix_emoji() |> fix_emoji()
|> fix_tag() |> fix_tag()
|> set_sensitive()
|> fix_content_map() |> fix_content_map()
|> fix_addressing() |> fix_addressing()
|> fix_summary() |> fix_summary()
@ -741,7 +740,6 @@ def replies(_), do: []
# Prepares the object of an outgoing create activity. # Prepares the object of an outgoing create activity.
def prepare_object(object) do def prepare_object(object) do
object object
|> set_sensitive
|> add_hashtags |> add_hashtags
|> add_mention_tags |> add_mention_tags
|> add_emoji_tags |> add_emoji_tags
@ -932,15 +930,6 @@ def set_conversation(object) do
Map.put(object, "conversation", object["context"]) Map.put(object, "conversation", object["context"])
end 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 def set_type(%{"type" => "Answer"} = object) do
Map.put(object, "type", "Note") Map.put(object, "type", "Note")
end end

View File

@ -179,7 +179,7 @@ defp context(draft) do
end end
defp sensitive(draft) do defp sensitive(draft) do
sensitive = draft.params[:sensitive] || Enum.member?(draft.tags, {"#nsfw", "nsfw"}) sensitive = draft.params[:sensitive]
%__MODULE__{draft | sensitive: sensitive} %__MODULE__{draft | sensitive: sensitive}
end end

View File

@ -217,7 +217,6 @@ def make_content_html(%ActivityDraft{} = draft) do
draft.status draft.status
|> format_input(content_type, options) |> format_input(content_type, options)
|> maybe_add_attachments(draft.attachments, attachment_links) |> maybe_add_attachments(draft.attachments, attachment_links)
|> maybe_add_nsfw_tag(draft.params)
end end
defp get_content_type(content_type) do defp get_content_type(content_type) do
@ -228,13 +227,6 @@ defp get_content_type(content_type) do
end end
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 def make_context(_, %Participation{} = participation) do
Repo.preload(participation, :conversation).conversation.ap_id Repo.preload(participation, :conversation).conversation.ap_id
end end

View File

@ -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

View File

@ -75,10 +75,7 @@ test "has a matching host" do
local_message = build_local_message() local_message = build_local_message()
assert SimplePolicy.filter(media_message) == assert SimplePolicy.filter(media_message) ==
{:ok, {:ok, put_in(media_message, ["object", "sensitive"], true)}
media_message
|> put_in(["object", "tag"], ["foo", "nsfw"])
|> put_in(["object", "sensitive"], true)}
assert SimplePolicy.filter(local_message) == {:ok, local_message} assert SimplePolicy.filter(local_message) == {:ok, local_message}
end end
@ -89,10 +86,7 @@ test "match with wildcard domain" do
local_message = build_local_message() local_message = build_local_message()
assert SimplePolicy.filter(media_message) == assert SimplePolicy.filter(media_message) ==
{:ok, {:ok, put_in(media_message, ["object", "sensitive"], true)}
media_message
|> put_in(["object", "tag"], ["foo", "nsfw"])
|> put_in(["object", "sensitive"], true)}
assert SimplePolicy.filter(local_message) == {:ok, local_message} assert SimplePolicy.filter(local_message) == {:ok, local_message}
end end

View File

@ -114,7 +114,7 @@ test "Mark as sensitive on presence of attachments" do
except_message = %{ except_message = %{
"actor" => actor.ap_id, "actor" => actor.ap_id,
"type" => "Create", "type" => "Create",
"object" => %{"tag" => ["test", "nsfw"], "attachment" => ["file1"], "sensitive" => true} "object" => %{"tag" => ["test"], "attachment" => ["file1"], "sensitive" => true}
} }
assert TagPolicy.filter(message) == {:ok, except_message} assert TagPolicy.filter(message) == {:ok, except_message}

View File

@ -68,7 +68,12 @@ test "it works as expected with noop policy" do
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy]) clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy])
expected = %{ expected = %{
mrf_policies: ["NoOpPolicy"], mrf_policies: ["NoOpPolicy", "HashtagPolicy"],
mrf_hashtag: %{
federated_timeline_removal: [],
reject: [],
sensitive: ["nsfw"]
},
exclusions: false exclusions: false
} }
@ -79,8 +84,13 @@ test "it works as expected with mock policy" do
clear_config([:mrf, :policies], [MRFModuleMock]) clear_config([:mrf, :policies], [MRFModuleMock])
expected = %{ expected = %{
mrf_policies: ["MRFModuleMock"], mrf_policies: ["MRFModuleMock", "HashtagPolicy"],
mrf_module_mock: "some config data", mrf_module_mock: "some config data",
mrf_hashtag: %{
federated_timeline_removal: [],
reject: [],
sensitive: ["nsfw"]
},
exclusions: false exclusions: false
} }

View File

@ -153,15 +153,6 @@ test "it turns mentions into tags" do
end end
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 test "it adds the json-ld context and the conversation property" do
user = insert(:user) user = insert(:user)