Remove sensitive-property setting #nsfw, create HashtagPolicy
This commit is contained in:
parent
998437d4a4
commit
3bc7d12271
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
116
lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
Normal file
116
lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
31
test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs
Normal file
31
test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue