From 98464607022cf1850d245bfde5a342514d3c58d2 Mon Sep 17 00:00:00 2001 From: Oneric Date: Fri, 4 Apr 2025 02:09:36 +0200 Subject: [PATCH 1/4] cosmetic/mix/database: refactor object prune into subfunctions Statmenets for keeping and breaking threads vastly differ and the whole if block doesn't even fit on one screen. Thus move each version out into its own function to improve readability --- lib/mix/tasks/pleroma/database.ex | 130 ++++++++++++++++-------------- 1 file changed, 69 insertions(+), 61 deletions(-) diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 0a09a1c4a..642fb7d6f 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -116,6 +116,73 @@ def prune_orphaned_activities(limit \\ 0, opts \\ []) when is_number(limit) do del_single + del_array end + defp deletable_objects_keeping_threads(time_deadline, limit_cnt, options) do + # We want to delete objects from threads where + # 1. the newest post is still old + # 2. none of the activities is local + # 3. none of the activities is bookmarked + # 4. optionally none of the posts is non-public + deletable_context = + if Keyword.get(options, :keep_non_public) do + Pleroma.Activity + |> join(:left, [a], b in Pleroma.Bookmark, on: a.id == b.activity_id) + |> group_by([a], fragment("? ->> 'context'::text", a.data)) + |> having( + [a], + not fragment( + # Posts (checked on Create Activity) is non-public + "bool_or((not(?->'to' \\? ? OR ?->'cc' \\? ?)) and ? ->> 'type' = 'Create')", + a.data, + ^Pleroma.Constants.as_public(), + a.data, + ^Pleroma.Constants.as_public(), + a.data + ) + ) + else + Pleroma.Activity + |> join(:left, [a], b in Pleroma.Bookmark, on: a.id == b.activity_id) + |> group_by([a], fragment("? ->> 'context'::text", a.data)) + end + |> having([a], max(a.updated_at) < ^time_deadline) + |> having([a], not fragment("bool_or(?)", a.local)) + |> having([_, b], fragment("max(?::text) is null", b.id)) + |> maybe_limit(limit_cnt) + |> select([a], fragment("? ->> 'context'::text", a.data)) + + Pleroma.Object + |> where([o], fragment("? ->> 'context'::text", o.data) in subquery(deletable_context)) + end + + defp deletable_objects_breaking_threads(time_deadline, limit_cnt, options) do + deletable = + if Keyword.get(options, :keep_non_public) do + Pleroma.Object + |> where( + [o], + fragment( + "?->'to' \\? ? OR ?->'cc' \\? ?", + o.data, + ^Pleroma.Constants.as_public(), + o.data, + ^Pleroma.Constants.as_public() + ) + ) + else + Pleroma.Object + end + |> where([o], o.updated_at < ^time_deadline) + |> where( + [o], + fragment("split_part(?->>'actor', '/', 3) != ?", o.data, ^Pleroma.Web.Endpoint.host()) + ) + |> maybe_limit(limit_cnt) + |> select([o], o.id) + + Pleroma.Object + |> where([o], o.id in subquery(deletable)) + end + def run(["remove_embedded_objects" | args]) do {options, [], []} = OptionParser.parse( @@ -251,68 +318,9 @@ def run(["prune_objects" | args]) do {del_obj, _} = if Keyword.get(options, :keep_threads) do - # We want to delete objects from threads where - # 1. the newest post is still old - # 2. none of the activities is local - # 3. none of the activities is bookmarked - # 4. optionally none of the posts is non-public - deletable_context = - if Keyword.get(options, :keep_non_public) do - Pleroma.Activity - |> join(:left, [a], b in Pleroma.Bookmark, on: a.id == b.activity_id) - |> group_by([a], fragment("? ->> 'context'::text", a.data)) - |> having( - [a], - not fragment( - # Posts (checked on Create Activity) is non-public - "bool_or((not(?->'to' \\? ? OR ?->'cc' \\? ?)) and ? ->> 'type' = 'Create')", - a.data, - ^Pleroma.Constants.as_public(), - a.data, - ^Pleroma.Constants.as_public(), - a.data - ) - ) - else - Pleroma.Activity - |> join(:left, [a], b in Pleroma.Bookmark, on: a.id == b.activity_id) - |> group_by([a], fragment("? ->> 'context'::text", a.data)) - end - |> having([a], max(a.updated_at) < ^time_deadline) - |> having([a], not fragment("bool_or(?)", a.local)) - |> having([_, b], fragment("max(?::text) is null", b.id)) - |> maybe_limit(limit_cnt) - |> select([a], fragment("? ->> 'context'::text", a.data)) - - Pleroma.Object - |> where([o], fragment("? ->> 'context'::text", o.data) in subquery(deletable_context)) + deletable_objects_keeping_threads(time_deadline, limit_cnt, options) else - deletable = - if Keyword.get(options, :keep_non_public) do - Pleroma.Object - |> where( - [o], - fragment( - "?->'to' \\? ? OR ?->'cc' \\? ?", - o.data, - ^Pleroma.Constants.as_public(), - o.data, - ^Pleroma.Constants.as_public() - ) - ) - else - Pleroma.Object - end - |> where([o], o.updated_at < ^time_deadline) - |> where( - [o], - fragment("split_part(?->>'actor', '/', 3) != ?", o.data, ^Pleroma.Web.Endpoint.host()) - ) - |> maybe_limit(limit_cnt) - |> select([o], o.id) - - Pleroma.Object - |> where([o], o.id in subquery(deletable)) + deletable_objects_breaking_threads(time_deadline, limit_cnt, options) end |> Repo.delete_all(timeout: :infinity) From be5312228f893dca3a39825d3af82e8b668d8137 Mon Sep 17 00:00:00 2001 From: Oneric Date: Fri, 4 Apr 2025 02:17:38 +0200 Subject: [PATCH 2/4] cosmetic/mix/database: refactor log_message building This is more concise and readable --- lib/mix/tasks/pleroma/database.ex | 69 +++++++++---------------------- 1 file changed, 20 insertions(+), 49 deletions(-) diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 642fb7d6f..d44ece13a 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -20,6 +20,10 @@ defmodule Mix.Tasks.Pleroma.Database do @shortdoc "A collection of database related tasks" @moduledoc File.read!("docs/docs/administration/CLI_tasks/database.md") + defp maybe_concat(str, condition, appendix) do + if condition, do: str <> appendix, else: str + end + defp maybe_limit(query, limit_cnt) do if is_number(limit_cnt) and limit_cnt > 0 do limit(query, [], ^limit_cnt) @@ -240,16 +244,9 @@ def run(["prune_orphaned_activities" | args]) do {limit, options} = Keyword.pop(options, :limit, 0) - log_message = "Pruning orphaned activities" - - log_message = - if limit > 0 do - log_message <> ", limiting deletion to #{limit} rows" - else - log_message - end - - Logger.info(log_message) + "Pruning orphaned activities" + |> maybe_concat(limit > 0, ", limiting deletion to #{limit} rows") + |> Logger.info() deleted = prune_orphaned_activities(limit, options) @@ -276,45 +273,19 @@ def run(["prune_objects" | args]) do limit_cnt = Keyword.get(options, :limit, 0) - log_message = "Pruning objects older than #{deadline} days" - - log_message = - if Keyword.get(options, :keep_non_public) do - log_message <> ", keeping non public posts" - else - log_message - end - - log_message = - if Keyword.get(options, :keep_threads) do - log_message <> ", keeping threads intact" - else - log_message - end - - log_message = - if Keyword.get(options, :prune_orphaned_activities) do - log_message <> ", pruning orphaned activities" - else - log_message - end - - log_message = - if Keyword.get(options, :vacuum) do - log_message <> - ", doing a full vacuum (you shouldn't do this as a recurring maintanance task)" - else - log_message - end - - log_message = - if limit_cnt > 0 do - log_message <> ", limiting to #{limit_cnt} rows" - else - log_message - end - - Logger.info(log_message) + "Pruning objects older than #{deadline} days" + |> maybe_concat(Keyword.get(options, :keep_non_public), ", keeping non public posts") + |> maybe_concat(Keyword.get(options, :keep_threads), ", keeping threads intact") + |> maybe_concat( + Keyword.get(options, :prune_orphaned_activities), + ", pruning orphaned activities" + ) + |> maybe_concat( + Keyword.get(options, :vacuum), + ", doing a full vacuum (you shouldn't do this as a recurring maintanance task)" + ) + |> maybe_concat(limit_cnt > 0, ", limiting to #{limit_cnt} rows") + |> Logger.info() {del_obj, _} = if Keyword.get(options, :keep_threads) do From 8576ec42ecb40553426ce1266729d552a8192292 Mon Sep 17 00:00:00 2001 From: Oneric Date: Fri, 4 Apr 2025 02:53:09 +0200 Subject: [PATCH 3/4] mix/database: retain pinned objects by default Pinned objects and their threads will be refetched on user refresh which by default happens after a day once a user is encountered again in any form including a mention. We observed pruning pinned objects usually results in heavy load for hours after a database prune due to a clogged up remote fetch queue as pinned posts and their threads of many (most?) users get refetched. Thus do not prune pinned posts by default. Keeping closer to earlier behaviour this will still prune threads of pinned posts regardless of --keep-threads if nothing else prevenets it. --- .../docs/administration/CLI_tasks/database.md | 4 +- lib/mix/tasks/pleroma/database.ex | 24 +++++++ test/mix/tasks/pleroma/database_test.exs | 68 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/docs/docs/administration/CLI_tasks/database.md b/docs/docs/administration/CLI_tasks/database.md index 580c9d32b..a6fecb38e 100644 --- a/docs/docs/administration/CLI_tasks/database.md +++ b/docs/docs/administration/CLI_tasks/database.md @@ -48,10 +48,12 @@ This will prune remote posts older than 90 days (configurable with [`config :ple ### Options -- `--keep-threads` - Don't prune posts when they are part of a thread where at least one post has seen local interaction (e.g. one of the posts is a local post, or is favourited by a local user, or has been repeated by a local user...). It also wont delete posts when at least one of the posts in that thread is kept (e.g. because one of the posts has seen recent activity). +- `--keep-threads` - Don't prune posts when they are part of a thread where at least one post has seen local interaction (e.g. one of the posts is a local post, or is favourited by a local user, or has been repeated by a local user...). It also wont delete posts when at least one of the posts in that thread has seen recent activity. - `--keep-non-public` - Keep non-public posts like DM's and followers-only, even if they are remote. - `--limit` - limits how many remote posts get pruned. This limit does **not** apply to any of the follow up jobs. If wanting to keep the database load in check it is thus advisable to run the standalone `prune_orphaned_activities` task with a limit afterwards instead of passing `--prune-orphaned-activities` to this task. - `--prune-orphaned-activities` - Also prune orphaned activities afterwards. Activities are things like Like, Create, Announce, Flag (aka reports)... They can significantly help reduce the database size. +- `--prune-pinned` - Also prune pinned posts; keeping pinned posts does not suffice to protect their threads from pruning, even when using `--keep-threads`. + Note, if using this option and pinned posts are pruned, they and their threads will just be refetched on the next user update. Therefore it usually doesn't bring much gain while incurring a heavy fetch load after pruning. - `--vacuum` - Run `VACUUM FULL` after the objects are pruned. This should not be used on a regular basis, but is useful if your instance has been running for a long time before pruning. ## Prune orphaned activities from the database diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index d44ece13a..a746dc8fe 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -120,6 +120,21 @@ def prune_orphaned_activities(limit \\ 0, opts \\ []) when is_number(limit) do del_single + del_array end + defp query_pinned_object_apids() do + Pleroma.User + |> select([u], %{ap_id: fragment("jsonb_object_keys(?)", u.pinned_objects)}) + end + + defp query_pinned_object_ids() do + # If this additional level of subquery is omitted and we directly supply AP ids + # to te final query, it appears to overexert PostgreSQL(17)'s planner leading + # to a very inefficient query with enormous memory and time consumption. + # By supplying database IDs it ends up quite cheap however. + Object + |> where([o], fragment("?->>'id' IN ?", o.data, subquery(query_pinned_object_apids()))) + |> select([o], o.id) + end + defp deletable_objects_keeping_threads(time_deadline, limit_cnt, options) do # We want to delete objects from threads where # 1. the newest post is still old @@ -262,6 +277,7 @@ def run(["prune_objects" | args]) do keep_threads: :boolean, keep_non_public: :boolean, prune_orphaned_activities: :boolean, + prune_pinned: :boolean, limit: :integer ] ) @@ -276,6 +292,7 @@ def run(["prune_objects" | args]) do "Pruning objects older than #{deadline} days" |> maybe_concat(Keyword.get(options, :keep_non_public), ", keeping non public posts") |> maybe_concat(Keyword.get(options, :keep_threads), ", keeping threads intact") + |> maybe_concat(Keyword.get(options, :prune_pinned), ", pruning pinned posts") |> maybe_concat( Keyword.get(options, :prune_orphaned_activities), ", pruning orphaned activities" @@ -293,6 +310,13 @@ def run(["prune_objects" | args]) do else deletable_objects_breaking_threads(time_deadline, limit_cnt, options) end + |> then(fn q -> + if Keyword.get(options, :prune_pinned) do + q + else + where(q, [o], o.id not in subquery(query_pinned_object_ids())) + end + end) |> Repo.delete_all(timeout: :infinity) Logger.info("Deleted #{del_obj} objects...") diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs index 4f97a978a..0b9a9e75f 100644 --- a/test/mix/tasks/pleroma/database_test.exs +++ b/test/mix/tasks/pleroma/database_test.exs @@ -88,6 +88,74 @@ test "it prunes old objects from the database", %{old_insert_date: old_insert_da refute Object.get_by_id(note_remote_non_public_id) end + test "it retains pinned posts by default", %{old_insert_date: old_insert_date} do + insert(:note) + + pin_user = insert(:user, local: false) + + %{id: note_remote_pinned_id, data: note_remote_pinned_data} = + :note + |> insert(user: pin_user) + |> Ecto.Changeset.change(%{updated_at: old_insert_date}) + |> Repo.update!() + + User.add_pinned_object_id(pin_user, note_remote_pinned_data["id"]) + + note_remote_non_public = + %{id: note_remote_non_public_id, data: note_remote_non_public_data} = + :note + |> insert() + + note_remote_non_public + |> Ecto.Changeset.change(%{ + updated_at: old_insert_date, + data: note_remote_non_public_data |> update_in(["to"], fn _ -> [] end) + }) + |> Repo.update!() + + assert length(Repo.all(Object)) == 3 + + Mix.Tasks.Pleroma.Database.run(["prune_objects"]) + + assert length(Repo.all(Object)) == 2 + assert Object.get_by_id(note_remote_pinned_id) + refute Object.get_by_id(note_remote_non_public_id) + end + + test "it prunes pinned posts with --prune-pinned", %{old_insert_date: old_insert_date} do + insert(:note) + + pin_user = insert(:user, local: false) + + %{id: note_remote_pinned_id, data: note_remote_pinned_data} = + :note + |> insert(user: pin_user) + |> Ecto.Changeset.change(%{updated_at: old_insert_date}) + |> Repo.update!() + + User.add_pinned_object_id(pin_user, note_remote_pinned_data["id"]) + + note_remote_non_public = + %{id: note_remote_non_public_id, data: note_remote_non_public_data} = + :note + |> insert() + + note_remote_non_public + |> Ecto.Changeset.change(%{ + updated_at: old_insert_date, + data: note_remote_non_public_data |> update_in(["to"], fn _ -> [] end) + }) + |> Repo.update!() + + assert length(Repo.all(Object)) == 3 + + Mix.Tasks.Pleroma.Database.run(["prune_objects", "--prune-pinned"]) + + assert length(Repo.all(Object)) == 1 + refute Object.get_by_id(note_remote_pinned_id) + refute Object.get_by_id(note_remote_non_public_id) + end + test "it cleans up bookmarks", %{old_insert_date: old_insert_date} do user = insert(:user) {:ok, old_object_activity} = CommonAPI.post(user, %{status: "yadayada"}) From 516827c356d3c1fae0b79419c1e33eb08ef7a682 Mon Sep 17 00:00:00 2001 From: Oneric Date: Fri, 4 Apr 2025 03:42:15 +0200 Subject: [PATCH 4/4] mix/database: add keep-followed option for object pruning This allows to retain posts and boosts of remote actors with local follows regardless of age. With the "full" setting this can be taken further treating such followed actors just like local users even keeping all posts they liked or reacated to. --- .../docs/administration/CLI_tasks/database.md | 5 +- lib/mix/tasks/pleroma/database.ex | 70 ++++++++++++++++ test/mix/tasks/pleroma/database_test.exs | 79 +++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) diff --git a/docs/docs/administration/CLI_tasks/database.md b/docs/docs/administration/CLI_tasks/database.md index a6fecb38e..f92cd1a52 100644 --- a/docs/docs/administration/CLI_tasks/database.md +++ b/docs/docs/administration/CLI_tasks/database.md @@ -48,7 +48,10 @@ This will prune remote posts older than 90 days (configurable with [`config :ple ### Options -- `--keep-threads` - Don't prune posts when they are part of a thread where at least one post has seen local interaction (e.g. one of the posts is a local post, or is favourited by a local user, or has been repeated by a local user...). It also wont delete posts when at least one of the posts in that thread has seen recent activity. +- `--keep-followed ` - If set to `posts` all posts and boosts of users with local follows will be kept. + If set to `full` it will additionally keep any posts such users interacted with; this requires `--keep-threads`. + By default this is set to `none` and followed users are not treated special. +- `--keep-threads` - Don't prune posts when they are part of a thread where at least one post has seen local interaction (e.g. one of the posts is a local post, or is favourited by a local user, or has been repeated by a local user...). It also won’t delete posts when at least one of the posts in the thread has seen recent activity or is kept due to `--keep-followed`. - `--keep-non-public` - Keep non-public posts like DM's and followers-only, even if they are remote. - `--limit` - limits how many remote posts get pruned. This limit does **not** apply to any of the follow up jobs. If wanting to keep the database load in check it is thus advisable to run the standalone `prune_orphaned_activities` task with a limit afterwards instead of passing `--prune-orphaned-activities` to this task. - `--prune-orphaned-activities` - Also prune orphaned activities afterwards. Activities are things like Like, Create, Announce, Flag (aka reports)... They can significantly help reduce the database size. diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index a746dc8fe..c8b6c2329 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -135,6 +135,55 @@ defp query_pinned_object_ids() do |> select([o], o.id) end + defp query_followed_remote_user_apids() do + Pleroma.FollowingRelationship + |> join(:inner, [rel], ufing in User, on: rel.following_id == ufing.id) + |> join(:inner, [rel], ufer in User, on: rel.follower_id == ufer.id) + |> where([rel], rel.state == :follow_accept) + |> where([_rel, ufing, ufer], ufer.local and not ufing.local) + |> select([_rel, ufing], %{ap_id: ufing.ap_id}) + end + + defp parse_keep_followed_arg(options) do + case Keyword.get(options, :keep_followed) do + "full" -> :full + "posts" -> :posts + "none" -> false + nil -> false + _ -> raise "Invalid argument for keep_followed! Must be 'full', 'posts' or 'none'" + end + end + + defp maybe_restrict_followed_activities(query, options) do + case Keyword.get(options, :keep_followed) do + :full -> + having( + query, + [a], + fragment( + "bool_and(?->>'actor' NOT IN ?)", + a.data, + subquery(query_followed_remote_user_apids()) + ) + ) + + :posts -> + having( + query, + [a], + not fragment( + "bool_or(?->>'actor' IN ? AND ?->>'type' = ANY('{Create,Announce}'))", + a.data, + subquery(query_followed_remote_user_apids()), + a.data + ) + ) + + _ -> + query + end + end + defp deletable_objects_keeping_threads(time_deadline, limit_cnt, options) do # We want to delete objects from threads where # 1. the newest post is still old @@ -166,6 +215,7 @@ defp deletable_objects_keeping_threads(time_deadline, limit_cnt, options) do |> having([a], max(a.updated_at) < ^time_deadline) |> having([a], not fragment("bool_or(?)", a.local)) |> having([_, b], fragment("max(?::text) is null", b.id)) + |> maybe_restrict_followed_activities(options) |> maybe_limit(limit_cnt) |> select([a], fragment("? ->> 'context'::text", a.data)) @@ -195,6 +245,17 @@ defp deletable_objects_breaking_threads(time_deadline, limit_cnt, options) do [o], fragment("split_part(?->>'actor', '/', 3) != ?", o.data, ^Pleroma.Web.Endpoint.host()) ) + |> then(fn q -> + if Keyword.get(options, :keep_followed) do + where( + q, + [o], + fragment("?->>'actor'", o.data) not in subquery(query_followed_remote_user_apids()) + ) + else + q + end + end) |> maybe_limit(limit_cnt) |> select([o], o.id) @@ -274,6 +335,7 @@ def run(["prune_objects" | args]) do args, strict: [ vacuum: :boolean, + keep_followed: :string, keep_threads: :boolean, keep_non_public: :boolean, prune_orphaned_activities: :boolean, @@ -282,6 +344,13 @@ def run(["prune_objects" | args]) do ] ) + kf = parse_keep_followed_arg(options) + options = Keyword.put(options, :keep_followed, kf) + + if kf == :full and not Keyword.get(options, :keep_threads) do + raise "keep_followed=full only works in conjunction with keep_thread!" + end + start_pleroma() deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) @@ -292,6 +361,7 @@ def run(["prune_objects" | args]) do "Pruning objects older than #{deadline} days" |> maybe_concat(Keyword.get(options, :keep_non_public), ", keeping non public posts") |> maybe_concat(Keyword.get(options, :keep_threads), ", keeping threads intact") + |> maybe_concat(kf, ", keeping #{kf} activities of followed users") |> maybe_concat(Keyword.get(options, :prune_pinned), ", pruning pinned posts") |> maybe_concat( Keyword.get(options, :prune_orphaned_activities), diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs index 0b9a9e75f..b1de10c9b 100644 --- a/test/mix/tasks/pleroma/database_test.exs +++ b/test/mix/tasks/pleroma/database_test.exs @@ -419,6 +419,85 @@ test "with the --keep-threads option it keeps old threads with bookmarked posts" assert length(Repo.all(Object)) == 1 end + defp prepare_keep_followed_test(old_insert_date) do + remote_user = insert(:user, local: false) + local_user = insert(:user, local: true) + third_party = insert(:user, local: false) + + CommonAPI.follow(local_user, remote_user) + CommonAPI.accept_follow_request(local_user, remote_user) + + assert :follow_accept == Pleroma.FollowingRelationship.get(local_user, remote_user).state + + {:ok, old_remote_post_activity} = + CommonAPI.post(remote_user, %{status: "some thing", local: false}) + + old_remote_post_activity + |> Ecto.Changeset.change(%{local: false, updated_at: old_insert_date}) + |> Repo.update!() + + old_remote_post_activity.object + |> Ecto.Changeset.change(%{updated_at: old_insert_date}) + |> Repo.update!() + + {:ok, old_liked_post_activity} = + CommonAPI.post(third_party, %{status: "boo!", local: false}) + + {:ok, old_like_activity} = CommonAPI.favorite(remote_user, old_liked_post_activity.id) + + old_liked_post_activity + |> Ecto.Changeset.change(%{local: false, updated_at: old_insert_date}) + |> Repo.update!() + + old_liked_post_activity.object + |> Ecto.Changeset.change(%{updated_at: old_insert_date}) + |> Repo.update!() + + old_like_activity + |> Ecto.Changeset.change(%{local: false, updated_at: old_insert_date}) + |> Repo.update!() + + assert length(Repo.all(Object)) == 2 + + {old_remote_post_activity.object.id, old_liked_post_activity.object.id} + end + + test "by default does not keep posts of followed users", %{ + old_insert_date: old_insert_date + } do + _ = prepare_keep_followed_test(old_insert_date) + Mix.Tasks.Pleroma.Database.run(["prune_objects"]) + assert length(Repo.all(Object)) == 0 + end + + test "with the --keep-followed posts option it keeps old posts of followed users", %{ + old_insert_date: old_insert_date + } do + {old_remote_post_id, old_liked_post_id} = + prepare_keep_followed_test(old_insert_date) + + Mix.Tasks.Pleroma.Database.run(["prune_objects", "--keep-followed", "posts"]) + + assert length(Repo.all(Object)) == 1 + assert Object.get_by_id(old_remote_post_id) + refute Object.get_by_id(old_liked_post_id) + end + + test "with the --keep-followed full option it keeps old posts liked by a followed user", %{ + old_insert_date: old_insert_date + } do + _ = prepare_keep_followed_test(old_insert_date) + + Mix.Tasks.Pleroma.Database.run([ + "prune_objects", + "--keep-followed", + "full", + "--keep-threads" + ]) + + assert length(Repo.all(Object)) == 2 + end + test "We don't have unexpected tables which may contain objects that are referenced by activities" do # We can delete orphaned activities. For that we look for the objects they reference in the 'objects', 'activities', and 'users' table. # If someone adds another table with objects (idk, maybe with separate relations, or collections or w/e), then we need to make sure we