MRF: add ObjectAgePolicy which deals with old posts being imported
authorAriadne Conill <ariadne@dereferenced.org>
Fri, 15 Nov 2019 01:48:10 +0000 (19:48 -0600)
committerAriadne Conill <ariadne@dereferenced.org>
Fri, 15 Nov 2019 01:48:10 +0000 (19:48 -0600)
lib/pleroma/web/activity_pub/mrf/object_age_policy.ex [new file with mode: 0644]
test/web/activity_pub/mrf/object_age_policy_test.exs [new file with mode: 0644]

diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
new file mode 100644 (file)
index 0000000..f6c6f31
--- /dev/null
@@ -0,0 +1,103 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
+  alias Pleroma.Config
+  alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.MRF
+
+  require Logger
+  require Pleroma.Constants
+
+  @moduledoc "Filter activities depending on their age"
+  @behaviour MRF
+
+  defp check_date(%{"published" => published} = message) do
+    with %DateTime{} = now <- DateTime.utc_now(),
+         {:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
+         max_ttl <- Config.get([:mrf_object_age, :threshold]),
+         {:ttl, false} <- {:ttl, DateTime.diff(now, then) > max_ttl} do
+      {:ok, message}
+    else
+      {:ttl, true} ->
+        {:reject, nil}
+
+      e ->
+        {:error, e}
+    end
+  end
+
+  defp check_reject(message, actions) do
+    if :reject in actions do
+      {:reject, nil}
+    else
+      {:ok, message}
+    end
+  end
+
+  defp check_delist(message, actions) do
+    if :delist in actions do
+      with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
+        to = List.delete(message["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
+        cc = List.delete(message["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
+
+        message =
+          message
+          |> Map.put("to", to)
+          |> Map.put("cc", cc)
+
+        {:ok, message}
+      else
+        # Unhandleable error: somebody is messing around, just drop the message.
+        e ->
+          Logger.error("ERROR: #{inspect(e)}")
+          {:reject, nil}
+      end
+    else
+      {:ok, message}
+    end
+  end
+
+  defp check_strip_followers(message, actions) do
+    if :strip_followers in actions do
+      with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
+        to = List.delete(message["to"], user.follower_address)
+        cc = List.delete(message["cc"], user.follower_address)
+
+        message =
+          message
+          |> Map.put("to", to)
+          |> Map.put("cc", cc)
+
+        {:ok, message}
+      else
+        # Unhandleable error: somebody is messing around, just drop the message.
+        _e ->
+          {:reject, nil}
+      end
+    else
+      {:ok, message}
+    end
+  end
+
+  @impl true
+  def filter(%{"type" => "Create", "published" => _} = message) do
+    with actions <- Config.get([:mrf_object_age, :actions]),
+         {:reject, _} <- check_date(message),
+         {:ok, message} <- check_reject(message, actions),
+         {:ok, message} <- check_delist(message, actions),
+         {:ok, message} <- check_strip_followers(message, actions) do
+      {:ok, message}
+    else
+      # check_date() is allowed to short-circuit the pipeline
+      e -> e
+    end
+  end
+
+  @impl true
+  def filter(message), do: {:ok, message}
+
+  @impl true
+  def describe, do: {:ok, %{}}
+end
diff --git a/test/web/activity_pub/mrf/object_age_policy_test.exs b/test/web/activity_pub/mrf/object_age_policy_test.exs
new file mode 100644 (file)
index 0000000..3ea00d7
--- /dev/null
@@ -0,0 +1,105 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
+  use Pleroma.DataCase
+  alias Pleroma.Config
+  alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.Visibility
+  alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy
+
+  clear_config([:mrf_object_age]) do
+    Config.put(:mrf_object_age,
+      threshold: 172_800,
+      actions: [:delist, :strip_followers]
+    )
+  end
+
+  setup_all do
+    Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+    :ok
+  end
+
+  describe "with reject action" do
+    test "it rejects an old post" do
+      Config.put([:mrf_object_age, :actions], [:reject])
+
+      data =
+        File.read!("test/fixtures/mastodon-post-activity.json")
+        |> Poison.decode!()
+
+      {:reject, _} = ObjectAgePolicy.filter(data)
+    end
+
+    test "it allows a new post" do
+      Config.put([:mrf_object_age, :actions], [:reject])
+
+      data =
+        File.read!("test/fixtures/mastodon-post-activity.json")
+        |> Poison.decode!()
+        |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
+
+      {:ok, _} = ObjectAgePolicy.filter(data)
+    end
+  end
+
+  describe "with delist action" do
+    test "it delists an old post" do
+      Config.put([:mrf_object_age, :actions], [:delist])
+
+      data =
+        File.read!("test/fixtures/mastodon-post-activity.json")
+        |> Poison.decode!()
+
+      {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"])
+
+      {:ok, data} = ObjectAgePolicy.filter(data)
+
+      assert Visibility.get_visibility(%{data: data}) == "unlisted"
+    end
+
+    test "it allows a new post" do
+      Config.put([:mrf_object_age, :actions], [:delist])
+
+      data =
+        File.read!("test/fixtures/mastodon-post-activity.json")
+        |> Poison.decode!()
+        |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
+
+      {:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"])
+
+      {:ok, ^data} = ObjectAgePolicy.filter(data)
+    end
+  end
+
+  describe "with strip_followers action" do
+    test "it strips followers collections from an old post" do
+      Config.put([:mrf_object_age, :actions], [:strip_followers])
+
+      data =
+        File.read!("test/fixtures/mastodon-post-activity.json")
+        |> Poison.decode!()
+
+      {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"])
+
+      {:ok, data} = ObjectAgePolicy.filter(data)
+
+      refute user.follower_address in data["to"]
+      refute user.follower_address in data["cc"]
+    end
+
+    test "it allows a new post" do
+      Config.put([:mrf_object_age, :actions], [:strip_followers])
+
+      data =
+        File.read!("test/fixtures/mastodon-post-activity.json")
+        |> Poison.decode!()
+        |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
+
+      {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"])
+
+      {:ok, ^data} = ObjectAgePolicy.filter(data)
+    end
+  end
+end