 61fae45390
			
		
	
	61fae45390
	
	
	
		
			
			* feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする * モデログに対応&エンドポイントを単一オブジェクトでのサポートに変更(API経由で大量に作るシチュエーションもないと思うので) * fix spdx * fix migration * fix migration * fix models * add e2e webhook * tweak * fix modlog * fix bugs * add tests and fix bugs * add tests and fix bugs * add tests * fix path * regenerate locale * 混入除去 * 混入除去 * add abuseReportResolved * fix pnpm-lock.yaml * add abuseReportResolved test * fix bugs * fix ui * add tests * fix CHANGELOG.md * add tests * add RoleService.getModeratorIds tests * WebhookServiceをUserとSystemに分割 * fix CHANGELOG.md * fix test * insertOneを使う用に * fix * regenerate locales * revert version * separate webhook job queue * fix * 🎨 * Update QueueProcessorService.ts --------- Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
		
			
				
	
	
		
			402 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			402 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /*
 | |
|  * SPDX-FileCopyrightText: syuilo and misskey-project
 | |
|  * SPDX-License-Identifier: AGPL-3.0-only
 | |
|  */
 | |
| 
 | |
| import { entities } from 'misskey-js';
 | |
| import { beforeEach, describe, test } from '@jest/globals';
 | |
| import Fastify from 'fastify';
 | |
| import { api, randomString, role, signup, startJobQueue, UserToken } from '../../utils.js';
 | |
| import type { INestApplicationContext } from '@nestjs/common';
 | |
| 
 | |
| const WEBHOOK_HOST = 'http://localhost:15080';
 | |
| const WEBHOOK_PORT = 15080;
 | |
| process.env.NODE_ENV = 'test';
 | |
| 
 | |
