From 389ec6350be1b5f980eb19db4c98ba2ebf22ca38 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Wed, 26 Feb 2025 09:29:12 +0900 Subject: fix(backend): send Delete activity of a note to users who renoted or replied to it (#15554) * fix(backend): send Delete activity of a note to users who renoted or replied to it * Update CHANGELOG.md --- packages/backend/src/core/NoteDeleteService.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) (limited to 'packages/backend/src/core/NoteDeleteService.ts') diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 4ecd2592b2..e394506a44 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Brackets, In } from 'typeorm'; +import { Brackets, In, IsNull, Not } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; @@ -189,13 +189,27 @@ export class NoteDeleteService { }) as MiRemoteUser[]; } + @bindThis + private async getRenotedOrRepliedRemoteUsers(note: MiNote) { + const query = this.notesRepository.createQueryBuilder('note') + .leftJoinAndSelect('note.user', 'user') + .where(new Brackets(qb => { + qb.orWhere('note.renoteId = :renoteId', { renoteId: note.id }); + qb.orWhere('note.replyId = :replyId', { replyId: note.id }); + })) + .andWhere({ userHost: Not(IsNull()) }); + const notes = await query.getMany() as (MiNote & { user: MiRemoteUser })[]; + const remoteUsers = notes.map(({ user }) => user); + return remoteUsers; + } + @bindThis private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); - const remoteUsers = await this.getMentionedRemoteUsers(note); - for (const remoteUser of remoteUsers) { - this.apDeliverManagerService.deliverToUser(user, content, remoteUser); - } + this.apDeliverManagerService.deliverToUsers(user, content, [ + ...await this.getMentionedRemoteUsers(note), + ...await this.getRenotedOrRepliedRemoteUsers(note), + ]); } } -- cgit v1.2.3-freya From c302a5c2d7696bc9dddeabe914b92ad2fdc0b0ba Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 5 May 2025 17:44:00 -0400 Subject: reorder relay activities to avoid delivery race condition --- packages/backend/src/core/AccountMoveService.ts | 3 ++- packages/backend/src/core/AccountUpdateService.ts | 9 +++------ packages/backend/src/core/NoteCreateService.ts | 12 ++++++------ packages/backend/src/core/NoteDeleteService.ts | 6 +++--- packages/backend/src/core/NoteEditService.ts | 12 ++++++------ packages/backend/src/core/NotePiningService.ts | 17 +++++++---------- packages/backend/src/core/PollService.ts | 9 +++------ .../src/core/activitypub/models/ApNoteService.ts | 4 ++-- packages/backend/src/misc/promise-tracker.ts | 4 ++++ packages/backend/src/server/api/endpoints/i/update.ts | 2 +- .../src/server/api/endpoints/notes/polls/vote.ts | 2 +- 11 files changed, 38 insertions(+), 42 deletions(-) (limited to 'packages/backend/src/core/NoteDeleteService.ts') diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 7bf33e13c5..738026f753 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -95,11 +95,12 @@ export class AccountMoveService { const srcPerson = await this.apRendererService.renderPerson(src); const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src)); await this.apDeliverManagerService.deliverToFollowers(src, updateAct); - this.relayService.deliverToRelays(src, updateAct); + await this.relayService.deliverToRelays(src, updateAct); // Deliver Move activity to the followers of the old account const moveAct = this.apRendererService.addContext(this.apRendererService.renderMove(src, dst)); await this.apDeliverManagerService.deliverToFollowers(src, moveAct); + await this.relayService.deliverToRelays(src, moveAct); // Publish meUpdated event const iObj = await this.userEntityService.pack(src.id, src, { schema: 'MeDetailed', includeSecrets: true }); diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 69a57b4854..ef2962fc12 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -27,15 +27,12 @@ export class AccountUpdateService { } @bindThis - public async publishToFollowers(userId: MiUser['id']) { - const user = await this.usersRepository.findOneBy({ id: userId }); - if (user == null) throw new Error('user not found'); - + public async publishToFollowers(user: MiUser) { // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 if (this.userEntityService.isLocalUser(user)) { const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); - this.apDeliverManagerService.deliverToFollowers(user, content); - this.relayService.deliverToRelays(user, content); + await this.apDeliverManagerService.deliverToFollowers(user, content); + await this.relayService.deliverToRelays(user, content); } } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index fd6300483f..ed97908f66 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -51,7 +51,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { isReply } from '@/misc/is-reply.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; +import { trackTask } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { LatestNoteService } from '@/core/LatestNoteService.js'; @@ -729,7 +729,7 @@ export class NoteCreateService implements OnApplicationShutdown { //#region AP deliver if (!data.localOnly && this.userEntityService.isLocalUser(user)) { - (async () => { + trackTask(async () => { const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user); const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); @@ -755,12 +755,12 @@ export class NoteCreateService implements OnApplicationShutdown { dm.addFollowersRecipe(); } + await dm.execute(); + if (['public'].includes(note.visibility)) { - this.relayService.deliverToRelays(user, noteActivity); + await this.relayService.deliverToRelays(user, noteActivity); } - - trackPromise(dm.execute()); - })(); + }); } //#endregion } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 8ec05c88dc..9b6c4754d1 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -247,11 +247,11 @@ export class NoteDeleteService { @bindThis private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { - this.apDeliverManagerService.deliverToFollowers(user, content); - this.relayService.deliverToRelays(user, content); - this.apDeliverManagerService.deliverToUsers(user, content, [ + await this.apDeliverManagerService.deliverToFollowers(user, content); + await this.apDeliverManagerService.deliverToUsers(user, content, [ ...await this.getMentionedRemoteUsers(note), ...await this.getRenotedOrRepliedRemoteUsers(note), ]); + await this.relayService.deliverToRelays(user, content); } } diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index e70ecf396d..332560154d 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -46,7 +46,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { CacheService } from '@/core/CacheService.js'; import { isReply } from '@/misc/is-reply.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; +import { trackTask } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { LatestNoteService } from '@/core/LatestNoteService.js'; @@ -669,7 +669,7 @@ export class NoteEditService implements OnApplicationShutdown { //#region AP deliver if (!data.localOnly && this.userEntityService.isLocalUser(user)) { - (async () => { + trackTask(async () => { const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user); const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); @@ -713,12 +713,12 @@ export class NoteEditService implements OnApplicationShutdown { } } + await dm.execute(); + if (['public'].includes(note.visibility)) { - this.relayService.deliverToRelays(user, noteActivity); + await this.relayService.deliverToRelays(user, noteActivity); } - - trackPromise(dm.execute()); - })(); + }); } //#endregion } diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index d38b48b65d..6ab7268254 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -49,7 +49,7 @@ export class NotePiningService { * @param noteId */ @bindThis - public async addPinned(user: { id: MiUser['id']; host: MiUser['host']; }, noteId: MiNote['id']) { + public async addPinned(user: MiUser, noteId: MiNote['id']) { // Fetch pinee const note = await this.notesRepository.findOneBy({ id: noteId, @@ -78,7 +78,7 @@ export class NotePiningService { // Deliver to remote followers if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) { - this.deliverPinnedChange(user.id, note.id, true); + this.deliverPinnedChange(user, note.id, true); } } @@ -88,7 +88,7 @@ export class NotePiningService { * @param noteId */ @bindThis - public async removePinned(user: { id: MiUser['id']; host: MiUser['host']; }, noteId: MiNote['id']) { + public async removePinned(user: MiUser, noteId: MiNote['id']) { // Fetch unpinee const note = await this.notesRepository.findOneBy({ id: noteId, @@ -106,22 +106,19 @@ export class NotePiningService { // Deliver to remote followers if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) { - this.deliverPinnedChange(user.id, noteId, false); + this.deliverPinnedChange(user, noteId, false); } } @bindThis - public async deliverPinnedChange(userId: MiUser['id'], noteId: MiNote['id'], isAddition: boolean) { - const user = await this.usersRepository.findOneBy({ id: userId }); - if (user == null) throw new Error('user not found'); - + public async deliverPinnedChange(user: MiUser, noteId: MiNote['id'], isAddition: boolean) { if (!this.userEntityService.isLocalUser(user)) return; const target = `${this.config.url}/users/${user.id}/collections/featured`; const item = `${this.config.url}/notes/${noteId}`; const content = this.apRendererService.addContext(isAddition ? this.apRendererService.renderAdd(user, target, item) : this.apRendererService.renderRemove(user, target, item)); - this.apDeliverManagerService.deliverToFollowers(user, content); - this.relayService.deliverToRelays(user, content); + await this.apDeliverManagerService.deliverToFollowers(user, content); + await this.relayService.deliverToRelays(user, content); } } diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index d6364613bd..33262a4804 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -90,10 +90,7 @@ export class PollService { } @bindThis - public async deliverQuestionUpdate(noteId: MiNote['id']) { - const note = await this.notesRepository.findOneBy({ id: noteId }); - if (note == null) throw new Error('note not found'); - + public async deliverQuestionUpdate(note: MiNote) { if (note.localOnly) return; const user = await this.usersRepository.findOneBy({ id: note.userId }); @@ -101,8 +98,8 @@ export class PollService { if (this.userEntityService.isLocalUser(user)) { const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, user, false), user)); - this.apDeliverManagerService.deliverToFollowers(user, content); - this.relayService.deliverToRelays(user, content); + await this.apDeliverManagerService.deliverToFollowers(user, content); + await this.relayService.deliverToRelays(user, content); } } } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 11f5bbd943..f6152e3888 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -296,7 +296,7 @@ export class ApNoteService { await this.pollService.vote(actor, reply, index); // リモートフォロワーにUpdate配信 - this.pollService.deliverQuestionUpdate(reply.id); + this.pollService.deliverQuestionUpdate(reply); } return null; }; @@ -493,7 +493,7 @@ export class ApNoteService { await this.pollService.vote(actor, reply, index); // リモートフォロワーにUpdate配信 - this.pollService.deliverQuestionUpdate(reply.id); + this.pollService.deliverQuestionUpdate(reply); } return null; }; diff --git a/packages/backend/src/misc/promise-tracker.ts b/packages/backend/src/misc/promise-tracker.ts index 8a52ca703e..76b4dd810c 100644 --- a/packages/backend/src/misc/promise-tracker.ts +++ b/packages/backend/src/misc/promise-tracker.ts @@ -5,6 +5,10 @@ const promiseRefs: Set>> = new Set(); +export function trackTask(task: () => Promise): void { + trackPromise(task()); +} + /** * This tracks promises that other modules decided not to wait for, * and makes sure they are all settled before fully closing down the server. diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 094c3da8e6..5f93597fd7 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -614,7 +614,7 @@ export default class extends Endpoint { // eslint- // フォロワーにUpdateを配信 if (this.userNeedsPublishing(user, updates) || this.profileNeedsPublishing(profile, updatedProfile)) { - this.accountUpdateService.publishToFollowers(user.id); + this.accountUpdateService.publishToFollowers(user); } return iObj; diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index a5014a490f..0b318304f3 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -174,7 +174,7 @@ export default class extends Endpoint { // eslint- } // リモートフォロワーにUpdate配信 - this.pollService.deliverQuestionUpdate(note.id); + this.pollService.deliverQuestionUpdate(note); }); } } -- cgit v1.2.3-freya From 7cb7ed6fc9eb86acd89973aae43e2d71a810ddad Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 11 May 2025 05:06:10 -0400 Subject: decrement quote count correctly --- packages/backend/src/core/NoteDeleteService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src/core/NoteDeleteService.ts') diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 9b6c4754d1..5bfb6b9dee 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -124,7 +124,7 @@ export class NoteDeleteService { this.perUserNotesChart.update(user, note, false); } - if (note.renoteId && note.text || !note.renoteId) { + if (!isRenote(note) || isQuote(note)) { // Decrement notes count (user) this.decNotesCountOfUser(user); } -- cgit v1.2.3-freya From b22b5577b489769c36323a71abb5876e2457d554 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 11 May 2025 05:06:25 -0400 Subject: also delete AP logs for cascading notes --- packages/backend/src/core/NoteDeleteService.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/core/NoteDeleteService.ts') diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 5bfb6b9dee..1c634602f3 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -165,8 +165,11 @@ export class NoteDeleteService { }); } - if (note.uri) { - this.apLogService.deleteObjectLogs(note.uri) + const deletedUris = [note, ...cascadingNotes] + .map(n => n.uri) + .filter((u): u is string => u != null); + if (deletedUris.length > 0) { + this.apLogService.deleteObjectLogs(deletedUris) .catch(err => this.logger.error(err, `Failed to delete AP logs for note '${note.uri}'`)); } } -- cgit v1.2.3-freya From b4bce57fcb35e2bd7021eef1b608bd17d3533b10 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 11 May 2025 05:06:58 -0400 Subject: update user activity on Renote, Un-Renote, Note Edit, React, and Un-React --- packages/backend/src/core/NoteCreateService.ts | 2 ++ packages/backend/src/core/NoteDeleteService.ts | 2 ++ packages/backend/src/core/NoteEditService.ts | 2 ++ packages/backend/src/core/ReactionService.ts | 4 ++++ 4 files changed, 10 insertions(+) (limited to 'packages/backend/src/core/NoteDeleteService.ts') diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index b810ae0b5b..4514f3decd 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -592,6 +592,8 @@ export class NoteCreateService implements OnApplicationShutdown { if (!this.isRenote(note) || this.isQuote(note)) { // Increment notes count (user) this.incNotesCountOfUser(user); + } else { + this.usersRepository.update({ id: user.id }, { updatedAt: new Date() }); } this.pushToTl(note, user); diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 1c634602f3..9ce8cb6731 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -127,6 +127,8 @@ export class NoteDeleteService { if (!isRenote(note) || isQuote(note)) { // Decrement notes count (user) this.decNotesCountOfUser(user); + } else { + this.usersRepository.update({ id: user.id }, { updatedAt: new Date() }); } if (this.meta.enableStatsForFederatedInstances) { diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index e9637c56c7..58233b90ee 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -610,6 +610,8 @@ export class NoteEditService implements OnApplicationShutdown { } } + this.usersRepository.update({ id: user.id }, { updatedAt: new Date() }); + // ハッシュタグ更新 this.pushToTl(note, user); diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 6cc4ef4205..373fea8605 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -212,6 +212,8 @@ export class ReactionService { .execute(); } + this.usersRepository.update({ id: user.id }, { updatedAt: new Date() }); + // 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新 if ( Math.random() < 0.3 && @@ -330,6 +332,8 @@ export class ReactionService { .execute(); } + this.usersRepository.update({ id: user.id }, { updatedAt: new Date() }); + this.globalEventService.publishNoteStream(note.id, 'unreacted', { reaction: this.decodeReaction(exist.reaction).reaction, userId: user.id, -- cgit v1.2.3-freya