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>
401 lines
12 KiB
TypeScript
401 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');
|
|
});
|
|
});
|
|
});
|