Draft: MRF to automate CW/subject based on post content See merge request pleroma/pleroma!3130merge-requests/3130/merge
@@ -400,6 +400,8 @@ config :pleroma, :mrf_vocabulary, | |||
accept: [], | |||
reject: [] | |||
config :pleroma, :mrf_auto_subject, match: [] | |||
# threshold of 7 days | |||
config :pleroma, :mrf_object_age, | |||
threshold: 604_800, | |||
@@ -0,0 +1,124 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.ActivityPub.MRF.AutoSubjectPolicy do | |||
@moduledoc "Apply Subject to local posts matching certain keywords." | |||
@behaviour Pleroma.Web.ActivityPub.MRF | |||
alias Pleroma.User | |||
require Pleroma.Constants | |||
require Logger | |||
@trim_regex Regex.compile!("[.?!:;]+$") | |||
@impl true | |||
def filter(%{"type" => "Create", "actor" => actor, "object" => _object} = message) do | |||
with {:ok, %User{local: true}} <- User.get_or_fetch_by_ap_id(actor), | |||
{:ok, message} <- check_subject(message), | |||
{:ok, message} <- check_match(message) do | |||
{:ok, message} | |||
else | |||
{:ok, %User{local: false}} -> | |||
{:ok, message} | |||
{:error, :has_subject} -> | |||
{:ok, message} | |||
{:error, _} -> | |||
{:reject, "[AutoSubjectPolicy] Failed to get or fetch user by ap_id"} | |||
e -> | |||
{:reject, "[AutoSubjectPolicy] Unhandled error #{inspect(e)}"} | |||
end | |||
end | |||
@impl true | |||
def filter(message), do: {:ok, message} | |||
defp check_subject(%{"object" => %{"summary" => subject}} = message) do | |||
subject = String.trim(subject) | |||
if String.length(subject) == 0 do | |||
{:ok, message} | |||
else | |||
{:error, :has_subject} | |||
end | |||
end | |||
defp check_subject(message), do: {:ok, message} | |||
defp string_matches?(content, keywords) when is_list(keywords) do | |||
wordlist = content |> make_wordlist |> trim_punct | |||
Enum.any?(keywords, fn match -> String.downcase(match) in wordlist end) | |||
end | |||
defp string_matches?(content, keyword) when is_binary(keyword) do | |||
wordlist = content |> make_wordlist |> trim_punct | |||
String.downcase(keyword) in wordlist | |||
end | |||
defp check_match(%{"object" => %{} = object} = message) do | |||
match_settings = Pleroma.Config.get([:mrf_auto_subject, :match]) | |||
auto_summary = | |||
Enum.reduce(match_settings, [], fn {keywords, subject}, acc -> | |||
if string_matches?(object["content"], keywords) do | |||
[subject | acc] | |||
else | |||
acc | |||
end | |||
end) | |||
|> Enum.join(", ") | |||
message = put_in(message["object"]["summary"], auto_summary) | |||
{:ok, message} | |||
end | |||
defp make_wordlist(content), | |||
do: | |||
content | |||
|> String.downcase() | |||
|> String.split(" ", trim: true) | |||
|> Enum.uniq() | |||
defp trim_punct(wordlist) when is_list(wordlist), | |||
do: wordlist |> Enum.map(fn word -> String.replace(word, @trim_regex, "") end) | |||
@impl true | |||
def describe do | |||
mrf_autosubject = | |||
:mrf_auto_subject | |||
|> Pleroma.Config.get() | |||
|> Enum.into(%{}) | |||
{:ok, %{mrf_auto_subject: mrf_autosubject}} | |||
end | |||
@impl true | |||
def config_description do | |||
%{ | |||
key: :mrf_auto_subject, | |||
related_policy: "Pleroma.Web.ActivityPub.MRF.AutoSubjectPolicy", | |||
label: "MRF AutoSubject", | |||
description: | |||
"Adds subject to messages matching a keyword or list of keywords if no subject is defined.", | |||
children: [ | |||
%{ | |||
key: :match, | |||
type: {:keyword, :string}, | |||
description: """ | |||
**Keyword**: a string or list of keywords. E.g., ["cat", "dog"] to match on both "cat" and "dog". | |||
**Subject**: a string to insert into the subject field. | |||
Note: the keyword matching is case-insensitive and matches only the whole word. | |||
""" | |||
} | |||
] | |||
} | |||
end | |||
end |
@@ -0,0 +1,123 @@ | |||
defmodule Pleroma.Web.ActivityPub.MRF.AutoSubjectPolicyTest do | |||
use Pleroma.DataCase | |||
import Pleroma.Factory | |||
alias Pleroma.Web.ActivityPub.MRF.AutoSubjectPolicy | |||
describe "filter/1" do | |||
setup do | |||
user = insert(:user) | |||
[user: user] | |||
end | |||
test "pattern as string, matches case insensitive", %{user: user} do | |||
clear_config([:mrf_auto_subject, :match], [{"senate", "uspol"}]) | |||
assert {:ok, | |||
%{"object" => %{"content" => "The Senate is now in recess.", "summary" => "uspol"}}} = | |||
AutoSubjectPolicy.filter(%{ | |||
"type" => "Create", | |||
"actor" => user.ap_id, | |||
"object" => %{"content" => "The Senate is now in recess.", "summary" => ""} | |||
}) | |||
end | |||
test "pattern as list", %{user: user} do | |||
clear_config([:mrf_auto_subject, :match], [{["dinner", "sandwich"], "food"}]) | |||
assert {:ok, | |||
%{ | |||
"object" => %{ | |||
"content" => "I decided to eat leftovers for dinner again.", | |||
"summary" => "food" | |||
} | |||
}} = | |||
AutoSubjectPolicy.filter(%{ | |||
"type" => "Create", | |||
"actor" => user.ap_id, | |||
"object" => %{"content" => "I decided to eat leftovers for dinner again."} | |||
}) | |||
end | |||
test "multiple matches and punctuation trimming", %{user: user} do | |||
clear_config([:mrf_auto_subject, :match], [{["dog", "cat"], "pets"}, {"Torvalds", "Linux"}]) | |||
assert {:ok, | |||
%{ | |||
"object" => %{ | |||
"content" => "A long time ago I named my dog after Linus Torvalds.", | |||
"summary" => "Linux, pets" | |||
} | |||
}} = | |||
AutoSubjectPolicy.filter(%{ | |||
"type" => "Create", | |||
"actor" => user.ap_id, | |||
"object" => %{ | |||
"content" => "A long time ago I named my dog after Linus Torvalds." | |||
} | |||
}) | |||
end | |||
test "with no match", %{user: user} do | |||
clear_config([:mrf_auto_subject, :match], [{"puppy", "pets"}]) | |||
assert {:ok, %{"object" => %{"content" => "I have a kitten", "summary" => ""}}} = | |||
AutoSubjectPolicy.filter(%{ | |||
"type" => "Create", | |||
"actor" => user.ap_id, | |||
"object" => %{"content" => "I have a kitten", "summary" => ""} | |||
}) | |||
end | |||
test "user is not local" do | |||
user = insert(:user, local: false) | |||
clear_config([:mrf_auto_subject, :match], [{"puppy", "pets"}]) | |||
assert {:ok, %{"object" => %{"content" => "We just got a puppy", "summary" => ""}}} = | |||
AutoSubjectPolicy.filter(%{ | |||
"type" => "Create", | |||
"actor" => user.ap_id, | |||
"object" => %{"content" => "We just got a puppy", "summary" => ""} | |||
}) | |||
end | |||
test "subject is already set", %{user: user} do | |||
clear_config([:mrf_auto_subject, :match], [{"election", "politics"}]) | |||
assert {:ok, | |||
%{ | |||
"object" => %{ | |||
"content" => "If your election lasts more than 4 hours you should see a doctor", | |||
"summary" => "uspol, humor" | |||
} | |||
}} = | |||
AutoSubjectPolicy.filter(%{ | |||
"type" => "Create", | |||
"actor" => user.ap_id, | |||
"object" => %{ | |||
"content" => | |||
"If your election lasts more than 4 hours you should see a doctor", | |||
"summary" => "uspol, humor" | |||
} | |||
}) | |||
end | |||
end | |||
test "describe/0" do | |||
clear_config([:mrf_auto_subject, :match], [{"dog", "pets"}]) | |||
assert AutoSubjectPolicy.describe() == | |||
{:ok, | |||
%{ | |||
mrf_auto_subject: %{ | |||
match: [{"dog", "pets"}] | |||
} | |||
}} | |||
end | |||
test "config_description/0" do | |||
assert %{key: _, related_policy: _, label: _, description: _} = | |||
AutoSubjectPolicy.config_description() | |||
end | |||
end |