Merge remote-tracking branch 'origin/develop' into global-status-expiration
authorEgor Kislitsyn <egor@kislitsyn.com>
Thu, 12 Mar 2020 12:02:24 +0000 (16:02 +0400)
committerEgor Kislitsyn <egor@kislitsyn.com>
Thu, 12 Mar 2020 12:02:24 +0000 (16:02 +0400)
12 files changed:
CHANGELOG.md
config/config.exs
config/description.exs
docs/configuration/cheatsheet.md
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/mrf.ex
lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex [new file with mode: 0644]
lib/pleroma/web/common_api/activity_draft.ex
lib/pleroma/web/common_api/common_api.ex
test/web/activity_pub/activity_pub_test.exs
test/web/activity_pub/mrf/activity_expiration_policy_test.exs [new file with mode: 0644]
test/workers/cron/purge_expired_activities_worker_test.exs

index 100228c6c8c3d7574cc44c23f00ce32f20e8bd46..31156312ced8cdf21c2231b8abe31adb61397c73 100644 (file)
@@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## [Unreleased]
+
+### Changed
+
+- MFR policy to set global expiration for all local Create activities
+
+
 ## [2.0.0] - 2019-03-08
 ### Security
 - Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request.
index 2cd741213da5394e8ba320c33649801c11cfc73d..0c8e5e1c524a3b0a2e34b55b41d9191134a84288 100644 (file)
@@ -365,6 +365,8 @@ config :pleroma, :mrf_keyword,
 
 config :pleroma, :mrf_subchain, match_actor: %{}
 
+config :pleroma, :mrf_activity_expiration, days: 365
+
 config :pleroma, :mrf_vocabulary,
   accept: [],
   reject: []
index 9fdcfcd967883f02dd3772b995247e29e5b5b6be..f113931bd3722eee7aae98afd16efac7fd486c78 100644 (file)
@@ -1346,6 +1346,21 @@ config :pleroma, :config_description, [
       }
     ]
   },
+  %{
+    group: :pleroma,
+    key: :mrf_activity_expiration,
+    label: "MRF Activity Expiration Policy",
+    type: :group,
+    description: "Adds expiration to all local Create activities",
+    children: [
+      %{
+        key: :days,
+        type: :integer,
+        description: "Default global expiration time for all local Create activities (in days)",
+        suggestions: [90, 365]
+      }
+    ]
+  },
   %{
     group: :pleroma,
     key: :mrf_subchain,
index 05fd6ceb1f956f3edcb689a46792027dc977928b..3fd372b950b5cae60cfc46c0851d80409b517ee1 100644 (file)
@@ -35,7 +35,7 @@ To add configuration to your config file, you can copy it from the base config.
 * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
     * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default).
     * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production.
-    * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)).
+    * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certain instances (See [`:mrf_simple`](#mrf_simple)).
     * `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive).
     * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)).
     * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)).
@@ -45,7 +45,8 @@ To add configuration to your config file, you can copy it from the base config.
     * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
     * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
     * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
-* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
+    * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local Create activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)).
+* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
 * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
 * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``.
 * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
@@ -144,6 +145,10 @@ config :pleroma, :mrf_user_allowlist,
   * `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
   * `:reject` rejects the message entirely
 
+#### :mrf_activity_expiration
+
+* `days`: Default global expiration time for all local Create activities (in days)
+
 ### :activitypub
 * `unfollow_blocked`: Whether blocks result in people getting unfollowed
 * `outgoing_blocks`: Whether to federate blocks to other instances
index d9f74b6a4928004f074025369bd26183aa517253..7d8bb1270b52d95fd7b8bc0d8c756dabc9e4cdc9 100644 (file)
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.ActivityPub.ActivityPub do
   alias Pleroma.Activity
   alias Pleroma.Activity.Ir.Topics
+  alias Pleroma.ActivityExpiration
   alias Pleroma.Config
   alias Pleroma.Constants
   alias Pleroma.Conversation
@@ -137,12 +138,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
          {:containment, :ok} <- {:containment, Containment.contain_child(map)},
          {:ok, map, object} <- insert_full_object(map) do
       {:ok, activity} =
-        Repo.insert(%Activity{
+        %Activity{
           data: map,
           local: local,
           actor: map["actor"],
           recipients: recipients
-        })
+        }
+        |> Repo.insert()
+        |> maybe_create_activity_expiration()
 
       # Splice in the child object if we have one.
       activity =
@@ -182,6 +185,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     end
   end
 
+  defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do
+    with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
+      {:ok, activity}
+    end
+  end
+
+  defp maybe_create_activity_expiration(result), do: result
+
   defp create_or_bump_conversation(activity, actor) do
     with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
          %User{} = user <- User.get_cached_by_ap_id(actor),
index a0b3af432b18ecc5cde7a84765a3f07924d7ad1a..5a4a76085dd41e95016120e4c82fc21f607fc5a7 100644 (file)
@@ -8,11 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
   def filter(policies, %{} = object) do
     policies
     |> Enum.reduce({:ok, object}, fn
-      policy, {:ok, object} ->
-        policy.filter(object)
-
-      _, error ->
-        error
+      policy, {:ok, object} -> policy.filter(object)
+      _, error -> error
     end)
   end
 
diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
new file mode 100644 (file)
index 0000000..a9bdf3b
--- /dev/null
@@ -0,0 +1,39 @@
+# 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.ActivityExpirationPolicy do
+  @moduledoc "Adds expiration to all local Create activities"
+  @behaviour Pleroma.Web.ActivityPub.MRF
+
+  @impl true
+  def filter(activity) do
+    activity =
+      if activity["type"] == "Create" && local?(activity) do
+        maybe_add_expiration(activity)
+      else
+        activity
+      end
+
+    {:ok, activity}
+  end
+
+  @impl true
+  def describe, do: {:ok, %{}}
+
+  defp local?(%{"id" => id}) do
+    String.starts_with?(id, Pleroma.Web.Endpoint.url())
+  end
+
+  defp maybe_add_expiration(activity) do
+    days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
+    expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days)
+
+    with %{"expires_at" => existing_expires_at} <- activity,
+         :lt <- NaiveDateTime.compare(existing_expires_at, expires_at) do
+      activity
+    else
+      _ -> Map.put(activity, "expires_at", expires_at)
+    end
+  end
+end
index c4356f93bd341f08f657fc45ebc2462625826447..4211d6afb6a9c92b659855069200e18ac03a7078 100644 (file)
@@ -193,6 +193,13 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
 
   defp changes(draft) do
     direct? = draft.visibility == "direct"
+    additional = %{"cc" => draft.cc, "directMessage" => direct?}
+
+    additional =
+      case draft.expires_at do
+        %NaiveDateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
+        _ -> additional
+      end
 
     changes =
       %{
@@ -200,7 +207,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
         actor: draft.user,
         context: draft.context,
         object: draft.object,
-        additional: %{"cc" => draft.cc, "directMessage" => direct?}
+        additional: additional
       }
       |> Utils.maybe_add_list_data(draft.user, draft.visibility)
 
index 091011c6b1082abf268221c2b80c2a4169e66cf8..6b41c387c72a1f8123b51fee46f5c6145c618d6a 100644 (file)
@@ -282,20 +282,10 @@ defmodule Pleroma.Web.CommonAPI do
 
   def post(user, %{"status" => _} = data) do
     with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
-      draft.changes
-      |> ActivityPub.create(draft.preview?)
-      |> maybe_create_activity_expiration(draft.expires_at)
+      ActivityPub.create(draft.changes, draft.preview?)
     end
   end
 
-  defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
-    with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
-      {:ok, activity}
-    end
-  end
-
-  defp maybe_create_activity_expiration(result, _), do: result
-
   # Updates the emojis for a user based on their profile
   def update(user) do
     emoji = emoji_from_profile(user)
index 3dd3dd04dcd878959cbba77e38b9398d5cc3ce12..8c599faf3a5d3075d08eb9f2a4514e696b7b35aa 100644 (file)
@@ -1970,4 +1970,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
                ActivityPub.move(old_user, new_user)
     end
   end
+
+  describe "global activity expiration" do
+    clear_config([:instance, :rewrite_policy])
+
+    test "creates an activity expiration for local Create activities" do
+      Pleroma.Config.put(
+        [:instance, :rewrite_policy],
+        Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
+      )
+
+      {:ok, %{id: id_create}} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
+      {:ok, _follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"})
+
+      assert [%{activity_id: ^id_create}] = Pleroma.ActivityExpiration |> Repo.all()
+    end
+  end
 end
diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs
new file mode 100644 (file)
index 0000000..0d3bcc4
--- /dev/null
@@ -0,0 +1,61 @@
+# 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.ActivityExpirationPolicyTest do
+  use ExUnit.Case, async: true
+  alias Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
+
+  @id Pleroma.Web.Endpoint.url() <> "/activities/cofe"
+
+  test "adds `expires_at` property" do
+    assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} =
+             ActivityExpirationPolicy.filter(%{"id" => @id, "type" => "Create"})
+
+    assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364
+  end
+
+  test "keeps existing `expires_at` if it less than the config setting" do
+    expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: 1)
+
+    assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} =
+             ActivityExpirationPolicy.filter(%{
+               "id" => @id,
+               "type" => "Create",
+               "expires_at" => expires_at
+             })
+  end
+
+  test "overwrites existing `expires_at` if it greater than the config setting" do
+    too_distant_future = NaiveDateTime.utc_now() |> Timex.shift(years: 2)
+
+    assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} =
+             ActivityExpirationPolicy.filter(%{
+               "id" => @id,
+               "type" => "Create",
+               "expires_at" => too_distant_future
+             })
+
+    assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364
+  end
+
+  test "ignores remote activities" do
+    assert {:ok, activity} =
+             ActivityExpirationPolicy.filter(%{
+               "id" => "https://example.com/123",
+               "type" => "Create"
+             })
+
+    refute Map.has_key?(activity, "expires_at")
+  end
+
+  test "ignores non-Create activities" do
+    assert {:ok, activity} =
+             ActivityExpirationPolicy.filter(%{
+               "id" => "https://example.com/123",
+               "type" => "Follow"
+             })
+
+    refute Map.has_key?(activity, "expires_at")
+  end
+end
index 56c5aa4096274b3a6fceae1af4f7a2e04bc5c209..11b696bd80b1f3cdf10969e691060bc7b95e6f49 100644 (file)
@@ -12,6 +12,7 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do
   import ExUnit.CaptureLog
 
   clear_config([ActivityExpiration, :enabled])
+  clear_config([:instance, :rewrite_policy])
 
   test "deletes an expiration activity" do
     Pleroma.Config.put([ActivityExpiration, :enabled], true)
@@ -36,6 +37,35 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do
     refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id)
   end
 
+  test "works with ActivityExpirationPolicy" do
+    Pleroma.Config.put([ActivityExpiration, :enabled], true)
+
+    Pleroma.Config.put(
+      [:instance, :rewrite_policy],
+      Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
+    )
+
+    user = insert(:user)
+
+    days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
+
+    {:ok, %{id: id} = activity} = Pleroma.Web.CommonAPI.post(user, %{"status" => "cofe"})
+
+    past_date =
+      NaiveDateTime.utc_now() |> Timex.shift(days: -days) |> NaiveDateTime.truncate(:second)
+
+    activity
+    |> Repo.preload(:expiration)
+    |> Map.get(:expiration)
+    |> Ecto.Changeset.change(%{scheduled_at: past_date})
+    |> Repo.update!()
+
+    Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid)
+
+    assert [%{data: %{"type" => "Delete", "deleted_activity_id" => ^id}}] =
+             Pleroma.Repo.all(Pleroma.Activity)
+  end
+
   describe "delete_activity/1" do
     test "adds log message if activity isn't find" do
       assert capture_log([level: :error], fn ->