feat(retention): add bounded eviction of old remote threads
Pleroma.Retention treats remote content as a cache: a thread is evicted once nobody local has interacted with it and it has been quiet for longer than remote_post_retention_days, or when the objects table is over an optional row watermark. Local posts are never deleted. A single grouping scan collects activity and object ids per thread so the deletes only touch primary key and ap_id indexes.
This commit is contained in:
parent
f53342775d
commit
1ebcd4f79d
2 changed files with 632 additions and 0 deletions
283
lib/pleroma/retention.ex
Normal file
283
lib/pleroma/retention.ex
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Retention do
|
||||
@moduledoc """
|
||||
Bounded retention of remote content.
|
||||
|
||||
Local content is data; remote content is a cache of somebody else's data.
|
||||
This module treats it that way: a remote thread is evictable when nobody
|
||||
local has interacted with it and it has been quiet for longer than
|
||||
`remote_post_retention_days`, or when the objects table has grown past the
|
||||
configured watermark. Local posts are never evicted.
|
||||
|
||||
A thread is pinned (never evicted) if any activity in it
|
||||
|
||||
* is local (posts, replies, favourites, repeats and reactions by local
|
||||
users all carry the thread's context),
|
||||
* is bookmarked by a local user,
|
||||
* still has a notification for a local user (mentions),
|
||||
* is a report (Flag),
|
||||
|
||||
or if any object in it is referenced by a report.
|
||||
|
||||
Only activities that carry a context take part; relationship activities
|
||||
such as Follow or Block and chat messages have none and are never touched.
|
||||
|
||||
Eviction works in bounded batches so it can run continuously from a cron
|
||||
worker (`Pleroma.Workers.Cron.RetentionWorker`); the aim is a database that
|
||||
plateaus rather than one that has to be shrunk in maintenance windows.
|
||||
"""
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Bookmark
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
@type stats :: %{
|
||||
contexts: non_neg_integer(),
|
||||
objects: non_neg_integer(),
|
||||
activities: non_neg_integer()
|
||||
}
|
||||
|
||||
@empty_stats %{contexts: 0, objects: 0, activities: 0}
|
||||
|
||||
@doc """
|
||||
Runs one eviction batch using the `:retention` config and returns what was removed.
|
||||
"""
|
||||
@spec run(keyword()) :: stats()
|
||||
def run(config \\ Config.get(:retention, [])) do
|
||||
deadline = deadline()
|
||||
over_watermark = over_watermark?(config[:max_objects])
|
||||
|
||||
query =
|
||||
deadline
|
||||
|> evictable_contexts_query(
|
||||
keep_non_public: config[:keep_non_public],
|
||||
ignore_deadline: over_watermark
|
||||
)
|
||||
|> order_by([a], asc: max(a.updated_at), asc: fragment("min(?::text)", a.id))
|
||||
|> limit(^(config[:batch_size] || 500))
|
||||
|> select([a], %{
|
||||
activity_ids: type(fragment("array_agg(?)", a.id), {:array, FlakeId.Ecto.CompatType}),
|
||||
object_ids: fragment("array_agg(associated_object_id(?))", a.data)
|
||||
})
|
||||
|
||||
# Select and delete in one transaction, so a local interaction that lands
|
||||
# in between cannot be left pointing at an evicted object.
|
||||
{:ok, stats} =
|
||||
Repo.transaction(
|
||||
fn ->
|
||||
query
|
||||
|> Repo.all(timeout: :infinity)
|
||||
|> evict()
|
||||
end,
|
||||
timeout: :infinity
|
||||
)
|
||||
|
||||
if stats.contexts > 0, do: delete_unused_hashtags()
|
||||
|
||||
stats
|
||||
end
|
||||
|
||||
@doc """
|
||||
The point in time before which a quiet remote thread becomes evictable.
|
||||
"""
|
||||
@spec deadline() :: NaiveDateTime.t()
|
||||
def deadline do
|
||||
days = Config.get([:instance, :remote_post_retention_days])
|
||||
NaiveDateTime.add(NaiveDateTime.utc_now(), -days * 86_400)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Activities grouped by thread context, restricted to threads that may be evicted.
|
||||
|
||||
The query has no `select`; callers pick what they need from the group (the
|
||||
context alone for `IN (subquery)`, or aggregates of ids for deletion).
|
||||
|
||||
Options:
|
||||
|
||||
* `:keep_non_public` - also pin threads containing a non-public post
|
||||
* `:ignore_deadline` - consider every unpinned thread, not just quiet ones
|
||||
"""
|
||||
@spec evictable_contexts_query(NaiveDateTime.t(), keyword()) :: Ecto.Query.t()
|
||||
def evictable_contexts_query(deadline, opts \\ []) do
|
||||
ignore_deadline = Keyword.get(opts, :ignore_deadline, false) == true
|
||||
|
||||
Activity
|
||||
|> join(:left, [a], b in Bookmark, on: a.id == b.activity_id)
|
||||
|> join(:left, [a], n in Notification, on: a.id == n.activity_id)
|
||||
|> where([a], not is_nil(fragment("? ->> 'context'", a.data)))
|
||||
|> where([a], fragment("? ->> 'context'", a.data) not in subquery(reported_contexts_query()))
|
||||
|> group_by([a], fragment("? ->> 'context'", a.data))
|
||||
|> having([a], max(a.updated_at) < ^deadline or ^ignore_deadline)
|
||||
|> having([a], not fragment("bool_or(?)", a.local))
|
||||
|> having([_, b], fragment("max(?::text) is null", b.id))
|
||||
|> having([_, _, n], fragment("max(?) is null", n.id))
|
||||
|> maybe_keep_non_public(Keyword.get(opts, :keep_non_public, false) == true)
|
||||
end
|
||||
|
||||
defp maybe_keep_non_public(query, false), do: query
|
||||
|
||||
defp maybe_keep_non_public(query, true) do
|
||||
having(
|
||||
query,
|
||||
[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
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# Contexts touched by reports: the contexts of the reported objects, plus the
|
||||
# Flag activities' own contexts (a report gets a fresh context of its own).
|
||||
# Flag objects are a list mixing actor ids, object ids and embedded copies
|
||||
# of objects with an "id"; a single object instead of a list is tolerated.
|
||||
defp reported_contexts_query do
|
||||
flags = where(Activity, [f], fragment("? ->> 'type' = 'Flag'", f.data))
|
||||
|
||||
reported_objects =
|
||||
flags
|
||||
|> join(
|
||||
:inner,
|
||||
[f],
|
||||
fo in fragment(
|
||||
"jsonb_array_elements(CASE WHEN jsonb_typeof(? -> 'object') = 'array' THEN ? -> 'object' ELSE jsonb_build_array(? -> 'object') END)",
|
||||
f.data,
|
||||
f.data,
|
||||
f.data
|
||||
),
|
||||
on: true
|
||||
)
|
||||
|> join(:inner, [_, fo], o in Object,
|
||||
on: fragment("? ->> 'id' = coalesce(? ->> 'id', ? #>> '{}')", o.data, fo.value, fo.value)
|
||||
)
|
||||
|> where([_, _, o], not is_nil(fragment("? ->> 'context'", o.data)))
|
||||
|> select([_, _, o], fragment("? ->> 'context'", o.data))
|
||||
|
||||
flags
|
||||
|> where([f], not is_nil(fragment("? ->> 'context'", f.data)))
|
||||
|> select([f], fragment("? ->> 'context'", f.data))
|
||||
|> union(^reported_objects)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the given threads. Each entry carries the activity ids and object
|
||||
ids collected by `run/1`, so deletion uses primary key and ap_id indexes only.
|
||||
"""
|
||||
@spec evict([%{activity_ids: [String.t()], object_ids: [String.t() | nil]}]) :: stats()
|
||||
def evict([]), do: @empty_stats
|
||||
|
||||
def evict(threads) do
|
||||
activity_ids = Enum.flat_map(threads, & &1.activity_ids)
|
||||
object_ids = threads |> Enum.flat_map(& &1.object_ids) |> Enum.reject(&is_nil/1)
|
||||
|
||||
{:ok, stats} =
|
||||
Repo.transaction(
|
||||
fn ->
|
||||
{objects, deleted_ap_ids} =
|
||||
Object
|
||||
|> where([o], fragment("? ->> 'id'", o.data) in ^object_ids)
|
||||
|> where(
|
||||
[o],
|
||||
fragment(
|
||||
"left(? ->> 'actor', ?) != ?",
|
||||
o.data,
|
||||
^local_prefix_length(),
|
||||
^local_prefix()
|
||||
)
|
||||
)
|
||||
|> select([o], fragment("? ->> 'id'", o.data))
|
||||
|> Repo.delete_all(timeout: :infinity)
|
||||
|
||||
{activities, _} =
|
||||
Activity
|
||||
|> where([a], a.id in ^activity_ids)
|
||||
|> where([a], a.local == false)
|
||||
|> where([a], fragment("? ->> 'type' != 'Flag'", a.data))
|
||||
|> Repo.delete_all(timeout: :infinity)
|
||||
|
||||
# Remote activities outside the thread that point at objects we just
|
||||
# removed (e.g. Deletes carry no context). Flags are kept as they
|
||||
# may have report notes attached.
|
||||
{referencing, _} =
|
||||
Activity
|
||||
|> where([a], a.local == false)
|
||||
|> where([a], fragment("associated_object_id(?)", a.data) in ^deleted_ap_ids)
|
||||
|> where([a], fragment("? ->> 'type' != 'Flag'", a.data))
|
||||
|> Repo.delete_all(timeout: :infinity)
|
||||
|
||||
%{
|
||||
contexts: length(threads),
|
||||
objects: objects,
|
||||
activities: activities + referencing
|
||||
}
|
||||
end,
|
||||
timeout: :infinity
|
||||
)
|
||||
|
||||
stats
|
||||
end
|
||||
|
||||
@doc """
|
||||
Whether the objects table is estimated to hold more rows than `max_objects`.
|
||||
"""
|
||||
@spec over_watermark?(non_neg_integer() | nil) :: boolean()
|
||||
def over_watermark?(nil), do: false
|
||||
def over_watermark?(max_objects), do: estimated_object_count() > max_objects
|
||||
|
||||
@doc """
|
||||
Row count of the objects table from planner statistics, falling back to an
|
||||
exact count when the table has never been analyzed.
|
||||
"""
|
||||
@spec estimated_object_count() :: non_neg_integer()
|
||||
def estimated_object_count do
|
||||
%{rows: [[estimate]]} =
|
||||
Repo.query!("SELECT reltuples::bigint FROM pg_class WHERE oid = 'objects'::regclass")
|
||||
|
||||
if estimate > 0, do: estimate, else: Repo.aggregate(Object, :count, timeout: :infinity)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes hashtags no longer attached to any object and not followed by anyone.
|
||||
"""
|
||||
@spec delete_unused_hashtags() :: :ok
|
||||
def delete_unused_hashtags do
|
||||
Repo.query!(
|
||||
"""
|
||||
DELETE FROM hashtags AS ht
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM hashtags_objects hto
|
||||
WHERE ht.id = hto.hashtag_id
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_follows_hashtag ufh
|
||||
WHERE ht.id = ufh.hashtag_id
|
||||
)
|
||||
""",
|
||||
[],
|
||||
timeout: :infinity
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
# Actor ids of local users start with the instance url; an exact prefix
|
||||
# comparison avoids LIKE wildcards and host/port ambiguity.
|
||||
defp local_prefix, do: Pleroma.Web.Endpoint.url() <> "/"
|
||||
defp local_prefix_length, do: String.length(local_prefix())
|
||||
end
|
||||
349
test/pleroma/retention_test.exs
Normal file
349
test/pleroma/retention_test.exs
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.RetentionTest do
|
||||
use Pleroma.DataCase, async: false
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Bookmark
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Retention
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
setup do
|
||||
clear_config([:instance, :remote_post_retention_days], 90)
|
||||
clear_config([:retention, :enabled], true)
|
||||
clear_config([:retention, :max_objects], nil)
|
||||
clear_config([:retention, :batch_size], 500)
|
||||
clear_config([:retention, :keep_non_public], false)
|
||||
|
||||
old_date =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(-200 * 86_400)
|
||||
|> NaiveDateTime.truncate(:second)
|
||||
|
||||
%{old_date: old_date}
|
||||
end
|
||||
|
||||
# Posts made through CommonAPI are local; turn the activity into a remote one
|
||||
# and backdate it so it looks like an old federated post.
|
||||
defp make_remote_and_old(%Activity{} = activity, old_date) do
|
||||
activity
|
||||
|> Ecto.Changeset.change(%{local: false, updated_at: old_date})
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
defp make_old(%Activity{} = activity, old_date) do
|
||||
activity
|
||||
|> Ecto.Changeset.change(%{updated_at: old_date})
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
defp old_remote_post(user, old_date, params \\ %{}) do
|
||||
{:ok, activity} = CommonAPI.post(user, Map.merge(%{status: "some thing"}, params))
|
||||
make_remote_and_old(activity, old_date)
|
||||
end
|
||||
|
||||
describe "estimated_object_count/0" do
|
||||
test "falls back to an exact count when the planner has no statistics" do
|
||||
insert(:note)
|
||||
insert(:note)
|
||||
|
||||
assert Retention.estimated_object_count() == Repo.aggregate(Object, :count)
|
||||
end
|
||||
end
|
||||
|
||||
describe "run/1" do
|
||||
test "evicts an old remote thread nobody local touched", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
remote_user2 = insert(:user, local: false)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
{:ok, reply} =
|
||||
CommonAPI.post(remote_user2, %{status: "reply", in_reply_to_status_id: post.id})
|
||||
|
||||
make_remote_and_old(reply, old_date)
|
||||
|
||||
{:ok, like} = CommonAPI.favorite(post.id, remote_user2)
|
||||
make_remote_and_old(like, old_date)
|
||||
|
||||
assert %{contexts: 1, objects: 2, activities: 3} = Retention.run()
|
||||
|
||||
refute Object.get_by_ap_id(post.data["object"])
|
||||
refute Object.get_by_ap_id(reply.data["object"])
|
||||
assert Repo.all(Activity) == []
|
||||
end
|
||||
|
||||
test "keeps recent remote threads" do
|
||||
remote_user = insert(:user, local: false)
|
||||
{:ok, post} = CommonAPI.post(remote_user, %{status: "fresh"})
|
||||
post |> Ecto.Changeset.change(%{local: false}) |> Repo.update!()
|
||||
|
||||
assert %{contexts: 0, objects: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps old local posts", %{old_date: old_date} do
|
||||
user = insert(:user)
|
||||
{:ok, post} = CommonAPI.post(user, %{status: "mine"})
|
||||
make_old(post, old_date)
|
||||
|
||||
assert %{contexts: 0, objects: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote thread a local user favourited", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
{:ok, like} = CommonAPI.favorite(post.id, local_user)
|
||||
make_old(like, old_date)
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote thread a local user replied to", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
{:ok, reply} =
|
||||
CommonAPI.post(local_user, %{status: "reply", in_reply_to_status_id: post.id})
|
||||
|
||||
make_old(reply, old_date)
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote thread a local user bookmarked", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
{:ok, _} = Bookmark.create(local_user.id, post.id)
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote post that mentioned a local user", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date, %{status: "hey @#{local_user.nickname}"})
|
||||
|
||||
assert [_] = Repo.all(Pleroma.Notification)
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote post that was reported", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
{:ok, _flag} =
|
||||
CommonAPI.report(local_user, %{account_id: remote_user.id, status_ids: [post.id]})
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "evicts an old remote thread even if remote users interacted with it", %{
|
||||
old_date: old_date
|
||||
} do
|
||||
remote_user = insert(:user, local: false)
|
||||
remote_user2 = insert(:user, local: false)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
{:ok, announce} = CommonAPI.repeat(post.id, remote_user2)
|
||||
make_remote_and_old(announce, old_date)
|
||||
|
||||
assert %{contexts: 1, objects: 1, activities: 2} = Retention.run()
|
||||
refute Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "evicts old threads in batches, oldest first", %{old_date: old_date} do
|
||||
clear_config([:retention, :batch_size], 1)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
older = old_remote_post(remote_user, NaiveDateTime.add(old_date, -86_400))
|
||||
newer = old_remote_post(remote_user, old_date)
|
||||
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
refute Object.get_by_ap_id(older.data["object"])
|
||||
assert Object.get_by_ap_id(newer.data["object"])
|
||||
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
refute Object.get_by_ap_id(newer.data["object"])
|
||||
end
|
||||
|
||||
test "evicts recent unpinned threads when over the object watermark" do
|
||||
clear_config([:retention, :max_objects], 1)
|
||||
clear_config([:retention, :batch_size], 1)
|
||||
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
{:ok, older} = CommonAPI.post(remote_user, %{status: "older"})
|
||||
|
||||
older
|
||||
|> Ecto.Changeset.change(%{
|
||||
local: false,
|
||||
updated_at:
|
||||
NaiveDateTime.utc_now() |> NaiveDateTime.add(-60) |> NaiveDateTime.truncate(:second)
|
||||
})
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, newer} = CommonAPI.post(remote_user, %{status: "newer"})
|
||||
newer |> Ecto.Changeset.change(%{local: false}) |> Repo.update!()
|
||||
|
||||
{:ok, mine} = CommonAPI.post(local_user, %{status: "mine"})
|
||||
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
refute Object.get_by_ap_id(older.data["object"])
|
||||
assert Object.get_by_ap_id(newer.data["object"])
|
||||
assert Object.get_by_ap_id(mine.data["object"])
|
||||
end
|
||||
|
||||
test "does not evict anything below the watermark" do
|
||||
clear_config([:retention, :max_objects], 100)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
{:ok, post} = CommonAPI.post(remote_user, %{status: "fresh"})
|
||||
post |> Ecto.Changeset.change(%{local: false}) |> Repo.update!()
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "with keep_non_public it keeps old non-public remote threads", %{old_date: old_date} do
|
||||
clear_config([:retention, :keep_non_public], true)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
{:ok, private} = CommonAPI.post(remote_user, %{status: "psst", visibility: "private"})
|
||||
make_remote_and_old(private, old_date)
|
||||
|
||||
public = old_remote_post(remote_user, old_date)
|
||||
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
assert Object.get_by_ap_id(private.data["object"])
|
||||
refute Object.get_by_ap_id(public.data["object"])
|
||||
end
|
||||
|
||||
test "never touches activities without a context, even over the watermark" do
|
||||
clear_config([:retention, :max_objects], 1)
|
||||
remote_user = insert(:user, local: false)
|
||||
remote_user2 = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
{:ok, _, _, follow} = CommonAPI.follow(local_user, remote_user)
|
||||
{:ok, block} = CommonAPI.block(remote_user2, local_user)
|
||||
|
||||
# Enough objects to trip the watermark
|
||||
{:ok, post} = CommonAPI.post(remote_user, %{status: "a"})
|
||||
post |> Ecto.Changeset.change(%{local: false}) |> Repo.update!()
|
||||
{:ok, post2} = CommonAPI.post(remote_user, %{status: "b"})
|
||||
post2 |> Ecto.Changeset.change(%{local: false}) |> Repo.update!()
|
||||
|
||||
for activity <- [follow, block] do
|
||||
assert is_nil(activity.data["context"])
|
||||
|
||||
activity
|
||||
|> Ecto.Changeset.change(%{local: false})
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
Retention.run()
|
||||
|
||||
assert Activity.get_by_id(follow.id)
|
||||
assert Activity.get_by_id(block.id)
|
||||
end
|
||||
|
||||
test "keeps remote reports and the threads they point at", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
reporter = insert(:user, local: false)
|
||||
admin = insert(:user, is_admin: true)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
flag =
|
||||
Repo.insert!(%Activity{
|
||||
data: %{
|
||||
"id" => "https://domain1.com/activities/#{Ecto.UUID.generate()}",
|
||||
"type" => "Flag",
|
||||
"actor" => reporter.ap_id,
|
||||
"object" => [remote_user.ap_id, post.data["object"]],
|
||||
"context" => "https://domain1.com/contexts/#{Ecto.UUID.generate()}",
|
||||
"content" => "spam"
|
||||
},
|
||||
local: false,
|
||||
actor: reporter.ap_id,
|
||||
recipients: []
|
||||
})
|
||||
|
||||
make_old(flag, old_date)
|
||||
{:ok, _} = Pleroma.ReportNote.create(admin.id, flag.id, "looking into it")
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Activity.get_by_id(flag.id)
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "removes remote activities outside the thread that point at evicted objects", %{
|
||||
old_date: old_date
|
||||
} do
|
||||
remote_user = insert(:user, local: false)
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
delete =
|
||||
Repo.insert!(%Activity{
|
||||
data: %{
|
||||
"id" => "https://domain1.com/activities/#{Ecto.UUID.generate()}",
|
||||
"type" => "Delete",
|
||||
"actor" => remote_user.ap_id,
|
||||
"object" => post.data["object"]
|
||||
},
|
||||
local: false,
|
||||
actor: remote_user.ap_id,
|
||||
recipients: []
|
||||
})
|
||||
|
||||
assert %{contexts: 1, objects: 1, activities: 2} = Retention.run()
|
||||
refute Activity.get_by_id(delete.id)
|
||||
end
|
||||
|
||||
test "never deletes local objects, even inside an otherwise remote thread", %{
|
||||
old_date: old_date
|
||||
} do
|
||||
local_user = insert(:user)
|
||||
{:ok, post} = CommonAPI.post(local_user, %{status: "mine, honestly"})
|
||||
# Pretend the Create activity federated in from elsewhere
|
||||
make_remote_and_old(post, old_date)
|
||||
|
||||
assert %{contexts: 1, objects: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "removes hashtags that no longer have objects", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
old_remote_post(remote_user, old_date, %{status: "#lonelytag"})
|
||||
assert Pleroma.Hashtag.get_by_name("lonelytag")
|
||||
|
||||
Retention.run()
|
||||
|
||||
refute Pleroma.Hashtag.get_by_name("lonelytag")
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue