Merge remote-tracking branch 'origin/develop' into global-status-expiration
authorEgor Kislitsyn <egor@kislitsyn.com>
Mon, 2 Mar 2020 20:32:34 +0000 (00:32 +0400)
committerEgor Kislitsyn <egor@kislitsyn.com>
Mon, 2 Mar 2020 20:32:34 +0000 (00:32 +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 c8f3794a37713f0db64153c334ce9e21e5e6e7e7..ec6b0cb3828d59ace770513724b4db7ea6dc4154 100644 (file)
@@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Rate limiter is now disabled for localhost/socket (unless remoteip plug is enabled)
 - Logger: default log level changed from `warn` to `info`.
 - Config mix task `migrate_to_db` truncates `config` table before migrating the config file.
+- MFR policy to set global expiration for all local Create activities
 - Default to `prepare: :unnamed` in the database configuration.
 <details>
   <summary>API Changes</summary>
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 04b853dcf6cb9543ea2e326ddcc9a4d5c9428923..07121a798650b27f97e1af77543de71942c6a03f 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 263ed11af84a73a88c5539b98c19a4367faede43..b6e737de53dd6b18a4922c7ff394768dcec3287f 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 f7da81b349b5e6acff49fa365375b9710bec1cc1..7a83cad9c5507e3fa136d0e19f3c6a59ab8fd1aa 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 027b3dc306c0715a81dc51b9b15ddeede441620c..4ec13aafad21c840c059ba385650beae6d91e6cb 100644 (file)
@@ -277,20 +277,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 ->