fix(activitypub): deduplicate account favourites

This commit is contained in:
Lain Soykaf 2026-07-25 10:51:09 +04:00
commit 99befcaf1b
No known key found for this signature in database
3 changed files with 82 additions and 0 deletions

View file

@ -1025,6 +1025,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_assigned_account(query, _), do: query defp restrict_assigned_account(query, _), do: query
defp restrict_favorited_by(query, %{favorited_by: ap_id}) do defp restrict_favorited_by(query, %{favorited_by: ap_id}) do
newer_favorite =
from(newer_favorite in Activity,
where: newer_favorite.actor == ^ap_id,
where: newer_favorite.id > parent_as(:favorited_activity).id,
where: fragment("?->>'type' = ?", newer_favorite.data, "Like"),
where:
fragment(
"associated_object_id(?) = associated_object_id(?)",
newer_favorite.data,
parent_as(:favorited_activity).data
),
select: 1
)
from( from(
[activity, object: object] in query, [activity, object: object] in query,
join: favorited_activity in Activity, join: favorited_activity in Activity,
@ -1033,6 +1047,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
favorited_activity.actor == ^ap_id and favorited_activity.actor == ^ap_id and
fragment("?->>'type' = ?", favorited_activity.data, "Like") and fragment("?->>'type' = ?", favorited_activity.data, "Like") and
fragment("associated_object_id(?) = (?)->>'id'", favorited_activity.data, object.data), fragment("associated_object_id(?) = (?)->>'id'", favorited_activity.data, object.data),
where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id),
where: not exists(subquery(newer_favorite)),
select: %Activity{activity | pagination_id: favorited_activity.id} select: %Activity{activity | pagination_id: favorited_activity.id}
) )
end end

View file

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.AddActivityFavoritesLookupIndex do
use Ecto.Migration
@disable_ddl_transaction true
@disable_migration_lock true
def change do
create_if_not_exists(
index(:activities, [:actor, "associated_object_id(data)", "id DESC"],
name: :activities_favorites_lookup_index,
where: "data->>'type' = 'Like'",
concurrently: true
)
)
end
end

View file

@ -5,9 +5,12 @@
defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Activity
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
@ -127,6 +130,49 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
assert Enum.map(response, & &1["id"]) == [second_post.id, first_post.id, third_post.id] assert Enum.map(response, & &1["id"]) == [second_post.id, first_post.id, third_post.id]
end end
test "returns a status once when duplicate favorite activities exist", %{
conn: conn,
user: user
} do
older_activity = insert(:note_activity)
{:ok, _} = CommonAPI.favorite(older_activity.id, user)
activity = insert(:note_activity)
{:ok, favorite} = CommonAPI.favorite(activity.id, user)
duplicate =
Repo.insert!(%Activity{
actor: favorite.actor,
data: Map.put(favorite.data, "id", Utils.generate_activity_id()),
local: favorite.local,
recipients: favorite.recipients
})
conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites?limit=1")
response = json_response_and_validate_schema(conn, :ok)
assert Enum.map(response, & &1["id"]) == [activity.id]
assert [link_header] = get_resp_header(conn, "link")
assert link_header =~ "max_id=#{duplicate.id}"
assert link_header =~ "min_id=#{duplicate.id}"
response =
conn
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites?max_id=#{duplicate.id}&limit=1")
|> json_response_and_validate_schema(:ok)
assert Enum.map(response, & &1["id"]) == [older_activity.id]
{:ok, _} = CommonAPI.unfavorite(activity.id, user)
response =
conn
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response_and_validate_schema(:ok)
assert Enum.map(response, & &1["id"]) == [older_activity.id]
end
test "returns favorited DM only when user is logged in and he is one of recipients", %{ test "returns favorited DM only when user is logged in and he is one of recipients", %{
current_user: current_user, current_user: current_user,
user: user user: user