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
This commit is contained in:
zyoshoka 2025-02-26 09:29:12 +09:00 committed by GitHub
parent 2b6638e160
commit 389ec6350b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 127 additions and 23 deletions

View File

@ -27,6 +27,7 @@
- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正 - Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/886) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/886)
- Fix: メールアドレスの形式が正しくなければ以降の処理を行わないように - Fix: メールアドレスの形式が正しくなければ以降の処理を行わないように
- Fix: フォロワーではないユーザーにリートもしくは返信された場合にートのDeleteアクティビティが送られていない問題を修正
## 2025.2.0 ## 2025.2.0

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * 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 { Injectable, Inject } from '@nestjs/common';
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
@ -189,13 +189,27 @@ export class NoteDeleteService {
}) as MiRemoteUser[]; }) 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 @bindThis
private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) {
this.apDeliverManagerService.deliverToFollowers(user, content); this.apDeliverManagerService.deliverToFollowers(user, content);
this.relayService.deliverToRelays(user, content); this.relayService.deliverToRelays(user, content);
const remoteUsers = await this.getMentionedRemoteUsers(note); this.apDeliverManagerService.deliverToUsers(user, content, [
for (const remoteUser of remoteUsers) { ...await this.getMentionedRemoteUsers(note),
this.apDeliverManagerService.deliverToUser(user, content, remoteUser); ...await this.getRenotedOrRepliedRemoteUsers(note),
} ]);
} }
} }

View File

@ -196,6 +196,25 @@ export class ApDeliverManagerService {
await manager.execute(); await manager.execute();
} }
/**
* Deliver activity to users
* @param actor
* @param activity Activity
* @param targets Target users
*/
@bindThis
public async deliverToUsers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, targets: MiRemoteUser[]): Promise<void> {
const manager = new DeliverManager(
this.userEntityService,
this.followingsRepository,
this.queueService,
actor,
activity,
);
for (const to of targets) manager.addDirectRecipe(to);
await manager.execute();
}
@bindThis @bindThis
public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager { public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager {
return new DeliverManager( return new DeliverManager(

View File

@ -139,7 +139,8 @@ describe('Note', () => {
}); });
describe('Deletion', () => { describe('Deletion', () => {
describe('Check Delete consistency', () => { describe('Check Delete is delivered', () => {
describe('To followers', () => {
let carol: LoginUser; let carol: LoginUser;
beforeAll(async () => { beforeAll(async () => {
@ -149,7 +150,7 @@ describe('Note', () => {
await sleep(); await sleep();
}); });
test('Delete is derivered to followers', async () => { test('Check', async () => {
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, carol); const noteInA = await resolveRemoteNote('b.test', note.id, carol);
await bob.client.request('notes/delete', { noteId: note.id }); await bob.client.request('notes/delete', { noteId: note.id });
@ -163,6 +164,75 @@ describe('Note', () => {
}, },
); );
}); });
afterAll(async () => {
await carol.client.request('following/delete', { userId: bobInA.id });
await sleep();
});
});
describe('To renoted and not followed user', () => {
test('Check', async () => {
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, alice);
await alice.client.request('notes/create', { renoteId: noteInA.id });
await sleep();
await bob.client.request('notes/delete', { noteId: note.id });
await sleep();
await rejects(
async () => await alice.client.request('notes/show', { noteId: noteInA.id }),
(err: any) => {
strictEqual(err.code, 'NO_SUCH_NOTE');
return true;
},
);
});
});
describe('To replied and not followed user', () => {
test('Check', async () => {
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, alice);
await alice.client.request('notes/create', { text: 'Hello Bob!', replyId: noteInA.id });
await sleep();
await bob.client.request('notes/delete', { noteId: note.id });
await sleep();
await rejects(
async () => await alice.client.request('notes/show', { noteId: noteInA.id }),
(err: any) => {
strictEqual(err.code, 'NO_SUCH_NOTE');
return true;
},
);
});
});
/**
* FIXME: not delivered
* @see https://github.com/misskey-dev/misskey/issues/15548
*/
describe('To only resolved and not followed user', () => {
test.failing('Check', async () => {
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, alice);
await sleep();
await bob.client.request('notes/delete', { noteId: note.id });
await sleep();
await rejects(
async () => await alice.client.request('notes/show', { noteId: noteInA.id }),
(err: any) => {
strictEqual(err.code, 'NO_SUCH_NOTE');
return true;
},
);
});
});
}); });
describe('Deletion of remote user\'s note for moderation', () => { describe('Deletion of remote user\'s note for moderation', () => {