From 6e8d1a167fe4797a7d43ba371469b70b6d60fd00 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sun, 17 Mar 2024 14:08:07 +0800
Subject: [PATCH] fix: cascading bugs during deletion.

- During cascade deletion, the posts deleted by the cascade are not published to noteStream.
- During cascade deletion, the notes count of instance and user is incorrect.
---
 packages/backend/src/services/note/delete.ts | 57 +++++++++++++++-----
 1 file changed, 45 insertions(+), 12 deletions(-)

diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts
index 830f55dd16..0aaa46708c 100644
--- a/packages/backend/src/services/note/delete.ts
+++ b/packages/backend/src/services/note/delete.ts
@@ -6,7 +6,7 @@ import renderUndo from "@/remote/activitypub/renderer/undo.js";
 import { renderActivity } from "@/remote/activitypub/renderer/index.js";
 import renderTombstone from "@/remote/activitypub/renderer/tombstone.js";
 import config from "@/config/index.js";
-import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
+import { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
 import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js";
 import { Notes, Users, Instances } from "@/models/index.js";
 import {
@@ -16,8 +16,13 @@ import {
 import { countSameRenotes } from "@/misc/count-same-renotes.js";
 import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instance-doc.js";
 import { deliverToRelays } from "@/services/relay.js";
+import type { IActivity } from "@/remote/activitypub/type.js";
 
-async function recalculateNotesCountOfUser(user: { id: User["id"] }) {
+async function recalculateNotesCountOfUser(user: {
+	id: User["id"];
+	host: null;
+}) {
+	if (!Users.isLocalUser(user)) return;
 	const newCount = await Notes.createQueryBuilder()
 		.where(`"userId" = :id`, { id: user.id })
 		.getCount();
@@ -35,6 +40,7 @@ async function recalculateNotesCountOfUser(user: { id: User["id"] }) {
  * 投稿を削除します。
  * @param user 投稿者
  * @param note 投稿
+ * @param deleteFromDb false if called by making private
  */
 export default async function (
 	user: { id: User["id"]; uri: User["uri"]; host: User["host"] },
@@ -58,6 +64,16 @@ export default async function (
 		await Notes.decrement({ id: note.replyId }, "repliesCount", 1);
 	}
 
+	const cascadingNotes = await findCascadingNotes(note);
+	const affectedLocalUsers: Record<
+		User["id"],
+		{ id: User["id"]; uri: User["uri"]; host: null }
+	> = {};
+	if (Users.isLocalUser(user)) {
+		affectedLocalUsers[user.id] = user;
+	}
+	const instanceNotesCountDecreasement: Record<string, number> = {};
+
 	if (!quiet) {
 		// Only broadcast "deleted" to local if the note is deleted from db
 		if (deleteFromDb) {
@@ -101,12 +117,23 @@ export default async function (
 		}
 
 		// also deliever delete activity to cascaded notes
-		const cascadingNotes = (await findCascadingNotes(note)).filter(
-			(note) => !note.localOnly,
-		); // filter out local-only notes
 		for (const cascadingNote of cascadingNotes) {
+			if (cascadingNote.userId !== user.id) {
+				// For others, the post appears to have been deleted and publishNoteStream is also required.
+				publishNoteStream(cascadingNote.id, "deleted", {
+					deletedAt: deletedAt,
+				});
+			}
+
 			if (!cascadingNote.user) continue;
-			if (!Users.isLocalUser(cascadingNote.user)) continue;
+			if (!Users.isLocalUser(cascadingNote.user)) {
+				if (!Users.isRemoteUser(cascadingNote.user)) continue;
+				instanceNotesCountDecreasement[cascadingNote.user.host] ??= 0;
+				instanceNotesCountDecreasement[cascadingNote.user.host]++;
+				continue; // filter out remote users
+			}
+			affectedLocalUsers[cascadingNote.user.id] ??= cascadingNote.user;
+			if (cascadingNote.localOnly) continue; // filter out local-only notes
 			const content = renderActivity(
 				renderDelete(
 					renderTombstone(`${config.url}/notes/${cascadingNote.id}`),
@@ -118,8 +145,14 @@ export default async function (
 		//#endregion
 
 		if (Users.isRemoteUser(user)) {
-			registerOrFetchInstanceDoc(user.host).then((i) => {
-				Instances.decrement({ id: i.id }, "notesCount", 1);
+			instanceNotesCountDecreasement[user.host] ??= 0;
+			instanceNotesCountDecreasement[user.host]++;
+		}
+		for (const [host, number] of Object.entries(
+			instanceNotesCountDecreasement,
+		)) {
+			registerOrFetchInstanceDoc(host).then((i) => {
+				Instances.decrement({ id: i.id }, "notesCount", number);
 			});
 		}
 	}
@@ -130,9 +163,9 @@ export default async function (
 			userId: user.id,
 		});
 
-		if (Users.isLocalUser(user)) {
+		for (const [_, affectedUser] of Object.entries(affectedLocalUsers)) {
 			// For the case of cascading deletion, it cannot be solved by simply reducing the notesCount by 1.
-			recalculateNotesCountOfUser(user);
+			recalculateNotesCountOfUser(affectedUser);
 		}
 	}
 }
@@ -159,7 +192,7 @@ async function findCascadingNotes(note: Note) {
 	};
 	await recursive(note.id);
 
-	return cascadingNotes.filter((note) => note.userHost === null); // filter out non-local users
+	return cascadingNotes;
 }
 
 async function getMentionedRemoteUsers(note: Note) {
@@ -190,7 +223,7 @@ async function getMentionedRemoteUsers(note: Note) {
 async function deliverToConcerned(
 	user: { id: ILocalUser["id"]; host: null },
 	note: Note,
-	content: any,
+	content: IActivity | null,
 ) {
 	deliverToFollowers(user, content);
 	deliverToRelays(user, content);