| describe('[シナリオ] ユーザ通報', () => {
 | |
| 	let queue: INestApplicationContext;
 | |
| 	let admin: entities.SignupResponse;
 | |
| 	let alice: entities.SignupResponse;
 | |
| 	let bob: entities.SignupResponse;
 | |
| 
 | |
| 	type SystemWebhookPayload = {
 | |
| 		server: string;
 | |
| 		hookId: string;
 | |
| 		eventId: string;
 | |
| 		createdAt: string;
 | |
| 		type: string;
 | |
| 		body: any;
 | |
| 	}
 | |
| 
 | |
| 	// -------------------------------------------------------------------------------------------
 | |
| 
 | |
| 	async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>): Promise<T> {
 | |
| 		const fastify = Fastify();
 | |
| 
 | |
| 		let timeoutHandle: NodeJS.Timeout | null = null;
 | |
| 		const result = await new Promise<string>(async (resolve, reject) => {
 | |
| 			fastify.all('/', async (req, res) => {
 | |
| 				timeoutHandle && clearTimeout(timeoutHandle);
 | |
| 
 | |
| 				const body = JSON.stringify(req.body);
 | |
| 				res.status(200).send('ok');
 | |
| 				await fastify.close();
 | |
| 				resolve(body);
 | |
| 			});
 | |
| 
 | |
| 			await fastify.listen({ port: WEBHOOK_PORT });
 | |
| 
 | |
| 			timeoutHandle = setTimeout(async () => {
 | |
| 				await fastify.close();
 | |
| 				reject(new Error('timeout'));
 | |
| 			}, 3000);
 | |
| 
 | |
| 			try {
 | |
| 				await postAction();
 | |
| 			} catch (e) {
 | |
| 				await fastify.close();
 | |
| 				reject(e);
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		await fastify.close();
 | |
| 
 | |
| 		return JSON.parse(result) as T;
 | |
| 	}
 | |
| 
 | |
| 	async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
 | |
| 		const res = await api(
 | |
| 			'admin/system-webhook/create',
 | |
| 			{
 | |
| 				isActive: true,
 | |
| 				name: randomString(),
 | |
| 				on: ['abuseReport'],
 | |
| 				url: WEBHOOK_HOST,
 | |
| 				secret: randomString(),
 | |
| 				...args,
 | |
| 			},
 | |
| 			credential ?? admin,
 | |
| 		);
 | |
| 		return res.body;
 | |
| 	}
 | |
| 
 | |
| 	async function createAbuseReportNotificationRecipient(args?: Partial<entities.AdminAbuseReportNotificationRecipientCreateRequest>, credential?: UserToken): Promise<entities.AdminAbuseReportNotificationRecipientCreateResponse> {
 | |
| 		const res = await api(
 | |
| 			'admin/abuse-report/notification-recipient/create',
 | |
| 			{
 | |
| 				isActive: true,
 | |
| 				name: randomString(),
 | |
| 				method: 'webhook',
 | |
| 				...args,
 | |
| 			},
 | |
| 			credential ?? admin,
 | |
| 		);
 | |
| 		return res.body;
 | |
| 	}
 | |
| 
 | |
| 	async function createAbuseReport(args?: Partial<entities.UsersReportAbuseRequest>, credential?: UserToken): Promise<entities.EmptyResponse> {
 | |
| 		const res = await api(
 | |
| 			'users/report-abuse',
 | |
| 			{
 | |
| 				userId: alice.id,
 | |
| 				comment: randomString(),
 | |
| 				...args,
 | |
| 			},
 | |
| 			credential ?? admin,
 | |
| 		);
 | |
| 		return res.body;
 | |
| 	}
 | |
| 
 | |
| 	async function resolveAbuseReport(args?: Partial<entities.AdminResolveAbuseUserReportRequest>, credential?: UserToken): Promise<entities.EmptyResponse> {
 | |
| 		const res = await api(
 | |
| 			'admin/resolve-abuse-user-report',
 | |
| 			{
 | |
| 				reportId: admin.id,
 | |
| 				...args,
 | |
| 			},
 | |
| 			credential ?? admin,
 | |
| 		);
 | |
| 		return res.body;
 | |
| 	}
 | |
| 
 | |
| 	// -------------------------------------------------------------------------------------------
 | |
| 
 | |
| 	beforeAll(async () => {
 | |
| 		queue = await startJobQueue();
 | |
| 		admin = await signup({ username: 'admin' });
 | |
| 		alice = await signup({ username: 'alice' });
 | |
| 		bob = await signup({ username: 'bob' });
 | |
| 
 | |
| 		await role(admin, { isAdministrator: true });
 | |
| 	}, 1000 * 60 * 2);
 | |
| 
 | |
| 	afterAll(async () => {
 | |
| 		await queue.close();
 | |
| 	});
 | |
| 
 | |
| 	// -------------------------------------------------------------------------------------------
 | |
| 
 | |
| 	describe('SystemWebhook', () => {
 | |
| 		beforeEach(async () => {
 | |
| 			const webhooks = await api('admin/system-webhook/list', {}, admin);
 | |
| 			for (const webhook of webhooks.body) {
 | |
| 				await api('admin/system-webhook/delete', { id: webhook.id }, admin);
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		test('通報を受けた -> abuseReportが送出される', async () => {
 | |
| 			const webhook = await createSystemWebhook({
 | |
| 				on: ['abuseReport'],
 | |
| 				isActive: true,
 | |
| 			});
 | |
| 			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
 | |
| 
 | |
| 			// 通報(bob -> alice)
 | |
| 			const abuse = {
 | |
| 				userId: alice.id,
 | |
| 				comment: randomString(),
 | |
| 			};
 | |
| 			const webhookBody = await captureWebhook(async () => {
 | |
| 				await createAbuseReport(abuse, bob);
 | |
| 			});
 | |
| 
 | |
| 			console.log(JSON.stringify(webhookBody, null, 2));
 | |
| 
 | |
| 			expect(webhookBody.hookId).toBe(webhook.id);
 | |
| 			expect(webhookBody.type).toBe('abuseReport');
 | |
| 			expect(webhookBody.body.targetUserId).toBe(alice.id);
 | |
| 			expect(webhookBody.body.reporterId).toBe(bob.id);
 | |
| 			expect(webhookBody.body.comment).toBe(abuse.comment);
 | |
| 		});
 | |
| 
 | |
| 		test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが送出される', async () => {
 | |
| 			const webhook = await createSystemWebhook({
 | |
| 				on: ['abuseReport', 'abuseReportResolved'],
 | |
| 				isActive: true,
 | |
| 			});
 | |
| 			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
 | |
| 
 | |
| 			// 通報(bob -> alice)
 | |
| 			const abuse = {
 | |
| 				userId: alice.id,
 | |
| 				comment: randomString(),
 | |
| 			};
 | |
| 			const webhookBody1 = await captureWebhook(async () => {
 | |
| 				await createAbuseReport(abuse, bob);
 | |
| 			});
 | |
| 
 | |
| 			console.log(JSON.stringify(webhookBody1, null, 2));
 | |
| 			expect(webhookBody1.hookId).toBe(webhook.id);
 | |
| 			expect(webhookBody1.type).toBe('abuseReport');
 | |
| 			expect(webhookBody1.body.targetUserId).toBe(alice.id);
 | |
| 			expect(webhookBody1.body.reporterId).toBe(bob.id);
 | |
| 			expect(webhookBody1.body.assigneeId).toBeNull();
 | |
| 			expect(webhookBody1.body.resolved).toBe(false);
 | |
| 			expect(webhookBody1.body.comment).toBe(abuse.comment);
 | |
| 
 | |
| 			// 解決
 | |
| 			const webhookBody2 = await captureWebhook(async () => {
 | |
| 				await resolveAbuseReport({
 | |
| 					reportId: webhookBody1.body.id,
 | |
| 					forward: false,
 | |
| 				}, admin);
 | |
| 			});
 | |
| 
 | |
| 			console.log(JSON.stringify(webhookBody2, null, 2));
 | |
| 			expect(webhookBody2.hookId).toBe(webhook.id);
 | |
| 			expect(webhookBody2.type).toBe('abuseReportResolved');
 | |
| 			expect(webhookBody2.body.targetUserId).toBe(alice.id);
 | |
| 			expect(webhookBody2.body.reporterId).toBe(bob.id);
 | |
| 			expect(webhookBody2.body.assigneeId).toBe(admin.id);
 | |
| 			expect(webhookBody2.body.resolved).toBe(true);
 | |
| 			expect(webhookBody2.body.comment).toBe(abuse.comment);
 | |
| 		});
 | |
| 
 | |
| 		test('通報を受けた -> abuseReportが未許可の場合は送出されない', async () => {
 | |
| 			const webhook = await createSystemWebhook({
 | |
| 				on: [],
 | |
| 				isActive: true,
 | |
| 			});
 | |
| 			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
 | |
| 
 | |
| 			// 通報(bob -> alice)
 | |
| 			const abuse = {
 | |
| 				userId: alice.id,
 | |
| 				comment: randomString(),
 | |
| 			};
 | |
| 			const webhookBody = await captureWebhook(async () => {
 | |
| 				await createAbuseReport(abuse, bob);
 | |
| 			}).catch(e => e.message);
 | |
| 
 | |
| 			expect(webhookBody).toBe('timeout');
 | |
| 		});
 | |
| 
 | |
| 		test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが送出される', async () => {
 | |
| 			const webhook = await createSystemWebhook({
 | |
| 				on: ['abuseReportResolved'],
 | |
| 				isActive: true,
 | |
| 			});
 | |
| 			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
 | |
| 
 | |
| 			// 通報(bob -> alice)
 | |
| 			const abuse = {
 | |
| 				userId: alice.id,
 | |
| 				comment: randomString(),
 | |
| 			};
 | |
| 			const webhookBody1 = await captureWebhook(async () => {
 | |
| 				await createAbuseReport(abuse, bob);
 | |
| 			}).catch(e => e.message);
 | |
| 
 | |
| 			expect(webhookBody1).toBe('timeout');
 | |
| 
 | |
| 			const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
 | |
| 
 | |
| 			// 解決
 | |
| 			const webhookBody2 = await captureWebhook(async () => {
 | |
| 				await resolveAbuseReport({
 | |
| 					reportId: abuseReportId,
 | |
| 					forward: false,
 | |
| 				}, admin);
 | |
| 			});
 | |
| 
 | |
| 			console.log(JSON.stringify(webhookBody2, null, 2));
 | |
| 			expect(webhookBody2.hookId).toBe(webhook.id);
 | |
| 			expect(webhookBody2.type).toBe('abuseReportResolved');
 | |
| 			expect(webhookBody2.body.targetUserId).toBe(alice.id);
 | |
| 			expect(webhookBody2.body.reporterId).toBe(bob.id);
 | |
| 			expect(webhookBody2.body.assigneeId).toBe(admin.id);
 | |
| 			expect(webhookBody2.body.resolved).toBe(true);
 | |
| 			expect(webhookBody2.body.comment).toBe(abuse.comment);
 | |
| 		});
 | |
| 
 | |
| 		test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => {
 | |
| 			const webhook = await createSystemWebhook({
 | |
| 				on: ['abuseReport'],
 | |
| 				isActive: true,
 | |
| 			});
 | |
| 			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
 | |
| 
 | |
| 			// 通報(bob -> alice)
 | |
| 			const abuse = {
 | |
| 				userId: alice.id,
 | |
| 				comment: randomString(),
 | |
| 			};
 | |
| 			const webhookBody1 = await captureWebhook(async () => {
 | |
| 				await createAbuseReport(abuse, bob);
 | |
| 			});
 | |
| 
 | |
| 			console.log(JSON.stringify(webhookBody1, null, 2));
 | |
| 			expect(webhookBody1.hookId).toBe(webhook.id);
 | |
| 			expect(webhookBody1.type).toBe('abuseReport');
 | |
| 			expect(webhookBody1.body.targetUserId).toBe(alice.id);
 | |
| 			expect(webhookBody1.body.reporterId).toBe(bob.id);
 | |
| 			expect(webhookBody1.body.assigneeId).toBeNull();
 | |
| 			expect(webhookBody1.body.resolved).toBe(false);
 | |
| 			expect(webhookBody1.body.comment).toBe(abuse.comment);
 | |
| 
 | |
| 			// 解決
 | |
| 			const webhookBody2 = await captureWebhook(async () => {
 | |
| 				await resolveAbuseReport({
 | |
| 					reportId: webhookBody1.body.id,
 | |
| 					forward: false,
 | |
| 				}, admin);
 | |
| 			}).catch(e => e.message);
 | |
| 
 | |
| 			expect(webhookBody2).toBe('timeout');
 | |
| 		});
 | |
| 
 | |
| 		test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => {
 | |
| 			const webhook = await createSystemWebhook({
 | |
| 				on: [],
 | |
| 				isActive: true,
 | |
| 			});
 | |
| 			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
 | |
| 
 | |
| 			// 通報(bob -> alice)
 | |
| 			const abuse = {
 | |
| 				userId: alice.id,
 | |
| 				comment: randomString(),
 | |
| 			};
 | |
| 			const webhookBody1 = await captureWebhook(async () => {
 | |
| 				await createAbuseReport(abuse, bob);
 | |
| 			}).catch(e => e.message);
 | |
| 
 | |
| 			expect(webhookBody1).toBe('timeout');
 | |
| 
 | |
| 			const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
 | |
| 
 | |
| 			// 解決
 | |
| 			const webhookBody2 = await captureWebhook(async () => {
 | |
| 				await resolveAbuseReport({
 | |
| 					reportId: abuseReportId,
 | |
| 					forward: false,
 | |
| 				}, admin);
 | |
| 			}).catch(e => e.message);
 | |
| 
 | |
| 			expect(webhookBody2).toBe('timeout');
 | |
| 		});
 | |
| 
 | |
| 		test('通報を受けた -> Webhookが無効の場合は送出されない', async () => {
 | |
| 			const webhook = await createSystemWebhook({
 | |
| 				on: ['abuseReport', 'abuseReportResolved'],
 | |
| 				isActive: false,
 | |
| 			});
 | |
| 			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
 | |
| 
 | |
| 			// 通報(bob -> alice)
 | |
| 			const abuse = {
 | |
| 				userId: alice.id,
 | |
| 				comment: randomString(),
 | |
| 			};
 | |
| 			const webhookBody1 = await captureWebhook(async () => {
 | |
| 				await createAbuseReport(abuse, bob);
 | |
| 			}).catch(e => e.message);
 | |
| 
 | |
| 			expect(webhookBody1).toBe('timeout');
 | |
| 
 | |
| 			const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
 | |
| 
 | |
| 			// 解決
 | |
| 			const webhookBody2 = await captureWebhook(async () => {
 | |
| 				await resolveAbuseReport({
 | |
| 					reportId: abuseReportId,
 | |
| 					forward: false,
 | |
| 				}, admin);
 | |
| 			}).catch(e => e.message);
 | |
| 
 | |
| 			expect(webhookBody2).toBe('timeout');
 | |
| 		});
 | |
| 
 | |
| 		test('通報を受けた -> 通知設定が無効の場合は送出されない', async () => {
 | |
| 			const webhook = await createSystemWebhook({
 | |
| 				on: ['abuseReport', 'abuseReportResolved'],
 | |
| 				isActive: true,
 | |
| 			});
 | |
| 			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id, isActive: false });
 | |
| 
 | |
| 			// 通報(bob -> alice)
 | |
| 			const abuse = {
 | |
| 				userId: alice.id,
 | |
| 				comment: randomString(),
 | |
| 			};
 | |
| 			const webhookBody1 = await captureWebhook(async () => {
 | |
| 				await createAbuseReport(abuse, bob);
 | |
| 			}).catch(e => e.message);
 | |
| 
 | |
| 			expect(webhookBody1).toBe('timeout');
 | |
| 
 | |
| 			const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
 | |
| 
 | |
| 			// 解決
 | |
| 			const webhookBody2 = await captureWebhook(async () => {
 | |
| 				await resolveAbuseReport({
 | |
| 					reportId: abuseReportId,
 | |
| 					forward: false,
 | |
| 				}, admin);
 | |
| 			}).catch(e => e.message);
 | |
| 
 | |
| 			expect(webhookBody2).toBe('timeout');
 | |
| 		});
 | |
| 	});
 | |
| });
 |