This commit is contained in:
tamaina 2025-07-17 14:52:03 +09:00
parent e6112c092b
commit e48590f53e
3 changed files with 87 additions and 39 deletions

View File

@ -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),
]);
}
}
}

View File

@ -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);
}
}

View File

@ -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<GlobalEventService>;
let apRendererService: jest.Mocked<ApRendererService>;
let moderationLogService: jest.Mocked<ModerationLogService>;
let accountUpdateService: jest.Mocked<AccountUpdateService>;
let apDeliverManagerService: jest.Mocked<ApDeliverManagerService>;
let relayService: jest.Mocked<RelayService>;
async function createUser(data: Partial<MiUser> = {}): Promise<MiUser> {
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>(GlobalEventService) as jest.Mocked<GlobalEventService>;
apRendererService = app.get<ApRendererService>(ApRendererService) as jest.Mocked<ApRendererService>;
moderationLogService = app.get<ModerationLogService>(ModerationLogService) as jest.Mocked<ModerationLogService>;
accountUpdateService = app.get<AccountUpdateService>(AccountUpdateService) as jest.Mocked<AccountUpdateService>;
apDeliverManagerService = app.get<ApDeliverManagerService>(ApDeliverManagerService) as jest.Mocked<ApDeliverManagerService>;
relayService = app.get<RelayService>(RelayService) as jest.Mocked<RelayService>;
// 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 () => {