From db60640c5b91cd1ce2756565835673fa57afe082 Mon Sep 17 00:00:00 2001 From: floatingghost Date: Thu, 1 Dec 2022 15:00:53 +0000 Subject: [PATCH] Fixing up deletes a bit (#327) Co-authored-by: FloatingGhost Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/327 --- CHANGELOG.md | 6 ++ config/config.exs | 9 ++- .../docs/administration/CLI_tasks/database.md | 20 +++++++ lib/mix/tasks/pleroma/database.ex | 8 +++ lib/pleroma/activity/pruner.ex | 41 ++++++++++++++ lib/pleroma/object/pruner.ex | 31 ++++++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 17 ++++++ lib/pleroma/web/activity_pub/side_effects.ex | 1 - .../controllers/account_controller.ex | 1 - .../workers/cron/database_prune_worker.ex | 32 +++++++++++ lib/pleroma/workers/search_indexing_worker.ex | 5 +- ...331_add_notification_activity_id_index.exs | 7 +++ ...110627_add_bookmarks_activity_id_index.exs | 7 +++ ...727_add_report_notes_activity_id_index.exs | 7 +++ ...ade_to_report_notes_on_activity_delete.exs | 19 +++++++ priv/static/logo-512.png | Bin 0 -> 18688 bytes priv/static/logo.svg | 53 ++++++++++++++++++ test/pleroma/activity/pruner_test.exs | 27 +++++++++ test/pleroma/object/pruner_test.exs | 41 ++++++++++++++ .../web/activity_pub/activity_pub_test.exs | 25 +++++++++ .../mastodon_api/update_credentials_test.exs | 2 +- test/support/factory.ex | 40 ++++++++++++- 22 files changed, 389 insertions(+), 10 deletions(-) create mode 100644 lib/pleroma/activity/pruner.ex create mode 100644 lib/pleroma/object/pruner.ex create mode 100644 lib/pleroma/workers/cron/database_prune_worker.ex create mode 100644 priv/repo/migrations/20221129105331_add_notification_activity_id_index.exs create mode 100644 priv/repo/migrations/20221129110627_add_bookmarks_activity_id_index.exs create mode 100644 priv/repo/migrations/20221129110727_add_report_notes_activity_id_index.exs create mode 100644 priv/repo/migrations/20221129112022_add_cascade_to_report_notes_on_activity_delete.exs create mode 100755 priv/static/logo-512.png create mode 100755 priv/static/logo.svg create mode 100644 test/pleroma/activity/pruner_test.exs create mode 100644 test/pleroma/object/pruner_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index b49f77428..136c7e65f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Config: HTTP timeout options, :pool\_timeout and :receive\_timeout - Added statistic gathering about instances which do/don't have signed fetches when they request from us - Ability to set a default post expiry time, after which the post will be deleted. If used in concert with ActivityExpiration MRF, the expiry which comes _sooner_ will be applied. +- Regular task to prune local transient activities +- Task to manually run the transient prune job (pleroma.database prune\_task) ## Changed - MastoAPI: Accept BooleanLike input on `/api/v1/accounts/:id/follow` (fixes follows with mastodon.py) - Relays from akkoma are now off by default - NormalizeMarkup MRF is now on by default - Follow/Block/Mute imports now spin off into *n* tasks to avoid the oban timeout +- Transient activities recieved from remote servers are no longer persisted in the database + +## Upgrade Notes +- If you have an old instance, you will probably want to run `mix pleroma.database prune_task` in the foreground to catch it up with the history of your instance. ## 2022.11 diff --git a/config/config.exs b/config/config.exs index c98a18d39..8e0104400 100644 --- a/config/config.exs +++ b/config/config.exs @@ -569,7 +569,8 @@ new_users_digest: 1, mute_expire: 5, search_indexing: 10, - nodeinfo_fetcher: 1 + nodeinfo_fetcher: 1, + database_prune: 1 ], plugins: [ Oban.Plugins.Pruner, @@ -577,7 +578,8 @@ ], crontab: [ {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, - {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker} + {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}, + {"0 3 * * *", Pleroma.Workers.Cron.PruneDatabaseWorker} ] config :pleroma, :workers, @@ -605,7 +607,8 @@ new_users_digest: :timer.seconds(10), mute_expire: :timer.seconds(5), search_indexing: :timer.seconds(5), - nodeinfo_fetcher: :timer.seconds(10) + nodeinfo_fetcher: :timer.seconds(10), + database_prune: :timer.minutes(10) ] config :pleroma, Pleroma.Formatter, diff --git a/docs/docs/administration/CLI_tasks/database.md b/docs/docs/administration/CLI_tasks/database.md index 8b2ab93e6..73419dc81 100644 --- a/docs/docs/administration/CLI_tasks/database.md +++ b/docs/docs/administration/CLI_tasks/database.md @@ -159,3 +159,23 @@ Change `default_text_search_config` for database and (if necessary) text_search_ ``` See [PostgreSQL documentation](https://www.postgresql.org/docs/current/textsearch-configuration.html) and `docs/configuration/howto_search_cjk.md` for more detail. + +## Pruning old activities + +Over time, transient `Delete` activities and `Tombstone` objects +can accumulate in your database, inflating its size. This is not ideal. +There is a periodic task to prune these transient objects, +but on first run this may take a while on older instances to catch up +to the current day. + +=== "OTP" + + ```sh + ./bin/pleroma_ctl database prune_task + ``` + +=== "From Source" + + ```sh + mix pleroma.database prune_task + ``` \ No newline at end of file diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 99897e83e..0881974ee 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -110,6 +110,14 @@ def run(["prune_objects" | args]) do end end + def run(["prune_task"]) do + start_pleroma() + + nil + |> Pleroma.Workers.Cron.PruneDatabaseWorker.perform() + |> IO.inspect() + end + def run(["fix_likes_collections"]) do start_pleroma() diff --git a/lib/pleroma/activity/pruner.ex b/lib/pleroma/activity/pruner.ex new file mode 100644 index 000000000..054ee514a --- /dev/null +++ b/lib/pleroma/activity/pruner.ex @@ -0,0 +1,41 @@ +defmodule Pleroma.Activity.Pruner do + @moduledoc """ + Prunes activities from the database. + """ + @cutoff 30 + + alias Pleroma.Activity + alias Pleroma.Repo + import Ecto.Query + + def prune_deletes do + before_time = cutoff() + + from(a in Activity, + where: fragment("?->>'type' = ?", a.data, "Delete") and a.inserted_at < ^before_time + ) + |> Repo.delete_all(timeout: :infinity) + end + + def prune_undos do + before_time = cutoff() + + from(a in Activity, + where: fragment("?->>'type' = ?", a.data, "Undo") and a.inserted_at < ^before_time + ) + |> Repo.delete_all(timeout: :infinity) + end + + def prune_removes do + before_time = cutoff() + + from(a in Activity, + where: fragment("?->>'type' = ?", a.data, "Remove") and a.inserted_at < ^before_time + ) + |> Repo.delete_all(timeout: :infinity) + end + + defp cutoff do + DateTime.utc_now() |> Timex.shift(days: -@cutoff) + end +end diff --git a/lib/pleroma/object/pruner.ex b/lib/pleroma/object/pruner.ex new file mode 100644 index 000000000..991d8b0eb --- /dev/null +++ b/lib/pleroma/object/pruner.ex @@ -0,0 +1,31 @@ +defmodule Pleroma.Object.Pruner do + @moduledoc """ + Prunes objects from the database. + """ + @cutoff 30 + + alias Pleroma.Object + alias Pleroma.Delivery + alias Pleroma.Repo + import Ecto.Query + + def prune_tombstoned_deliveries do + from(d in Delivery) + |> join(:inner, [d], o in Object, on: d.object_id == o.id) + |> where([d, o], fragment("?->>'type' = ?", o.data, "Tombstone")) + |> Repo.delete_all(timeout: :infinity) + end + + def prune_tombstones do + before_time = cutoff() + + from(o in Object, + where: fragment("?->>'type' = ?", o.data, "Tombstone") and o.inserted_at < ^before_time + ) + |> Repo.delete_all(timeout: :infinity, on_delete: :delete_all) + end + + defp cutoff do + DateTime.utc_now() |> Timex.shift(days: -@cutoff) + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 76b99025b..db5dbc815 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -105,6 +105,23 @@ def persist(%{"type" => type} = object, meta) when type in @object_types do end end + @unpersisted_activity_types ~w[Undo Delete Remove] + @impl true + def persist(%{"type" => type} = object, [local: false] = meta) + when type in @unpersisted_activity_types do + {:ok, object, meta} + {recipients, _, _} = get_recipients(object) + + unpersisted = %Activity{ + data: object, + local: false, + recipients: recipients, + actor: object["actor"] + } + + {:ok, unpersisted, meta} + end + @impl true def persist(object, meta) do with local <- Keyword.fetch!(meta, :local), diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 18643662e..34617a218 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -288,7 +288,6 @@ def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do # Tasks this handles: # - Delete and unpins the create activity - # - Replace object with Tombstone # - Set up notification # - Reduce the user note count # - Reduce the reply count diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 80497a252..a3648c458 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -222,7 +222,6 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language])) |> Maps.put_if_present(:status_ttl_days, params[:status_ttl_days], status_ttl_days_value) - IO.inspect(user_params) # What happens here: # # We want to update the user through the pipeline, but the ActivityPub diff --git a/lib/pleroma/workers/cron/database_prune_worker.ex b/lib/pleroma/workers/cron/database_prune_worker.ex new file mode 100644 index 000000000..99ea2e836 --- /dev/null +++ b/lib/pleroma/workers/cron/database_prune_worker.ex @@ -0,0 +1,32 @@ +defmodule Pleroma.Workers.Cron.PruneDatabaseWorker do + @moduledoc """ + The worker to prune old data from the database. + """ + require Logger + use Oban.Worker, queue: "database_prune" + + alias Pleroma.Activity.Pruner, as: ActivityPruner + alias Pleroma.Object.Pruner, as: ObjectPruner + + @impl Oban.Worker + def perform(_job) do + Logger.info("Pruning old data from the database") + + Logger.info("Pruning old deletes") + ActivityPruner.prune_deletes() + + Logger.info("Pruning old undos") + ActivityPruner.prune_undos() + + Logger.info("Pruning old removes") + ActivityPruner.prune_removes() + + Logger.info("Pruning old tombstone delivery entries") + ObjectPruner.prune_tombstoned_deliveries() + + Logger.info("Pruning old tombstones") + ObjectPruner.prune_tombstones() + + :ok + end +end diff --git a/lib/pleroma/workers/search_indexing_worker.ex b/lib/pleroma/workers/search_indexing_worker.ex index 70a8d42d0..518a44c0a 100644 --- a/lib/pleroma/workers/search_indexing_worker.ex +++ b/lib/pleroma/workers/search_indexing_worker.ex @@ -14,11 +14,10 @@ def perform(%Job{args: %{"op" => "add_to_index", "activity" => activity_id}}) do end def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) do - object = Pleroma.Object.get_by_id(object_id) - search_module = Pleroma.Config.get([Pleroma.Search, :module]) - search_module.remove_from_index(object) + # Fake the object so we can remove it from the index without having to keep it in the DB + search_module.remove_from_index(%Pleroma.Object{id: object_id}) :ok end diff --git a/priv/repo/migrations/20221129105331_add_notification_activity_id_index.exs b/priv/repo/migrations/20221129105331_add_notification_activity_id_index.exs new file mode 100644 index 000000000..b1eb71f72 --- /dev/null +++ b/priv/repo/migrations/20221129105331_add_notification_activity_id_index.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddNotificationActivityIdIndex do + use Ecto.Migration + + def change do + create(index(:notifications, [:activity_id])) + end +end diff --git a/priv/repo/migrations/20221129110627_add_bookmarks_activity_id_index.exs b/priv/repo/migrations/20221129110627_add_bookmarks_activity_id_index.exs new file mode 100644 index 000000000..f7b7911f8 --- /dev/null +++ b/priv/repo/migrations/20221129110627_add_bookmarks_activity_id_index.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddBookmarksActivityIdIndex do + use Ecto.Migration + + def change do + create(index(:bookmarks, [:activity_id])) + end +end diff --git a/priv/repo/migrations/20221129110727_add_report_notes_activity_id_index.exs b/priv/repo/migrations/20221129110727_add_report_notes_activity_id_index.exs new file mode 100644 index 000000000..dfe74c191 --- /dev/null +++ b/priv/repo/migrations/20221129110727_add_report_notes_activity_id_index.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddReportNotesActivityIdIndex do + use Ecto.Migration + + def change do + create(index(:report_notes, [:activity_id])) + end +end diff --git a/priv/repo/migrations/20221129112022_add_cascade_to_report_notes_on_activity_delete.exs b/priv/repo/migrations/20221129112022_add_cascade_to_report_notes_on_activity_delete.exs new file mode 100644 index 000000000..960554a49 --- /dev/null +++ b/priv/repo/migrations/20221129112022_add_cascade_to_report_notes_on_activity_delete.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.AddCascadeToReportNotesOnActivityDelete do + use Ecto.Migration + + def up do + drop(constraint(:report_notes, "report_notes_activity_id_fkey")) + + alter table(:report_notes) do + modify(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all)) + end + end + + def down do + drop(constraint(:report_notes, "report_notes_activity_id_fkey")) + + alter table(:report_notes) do + modify(:activity_id, references(:activities, type: :uuid)) + end + end +end diff --git a/priv/static/logo-512.png b/priv/static/logo-512.png new file mode 100755 index 0000000000000000000000000000000000000000..02d36e7ab91556da3572e2cf2879e61ec3bc79f7 GIT binary patch literal 18688 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 TF}&OabR2`HtDnm{r-UW|@S{GM literal 0 HcmV?d00001 diff --git a/priv/static/logo.svg b/priv/static/logo.svg new file mode 100755 index 000000000..fbd5c6ec1 --- /dev/null +++ b/priv/static/logo.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/pleroma/activity/pruner_test.exs b/test/pleroma/activity/pruner_test.exs new file mode 100644 index 000000000..312d4f5e4 --- /dev/null +++ b/test/pleroma/activity/pruner_test.exs @@ -0,0 +1,27 @@ +defmodule Pleroma.Activity.PrunerTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Activity + alias Pleroma.Activity.Pruner + + import Pleroma.Factory + + describe "prune_deletes" do + test "it prunes old delete objects" do + user = insert(:user) + + new_delete = insert(:delete_activity, type: "Delete", user: user) + + old_delete = + insert(:delete_activity, + type: "Delete", + user: user, + inserted_at: DateTime.utc_now() |> DateTime.add(-31 * 24, :hour) + ) + + Pruner.prune_deletes() + assert Activity.get_by_id(new_delete.id) + refute Activity.get_by_id(old_delete.id) + end + end +end diff --git a/test/pleroma/object/pruner_test.exs b/test/pleroma/object/pruner_test.exs new file mode 100644 index 000000000..73c574b4b --- /dev/null +++ b/test/pleroma/object/pruner_test.exs @@ -0,0 +1,41 @@ +defmodule Pleroma.Object.PrunerTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Delivery + alias Pleroma.Object + alias Pleroma.Object.Pruner + + import Pleroma.Factory + + describe "prune_deletes" do + test "it prunes old delete objects" do + new_tombstone = insert(:tombstone) + + old_tombstone = + insert(:tombstone, + inserted_at: DateTime.utc_now() |> DateTime.add(-31 * 24, :hour) + ) + + Pruner.prune_tombstones() + assert Object.get_by_id(new_tombstone.id) + refute Object.get_by_id(old_tombstone.id) + end + end + + describe "prune_tombstoned_deliveries" do + test "it prunes old tombstone deliveries" do + user = insert(:user) + + tombstone = insert(:tombstone) + tombstoned = insert(:delivery, object: tombstone, user: user) + + note = insert(:note) + not_tombstoned = insert(:delivery, object: note, user: user) + + Pruner.prune_tombstoned_deliveries() + + refute Repo.get(Delivery, tombstoned.id) + assert Repo.get(Delivery, not_tombstoned.id) + end + end +end diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index c7b3334f3..8d39b1076 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do alias Pleroma.Activity alias Pleroma.Builders.ActivityBuilder + alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Config alias Pleroma.Notification alias Pleroma.Object @@ -2613,4 +2614,28 @@ test "allow fetching of accounts with an empty string name field" do {:ok, user} = ActivityPub.make_user_from_ap_id("https://princess.cat/users/mewmew") assert user.name == " " end + + describe "persist/1" do + test "should not persist remote delete activities" do + poster = insert(:user, local: false) + {:ok, post} = CommonAPI.post(poster, %{status: "hhhhhh"}) + + {:ok, delete_data, meta} = Builder.delete(poster, post) + local_opts = Keyword.put(meta, :local, false) + {:ok, act, _meta} = ActivityPub.persist(delete_data, local_opts) + refute act.inserted_at + end + + test "should not persist remote undo activities" do + poster = insert(:user, local: false) + liker = insert(:user, local: false) + {:ok, post} = CommonAPI.post(poster, %{status: "hhhhhh"}) + {:ok, like} = CommonAPI.favorite(liker, post.id) + + {:ok, undo_data, meta} = Builder.undo(liker, like) + local_opts = Keyword.put(meta, :local, false) + {:ok, act, _meta} = ActivityPub.persist(undo_data, local_opts) + refute act.inserted_at + end + end end diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index 435782d0a..a5403f360 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -226,7 +226,7 @@ test "resets the user's default post expiry", %{conn: conn} do test "does not allow negative integers other than -1 for TTL", %{conn: conn} do conn = patch(conn, "/api/v1/accounts/update_credentials", %{"status_ttl_days" => "-2"}) - assert user_data = json_response_and_validate_schema(conn, 403) + assert json_response_and_validate_schema(conn, 403) end test "updates the user's AKAs", %{conn: conn} do diff --git a/test/support/factory.ex b/test/support/factory.ex index bd9d7fe42..904987aaf 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -233,7 +233,7 @@ def article_factory do %Pleroma.Object{data: Map.merge(data, %{"type" => "Article"})} end - def tombstone_factory do + def tombstone_factory(attrs) do data = %{ "type" => "Tombstone", "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(), @@ -244,6 +244,7 @@ def tombstone_factory do %Pleroma.Object{ data: data } + |> merge_attributes(attrs) end def question_factory(attrs \\ %{}) do @@ -520,6 +521,33 @@ def question_activity_factory(attrs \\ %{}) do |> Map.merge(attrs) end + def delete_activity_factory(attrs \\ %{}) do + user = attrs[:user] || insert(:user) + note_activity = attrs[:note_activity] || insert(:note_activity, user: user) + + data_attrs = attrs[:data_attrs] || %{} + attrs = Map.drop(attrs, [:user, :data_attrs]) + + data = + %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "type" => "Delete", + "actor" => note_activity.data["actor"], + "to" => note_activity.data["to"], + "object" => note_activity.data["id"], + "published" => DateTime.utc_now() |> DateTime.to_iso8601(), + "context" => note_activity.data["context"] + } + |> Map.merge(data_attrs) + + %Pleroma.Activity{ + data: data, + actor: data["actor"], + recipients: data["to"] + } + |> Map.merge(attrs) + end + def oauth_app_factory do %Pleroma.Web.OAuth.App{ client_name: sequence(:client_name, &"Some client #{&1}"), @@ -676,4 +704,14 @@ def frontend_setting_profile_factory(params \\ %{}) do } |> Map.merge(params) end + + def delivery_factory(params \\ %{}) do + object = Map.get(params, :object, build(:note)) + user = Map.get(params, :user, build(:user)) + + %Pleroma.Delivery{ + object: object, + user: user + } + end end