Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: ユーザ作成時にSystemWebhookを発信できるようにする #14321

Merged
merged 2 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
Expand Down
4 changes: 4 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9392,6 +9392,10 @@ export interface Locale extends ILocale {
* ユーザーからの通報を処理したとき
*/
"abuseReportResolved": string;
/**
* ユーザーが作成されたとき
*/
"userCreated": string;
};
/**
* Webhookを削除しますか?
Expand Down
1 change: 1 addition & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2491,6 +2491,7 @@ _webhookSettings:
_systemEvents:
abuseReport: "ユーザーから通報があったとき"
abuseReportResolved: "ユーザーからの通報を処理したとき"
userCreated: "ユーザーが作成されたとき"
deleteConfirm: "Webhookを削除しますか?"

_abuseReport:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {

/**
* 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
* 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
* 通知先ユーザは{@link getModeratorIds}の取得結果に依る.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

実動作に害はないが、IDEAで見るとエラーになってしまうため

*
* @see RoleService.getModeratorIds
* @see GlobalEventService.publishAdminStream
Expand Down
5 changes: 4 additions & 1 deletion packages/backend/src/core/SignupService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserService } from '@/core/UserService.js';

@Injectable()
export class SignupService {
Expand All @@ -35,6 +36,7 @@ export class SignupService {
private usedUsernamesRepository: UsedUsernamesRepository,

private utilityService: UtilityService,
private userService: UserService,
private userEntityService: UserEntityService,
private idService: IdService,
private metaService: MetaService,
Expand Down Expand Up @@ -148,7 +150,8 @@ export class SignupService {
}));
});

this.usersChart.update(account, true);
this.usersChart.update(account, true).then();
this.userService.notifySystemWebhook(account, 'userCreated').then();

return { account, secret };
}
Expand Down
24 changes: 23 additions & 1 deletion packages/backend/src/core/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';

@Injectable()
export class UserService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,

@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private systemWebhookService: SystemWebhookService,
private userEntityService: UserEntityService,
) {
}

Expand Down Expand Up @@ -50,4 +53,23 @@ export class UserService {
});
}
}

/**
* SystemWebhookを用いてユーザに関する操作内容を管理者各位に通知する.
* ここではJobQueueへのエンキューのみを行うため、即時実行されない.
*
* @see SystemWebhookService.enqueueSystemWebhook
*/
@bindThis
public async notifySystemWebhook(user: MiUser, type: 'userCreated') {
const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' });
const recipientWebhookIds = await this.systemWebhookService.fetchSystemWebhooks({ isActive: true, on: [type] });
for (const webhookId of recipientWebhookIds) {
await this.systemWebhookService.enqueueSystemWebhook(
webhookId,
type,
packedUser,
);
}
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/models/SystemWebhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const systemWebhookEventTypes = [
'abuseReport',
// 通報を処理したとき
'abuseReportResolved',
// ユーザが作成された時
'userCreated',
] as const;
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];

Expand Down
61 changes: 10 additions & 51 deletions packages/backend/test/e2e/synalio/abuse-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,65 +5,24 @@

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 {
api,
captureWebhook,
randomString,
role,
signup,
startJobQueue,
UserToken,
WEBHOOK_HOST,
} 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',
Expand Down
130 changes: 130 additions & 0 deletions packages/backend/test/e2e/synalio/user-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { setTimeout } from 'node:timers/promises';
import { entities } from 'misskey-js';
import { beforeEach, describe, test } from '@jest/globals';
import {
api,
captureWebhook,
randomString,
role,
signup,
startJobQueue,
UserToken,
WEBHOOK_HOST,
} from '../../utils.js';
import type { INestApplicationContext } from '@nestjs/common';

describe('[シナリオ] ユーザ作成', () => {
let queue: INestApplicationContext;
let admin: entities.SignupResponse;

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: ['userCreated'],
url: WEBHOOK_HOST,
secret: randomString(),
...args,
},
credential ?? admin,
);
return res.body;
}

// -------------------------------------------------------------------------------------------

beforeAll(async () => {
queue = await startJobQueue();
admin = await signup({ username: 'admin' });

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('ユーザが作成された -> userCreatedが送出される', async () => {
const webhook = await createSystemWebhook({
on: ['userCreated'],
isActive: true,
});

let alice: any = null;
const webhookBody = await captureWebhook(async () => {
alice = await signup({ username: 'alice' });
});

// webhookの送出後にいろいろやってるのでちょっと待つ必要がある
await setTimeout(2000);

console.log(alice);
console.log(JSON.stringify(webhookBody, null, 2));

expect(webhookBody.hookId).toBe(webhook.id);
expect(webhookBody.type).toBe('userCreated');

const body = webhookBody.body as entities.UserLite;
expect(alice.id).toBe(body.id);
expect(alice.name).toBe(body.name);
expect(alice.username).toBe(body.username);
expect(alice.host).toBe(body.host);
expect(alice.avatarUrl).toBe(body.avatarUrl);
expect(alice.avatarBlurhash).toBe(body.avatarBlurhash);
expect(alice.avatarDecorations).toEqual(body.avatarDecorations);
expect(alice.isBot).toBe(body.isBot);
expect(alice.isCat).toBe(body.isCat);
expect(alice.instance).toEqual(body.instance);
expect(alice.emojis).toEqual(body.emojis);
expect(alice.onlineStatus).toBe(body.onlineStatus);
expect(alice.badgeRoles).toEqual(body.badgeRoles);
});

test('ユーザ作成 -> userCreatedが未許可の場合は送出されない', async () => {
await createSystemWebhook({
on: [],
isActive: true,
});

let alice: any = null;
const webhookBody = await captureWebhook(async () => {
alice = await signup({ username: 'alice' });
}).catch(e => e.message);

expect(webhookBody).toBe('timeout');
expect(alice.id).not.toBeNull();
});

test('ユーザ作成 -> Webhookが無効の場合は送出されない', async () => {
await createSystemWebhook({
on: ['userCreated'],
isActive: false,
});

let alice: any = null;
const webhookBody = await captureWebhook(async () => {
alice = await signup({ username: 'alice' });
}).catch(e => e.message);

expect(webhookBody).toBe('timeout');
expect(alice.id).not.toBeNull();
});
});
});
Loading
Loading