Fix tagpolicy to also work with Update
[akkoma] / lib / pleroma / web / activity_pub / mrf / keyword_policy.ex
index 15e09dcf03abdbe3d11c8293ab0e5619aaddb832..7c921fc767d73fcaac873ca36c393145e02fd1d7 100644 (file)
@@ -1,5 +1,5 @@
 # Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
 
   @moduledoc "Reject or Word-Replace messages with a keyword or regex"
 
-  @behaviour Pleroma.Web.ActivityPub.MRF
+  @behaviour Pleroma.Web.ActivityPub.MRF.Policy
   defp string_matches?(string, _) when not is_binary(string) do
     false
   end
@@ -20,23 +20,53 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
     String.match?(string, pattern)
   end
 
-  defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = message) do
-    if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
-         string_matches?(content, pattern) or string_matches?(summary, pattern)
-       end) do
-      {:reject, "[KeywordPolicy] Matches with rejected keyword"}
-    else
+  defp object_payload(%{} = object) do
+    [object["content"], object["summary"], object["name"]]
+    |> Enum.filter(& &1)
+    |> Enum.join("\n")
+  end
+
+  defp check_reject(%{"object" => %{} = object} = message) do
+    with {:ok, _new_object} <-
+           Pleroma.Object.Updater.do_with_history(object, fn object ->
+             payload = object_payload(object)
+
+             if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
+                  string_matches?(payload, pattern)
+                end) do
+               {:reject, "[KeywordPolicy] Matches with rejected keyword"}
+             else
+               {:ok, message}
+             end
+           end) do
       {:ok, message}
+    else
+      e -> e
     end
   end
 
-  defp check_ftl_removal(
-         %{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message
-       ) do
-    if Pleroma.Constants.as_public() in to and
-         Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
-           string_matches?(content, pattern) or string_matches?(summary, pattern)
+  defp check_ftl_removal(%{"type" => "Create", "to" => to, "object" => %{} = object} = message) do
+    check_keyword = fn object ->
+      payload = object_payload(object)
+
+      if Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
+           string_matches?(payload, pattern)
          end) do
+        {:should_delist, nil}
+      else
+        {:ok, %{}}
+      end
+    end
+
+    should_delist? = fn object ->
+      with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check_keyword) do
+        false
+      else
+        _ -> true
+      end
+    end
+
+    if Pleroma.Constants.as_public() in to and should_delist?.(object) do
       to = List.delete(to, Pleroma.Constants.as_public())
       cc = [Pleroma.Constants.as_public() | message["cc"] || []]
 
@@ -51,39 +81,37 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
     end
   end
 
-  defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
-    content =
-      if is_binary(content) do
-        content
-      else
-        ""
-      end
+  defp check_ftl_removal(message) do
+    {:ok, message}
+  end
 
-    summary =
-      if is_binary(summary) do
-        summary
-      else
-        ""
-      end
+  defp check_replace(%{"object" => %{} = object} = message) do
+    replace_kw = fn object ->
+      ["content", "name", "summary"]
+      |> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)
+      |> Enum.reduce(object, fn field, object ->
+        data =
+          Enum.reduce(
+            Pleroma.Config.get([:mrf_keyword, :replace]),
+            object[field],
+            fn {pat, repl}, acc -> String.replace(acc, pat, repl) end
+          )
+
+        Map.put(object, field, data)
+      end)
+      |> (fn object -> {:ok, object} end).()
+    end
+
+    {:ok, object} = Pleroma.Object.Updater.do_with_history(object, replace_kw)
+
+    message = Map.put(message, "object", object)
 
-    {content, summary} =
-      Enum.reduce(
-        Pleroma.Config.get([:mrf_keyword, :replace]),
-        {content, summary},
-        fn {pattern, replacement}, {content_acc, summary_acc} ->
-          {String.replace(content_acc, pattern, replacement),
-           String.replace(summary_acc, pattern, replacement)}
-        end
-      )
-
-    {:ok,
-     message
-     |> put_in(["object", "content"], content)
-     |> put_in(["object", "summary"], summary)}
+    {:ok, message}
   end
 
   @impl true
-  def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do
+  def filter(%{"type" => type, "object" => %{"content" => _content}} = message)
+      when type in ["Create", "Update"] do
     with {:ok, message} <- check_reject(message),
          {:ok, message} <- check_ftl_removal(message),
          {:ok, message} <- check_replace(message) do
@@ -129,4 +157,48 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
 
     {:ok, %{mrf_keyword: mrf_keyword}}
   end
+
+  @impl true
+  def config_description do
+    %{
+      key: :mrf_keyword,
+      related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
+      label: "MRF Keyword",
+      description:
+        "Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
+      children: [
+        %{
+          key: :reject,
+          type: {:list, :string},
+          description: """
+            A list of patterns which result in message being rejected.
+
+            Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+          """,
+          suggestions: ["foo", ~r/foo/iu]
+        },
+        %{
+          key: :federated_timeline_removal,
+          type: {:list, :string},
+          description: """
+            A list of patterns which result in message being removed from federated timelines (a.k.a unlisted).
+
+            Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+          """,
+          suggestions: ["foo", ~r/foo/iu]
+        },
+        %{
+          key: :replace,
+          type: {:list, :tuple},
+          key_placeholder: "instance",
+          value_placeholder: "reason",
+          description: """
+            **Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+
+            **Replacement**: a string. Leaving the field empty is permitted.
+          """
+        }
+      ]
+    }
+  end
 end