From e48590f53ebae746526dfb096cc6e51bdd3e430b Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 17 Jul 2025 14:52:03 +0900 Subject: [PATCH] n --- .../backend/src/core/AccountUpdateService.ts | 33 ++++++++- .../backend/src/core/UserSuspendService.ts | 20 +---- .../backend/test/unit/UserSuspendService.ts | 73 ++++++++++++++----- 3 files changed, 87 insertions(+), 39 deletions(-) diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 69a57b4854..74f7f0fcab 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { RelayService } from '@/core/RelayService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; @@ -26,16 +26,43 @@ export class AccountUpdateService { ) { } + private async createUpdatePersonActivity(user: MiLocalUser) { + return this.apRendererService.addContext( + this.apRendererService.renderUpdate( + await this.apRendererService.renderPerson(user), user + ) + ); + } + @bindThis public async publishToFollowers(userId: MiUser['id']) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); - // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 + // 投稿者がローカルユーザーならUpdateを配信 if (this.userEntityService.isLocalUser(user)) { - const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); + const content = await this.createUpdatePersonActivity(user); this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); } } + + @bindThis + async publishToFollowersAndSharedInboxAndRelays(userId: MiUser['id']) { + const user = await this.usersRepository.findOneBy({ id: userId }); + if (user == null) throw new Error('user not found'); + + // 投稿者がローカルユーザーならUpdateを配信 + if (this.userEntityService.isLocalUser(user)) { + const content = await this.createUpdatePersonActivity(user); + const manager = this.apDeliverManagerService.createDeliverManager(user, content); + manager.addAllKnowingSharedInboxRecipe(); + manager.addFollowersRecipe(); + + await Promise.allSettled([ + manager.execute(), + this.relayService.deliverToRelays(user, content), + ]); + } + } } diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index 0b62cf2946..61f7731d1b 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -4,14 +4,12 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { Not, IsNull } from 'typeorm'; import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; @@ -29,9 +27,7 @@ export class UserSuspendService { private userEntityService: UserEntityService, private globalEventService: GlobalEventService, - private accountUpadateService: AccountUpdateService, - private apRendererService: ApRendererService, - private apDeliverManagerService: ApDeliverManagerService, + private accountUpdateService: AccountUpdateService, private moderationLogService: ModerationLogService, ) { } @@ -84,12 +80,7 @@ export class UserSuspendService { }); if (this.userEntityService.isLocalUser(user)) { - this.accountUpadateService.publishToFollowers(user.id); - const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); - const manager = this.apDeliverManagerService.createDeliverManager(user, content); - manager.addAllKnowingSharedInboxRecipe(); - manager.addFollowersRecipe(); - manager.execute(); + this.accountUpdateService.publishToFollowersAndSharedInboxAndRelays(user.id); } } @@ -98,12 +89,7 @@ export class UserSuspendService { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); if (this.userEntityService.isLocalUser(user)) { - this.accountUpadateService.publishToFollowers(user.id); - const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user)); - const manager = this.apDeliverManagerService.createDeliverManager(user, content); - manager.addAllKnowingSharedInboxRecipe(); - manager.addFollowersRecipe(); - manager.execute(); + this.accountUpdateService.publishToFollowersAndSharedInboxAndRelays(user.id); } } diff --git a/packages/backend/test/unit/UserSuspendService.ts b/packages/backend/test/unit/UserSuspendService.ts index 8d6b01b730..974b2c1903 100644 --- a/packages/backend/test/unit/UserSuspendService.ts +++ b/packages/backend/test/unit/UserSuspendService.ts @@ -26,6 +26,10 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { randomString } from '../utils.js'; +import { AccountUpdateService } from '@/core/AccountUpdateService.js'; +import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; +import { RelayService } from '@/core/RelayService.js'; +import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js'; function genHost() { return randomString() + '.example.com'; @@ -42,6 +46,9 @@ describe('UserSuspendService', () => { let globalEventService: jest.Mocked; let apRendererService: jest.Mocked; let moderationLogService: jest.Mocked; + let accountUpdateService: jest.Mocked; + let apDeliverManagerService: jest.Mocked; + let relayService: jest.Mocked; async function createUser(data: Partial = {}): Promise { const user = { @@ -84,6 +91,8 @@ describe('UserSuspendService', () => { imports: [GlobalModule], providers: [ UserSuspendService, + AccountUpdateService, + ApDeliverManagerService, { provide: UserEntityService, useFactory: () => ({ @@ -94,6 +103,7 @@ describe('UserSuspendService', () => { { provide: QueueService, useFactory: () => ({ + deliverMany: jest.fn(), deliver: jest.fn(), }), }, @@ -103,20 +113,38 @@ describe('UserSuspendService', () => { publishInternalEvent: jest.fn(), }), }, - { - provide: ApRendererService, - useFactory: () => ({ - addContext: jest.fn(), - renderDelete: jest.fn(), - renderUndo: jest.fn(), - }), - }, { provide: ModerationLogService, useFactory: () => ({ log: jest.fn(), }), }, + { + provide: RelayService, + useFactory: () => ({ + deliverToRelays: jest.fn(), + }), + }, + { + provide: ApRendererService, + useFactory: () => ({ + renderDelete: jest.fn(), + renderUndo: jest.fn(), + renderPerson: jest.fn(), + renderUpdate: jest.fn(), + addContext: jest.fn(), + }), + }, + { + provide: ApLoggerService, + useFactory: () => ({ + logger: { + createSubLogger: jest.fn().mockReturnValue({ + info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), + }), + }, + }), + }, ], }).compile(); @@ -131,6 +159,9 @@ describe('UserSuspendService', () => { globalEventService = app.get(GlobalEventService) as jest.Mocked; apRendererService = app.get(ApRendererService) as jest.Mocked; moderationLogService = app.get(ModerationLogService) as jest.Mocked; + accountUpdateService = app.get(AccountUpdateService) as jest.Mocked; + apDeliverManagerService = app.get(ApDeliverManagerService) as jest.Mocked; + relayService = app.get(RelayService) as jest.Mocked; // Reset mocks jest.clearAllMocks(); @@ -311,42 +342,46 @@ describe('UserSuspendService', () => { }); describe('ActivityPub delivery', () => { - test('should deliver Delete activity on suspend of local user', async () => { + test('should deliver Update Person activity on suspend of local user', async () => { const localUser = await createUser({ host: null }); const moderator = await createUser(); userEntityService.isLocalUser.mockReturnValue(true); userEntityService.genLocalUserUri.mockReturnValue(`https://example.com/users/${localUser.id}`); - apRendererService.renderDelete.mockReturnValue({ type: 'Delete' } as any); - apRendererService.addContext.mockReturnValue({ '@context': '...', type: 'Delete' } as any); + apRendererService.renderUpdate.mockReturnValue({ type: 'Update' } as any); + apRendererService.renderPerson.mockReturnValue({ type: 'Person' } as any); + apRendererService.addContext.mockReturnValue({ '@context': '...', type: 'Update' } as any); await userSuspendService.suspend(localUser, moderator); await setTimeout(250); // ActivityPub配信が呼ばれているかチェック expect(userEntityService.isLocalUser).toHaveBeenCalledWith(localUser); - expect(apRendererService.renderDelete).toHaveBeenCalled(); + expect(apRendererService.renderUpdate).toHaveBeenCalled(); + expect(apRendererService.renderPerson).toHaveBeenCalled(); expect(apRendererService.addContext).toHaveBeenCalled(); + expect(queueService.deliverMany).toHaveBeenCalled(); }); - test('should deliver Undo Delete activity on unsuspend of local user', async () => { + test('should deliver Update Person activity on unsuspend of local user', async () => { const localUser = await createUser({ host: null, isSuspended: true }); const moderator = await createUser(); userEntityService.isLocalUser.mockReturnValue(true); userEntityService.genLocalUserUri.mockReturnValue(`https://example.com/users/${localUser.id}`); - apRendererService.renderDelete.mockReturnValue({ type: 'Delete' } as any); - apRendererService.renderUndo.mockReturnValue({ type: 'Undo' } as any); - apRendererService.addContext.mockReturnValue({ '@context': '...', type: 'Undo' } as any); + apRendererService.renderUpdate.mockReturnValue({ type: 'Update' } as any); + apRendererService.renderPerson.mockReturnValue({ type: 'Person' } as any); + apRendererService.addContext.mockReturnValue({ '@context': '...', type: 'Update' } as any); - await userSuspendService.unsuspend(localUser, moderator); + await userSuspendService.suspend(localUser, moderator); await setTimeout(250); // ActivityPub配信が呼ばれているかチェック expect(userEntityService.isLocalUser).toHaveBeenCalledWith(localUser); - expect(apRendererService.renderDelete).toHaveBeenCalled(); - expect(apRendererService.renderUndo).toHaveBeenCalled(); + expect(apRendererService.renderUpdate).toHaveBeenCalled(); + expect(apRendererService.renderPerson).toHaveBeenCalled(); expect(apRendererService.addContext).toHaveBeenCalled(); + expect(queueService.deliverMany).toHaveBeenCalled(); }); test('should not deliver any activity on suspend of remote user', async () => {