feature: ユーザ作成時にSystemWebhookを発信できるようにする (#14321)
* feature: ユーザ作成時にSystemWebhookを発信できるようにする * fix CHANGELOG.md
This commit is contained in:
		
							parent
							
								
									0f0660d49e
								
							
						
					
					
						commit
						72bc789746
					
				
					 12 changed files with 237 additions and 62 deletions
				
			
		| 
						 | 
					@ -8,6 +8,7 @@
 | 
				
			||||||
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
 | 
					- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
 | 
				
			||||||
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
 | 
					- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
 | 
				
			||||||
  - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
 | 
					  - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
 | 
				
			||||||
 | 
					- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
 | 
				
			||||||
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
 | 
					- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
 | 
				
			||||||
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
 | 
					- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
 | 
				
			||||||
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
 | 
					- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										4
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -9392,6 +9392,10 @@ export interface Locale extends ILocale {
 | 
				
			||||||
             * ユーザーからの通報を処理したとき
 | 
					             * ユーザーからの通報を処理したとき
 | 
				
			||||||
             */
 | 
					             */
 | 
				
			||||||
            "abuseReportResolved": string;
 | 
					            "abuseReportResolved": string;
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					             * ユーザーが作成されたとき
 | 
				
			||||||
 | 
					             */
 | 
				
			||||||
 | 
					            "userCreated": string;
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Webhookを削除しますか?
 | 
					         * Webhookを削除しますか?
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2491,6 +2491,7 @@ _webhookSettings:
 | 
				
			||||||
  _systemEvents:
 | 
					  _systemEvents:
 | 
				
			||||||
    abuseReport: "ユーザーから通報があったとき"
 | 
					    abuseReport: "ユーザーから通報があったとき"
 | 
				
			||||||
    abuseReportResolved: "ユーザーからの通報を処理したとき"
 | 
					    abuseReportResolved: "ユーザーからの通報を処理したとき"
 | 
				
			||||||
 | 
					    userCreated: "ユーザーが作成されたとき"
 | 
				
			||||||
  deleteConfirm: "Webhookを削除しますか?"
 | 
					  deleteConfirm: "Webhookを削除しますか?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_abuseReport:
 | 
					_abuseReport:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
 | 
						 * 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
 | 
				
			||||||
	 * 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
 | 
						 * 通知先ユーザは{@link getModeratorIds}の取得結果に依る.
 | 
				
			||||||
	 *
 | 
						 *
 | 
				
			||||||
	 * @see RoleService.getModeratorIds
 | 
						 * @see RoleService.getModeratorIds
 | 
				
			||||||
	 * @see GlobalEventService.publishAdminStream
 | 
						 * @see GlobalEventService.publishAdminStream
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,7 @@ import { bindThis } from '@/decorators.js';
 | 
				
			||||||
import UsersChart from '@/core/chart/charts/users.js';
 | 
					import UsersChart from '@/core/chart/charts/users.js';
 | 
				
			||||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
					import { UtilityService } from '@/core/UtilityService.js';
 | 
				
			||||||
import { MetaService } from '@/core/MetaService.js';
 | 
					import { MetaService } from '@/core/MetaService.js';
 | 
				
			||||||
 | 
					import { UserService } from '@/core/UserService.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class SignupService {
 | 
					export class SignupService {
 | 
				
			||||||
| 
						 | 
					@ -35,6 +36,7 @@ export class SignupService {
 | 
				
			||||||
		private usedUsernamesRepository: UsedUsernamesRepository,
 | 
							private usedUsernamesRepository: UsedUsernamesRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private utilityService: UtilityService,
 | 
							private utilityService: UtilityService,
 | 
				
			||||||
 | 
							private userService: UserService,
 | 
				
			||||||
		private userEntityService: UserEntityService,
 | 
							private userEntityService: UserEntityService,
 | 
				
			||||||
		private idService: IdService,
 | 
							private idService: IdService,
 | 
				
			||||||
		private metaService: MetaService,
 | 
							private metaService: MetaService,
 | 
				
			||||||
| 
						 | 
					@ -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 };
 | 
							return { account, secret };
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,15 +8,18 @@ import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
 | 
				
			||||||
import type { MiUser } from '@/models/User.js';
 | 
					import type { MiUser } from '@/models/User.js';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import { bindThis } from '@/decorators.js';
 | 
					import { bindThis } from '@/decorators.js';
 | 
				
			||||||
 | 
					import { SystemWebhookService } from '@/core/SystemWebhookService.js';
 | 
				
			||||||
 | 
					import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class UserService {
 | 
					export class UserService {
 | 
				
			||||||
	constructor(
 | 
						constructor(
 | 
				
			||||||
		@Inject(DI.usersRepository)
 | 
							@Inject(DI.usersRepository)
 | 
				
			||||||
		private usersRepository: UsersRepository,
 | 
							private usersRepository: UsersRepository,
 | 
				
			||||||
 | 
					 | 
				
			||||||
		@Inject(DI.followingsRepository)
 | 
							@Inject(DI.followingsRepository)
 | 
				
			||||||
		private followingsRepository: FollowingsRepository,
 | 
							private followingsRepository: FollowingsRepository,
 | 
				
			||||||
 | 
							private systemWebhookService: SystemWebhookService,
 | 
				
			||||||
 | 
							private userEntityService: UserEntityService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,8 @@ export const systemWebhookEventTypes = [
 | 
				
			||||||
	'abuseReport',
 | 
						'abuseReport',
 | 
				
			||||||
	// 通報を処理したとき
 | 
						// 通報を処理したとき
 | 
				
			||||||
	'abuseReportResolved',
 | 
						'abuseReportResolved',
 | 
				
			||||||
 | 
						// ユーザが作成された時
 | 
				
			||||||
 | 
						'userCreated',
 | 
				
			||||||
] as const;
 | 
					] as const;
 | 
				
			||||||
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
 | 
					export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,65 +5,24 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { entities } from 'misskey-js';
 | 
					import { entities } from 'misskey-js';
 | 
				
			||||||
import { beforeEach, describe, test } from '@jest/globals';
 | 
					import { beforeEach, describe, test } from '@jest/globals';
 | 
				
			||||||
import Fastify from 'fastify';
 | 
					import {
 | 
				
			||||||
import { api, randomString, role, signup, startJobQueue, UserToken } from '../../utils.js';
 | 
						api,
 | 
				
			||||||
 | 
						captureWebhook,
 | 
				
			||||||
 | 
						randomString,
 | 
				
			||||||
 | 
						role,
 | 
				
			||||||
 | 
						signup,
 | 
				
			||||||
 | 
						startJobQueue,
 | 
				
			||||||
 | 
						UserToken,
 | 
				
			||||||
 | 
						WEBHOOK_HOST,
 | 
				
			||||||
 | 
					} from '../../utils.js';
 | 
				
			||||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
					import type { INestApplicationContext } from '@nestjs/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const WEBHOOK_HOST = 'http://localhost:15080';
 | 
					 | 
				
			||||||
const WEBHOOK_PORT = 15080;
 | 
					 | 
				
			||||||
process.env.NODE_ENV = 'test';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe('[シナリオ] ユーザ通報', () => {
 | 
					describe('[シナリオ] ユーザ通報', () => {
 | 
				
			||||||
	let queue: INestApplicationContext;
 | 
						let queue: INestApplicationContext;
 | 
				
			||||||
	let admin: entities.SignupResponse;
 | 
						let admin: entities.SignupResponse;
 | 
				
			||||||
	let alice: entities.SignupResponse;
 | 
						let alice: entities.SignupResponse;
 | 
				
			||||||
	let bob: 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> {
 | 
						async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
 | 
				
			||||||
		const res = await api(
 | 
							const res = await api(
 | 
				
			||||||
			'admin/system-webhook/create',
 | 
								'admin/system-webhook/create',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										130
									
								
								packages/backend/test/e2e/synalio/user-create.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								packages/backend/test/e2e/synalio/user-create.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -12,13 +12,14 @@ import WebSocket, { ClientOptions } from 'ws';
 | 
				
			||||||
import fetch, { File, RequestInit, type Headers } from 'node-fetch';
 | 
					import fetch, { File, RequestInit, type Headers } from 'node-fetch';
 | 
				
			||||||
import { DataSource } from 'typeorm';
 | 
					import { DataSource } from 'typeorm';
 | 
				
			||||||
import { JSDOM } from 'jsdom';
 | 
					import { JSDOM } from 'jsdom';
 | 
				
			||||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
 | 
					import { type Response } from 'node-fetch';
 | 
				
			||||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
 | 
					import Fastify from 'fastify';
 | 
				
			||||||
import { entities } from '../src/postgres.js';
 | 
					import { entities } from '../src/postgres.js';
 | 
				
			||||||
import { loadConfig } from '../src/config.js';
 | 
					import { loadConfig } from '../src/config.js';
 | 
				
			||||||
import type * as misskey from 'misskey-js';
 | 
					import type * as misskey from 'misskey-js';
 | 
				
			||||||
import { type Response } from 'node-fetch';
 | 
					import { DEFAULT_POLICIES } from '@/core/RoleService.js';
 | 
				
			||||||
import { ApiError } from "@/server/api/error.js";
 | 
					import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
 | 
				
			||||||
 | 
					import { ApiError } from '@/server/api/error.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
 | 
					export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,11 +28,23 @@ export interface UserToken {
 | 
				
			||||||
	bearer?: boolean;
 | 
						bearer?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SystemWebhookPayload = {
 | 
				
			||||||
 | 
						server: string;
 | 
				
			||||||
 | 
						hookId: string;
 | 
				
			||||||
 | 
						eventId: string;
 | 
				
			||||||
 | 
						createdAt: string;
 | 
				
			||||||
 | 
						type: string;
 | 
				
			||||||
 | 
						body: any;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const config = loadConfig();
 | 
					const config = loadConfig();
 | 
				
			||||||
export const port = config.port;
 | 
					export const port = config.port;
 | 
				
			||||||
export const origin = config.url;
 | 
					export const origin = config.url;
 | 
				
			||||||
export const host = new URL(config.url).host;
 | 
					export const host = new URL(config.url).host;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const WEBHOOK_HOST = 'http://localhost:15080';
 | 
				
			||||||
 | 
					export const WEBHOOK_PORT = 15080;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const cookie = (me: UserToken): string => {
 | 
					export const cookie = (me: UserToken): string => {
 | 
				
			||||||
	return `token=${me.token};`;
 | 
						return `token=${me.token};`;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -645,3 +658,37 @@ export async function sendEnvResetRequest() {
 | 
				
			||||||
export function castAsError(obj: Record<string, unknown>): { error: ApiError } {
 | 
					export function castAsError(obj: Record<string, unknown>): { error: ApiError } {
 | 
				
			||||||
	return obj as { error: ApiError };
 | 
						return obj as { error: ApiError };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>, port = WEBHOOK_PORT): 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 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,6 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
					<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
 | 
										<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
 | 
				
			||||||
						<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
 | 
											<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
 | 
				
			||||||
					</MkSwitch>
 | 
										</MkSwitch>
 | 
				
			||||||
 | 
										<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
 | 
				
			||||||
 | 
											<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
 | 
				
			||||||
 | 
										</MkSwitch>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</MkFolder>
 | 
								</MkFolder>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,6 +81,7 @@ import * as os from '@/os.js';
 | 
				
			||||||
type EventType = {
 | 
					type EventType = {
 | 
				
			||||||
	abuseReport: boolean;
 | 
						abuseReport: boolean;
 | 
				
			||||||
	abuseReportResolved: boolean;
 | 
						abuseReportResolved: boolean;
 | 
				
			||||||
 | 
						userCreated: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const emit = defineEmits<{
 | 
					const emit = defineEmits<{
 | 
				
			||||||
| 
						 | 
					@ -100,12 +104,14 @@ const secret = ref<string>('');
 | 
				
			||||||
const events = ref<EventType>({
 | 
					const events = ref<EventType>({
 | 
				
			||||||
	abuseReport: true,
 | 
						abuseReport: true,
 | 
				
			||||||
	abuseReportResolved: true,
 | 
						abuseReportResolved: true,
 | 
				
			||||||
 | 
						userCreated: true,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
const isActive = ref<boolean>(true);
 | 
					const isActive = ref<boolean>(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const disabledEvents = ref<EventType>({
 | 
					const disabledEvents = ref<EventType>({
 | 
				
			||||||
	abuseReport: false,
 | 
						abuseReport: false,
 | 
				
			||||||
	abuseReportResolved: false,
 | 
						abuseReportResolved: false,
 | 
				
			||||||
 | 
						userCreated: false,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const disableSubmitButton = computed(() => {
 | 
					const disableSubmitButton = computed(() => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4970,7 +4970,7 @@ export type components = {
 | 
				
			||||||
      latestSentAt: string | null;
 | 
					      latestSentAt: string | null;
 | 
				
			||||||
      latestStatus: number | null;
 | 
					      latestStatus: number | null;
 | 
				
			||||||
      name: string;
 | 
					      name: string;
 | 
				
			||||||
      on: ('abuseReport' | 'abuseReportResolved')[];
 | 
					      on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
 | 
				
			||||||
      url: string;
 | 
					      url: string;
 | 
				
			||||||
      secret: string;
 | 
					      secret: string;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
| 
						 | 
					@ -10042,7 +10042,7 @@ export type operations = {
 | 
				
			||||||
        'application/json': {
 | 
					        'application/json': {
 | 
				
			||||||
          isActive: boolean;
 | 
					          isActive: boolean;
 | 
				
			||||||
          name: string;
 | 
					          name: string;
 | 
				
			||||||
          on: ('abuseReport' | 'abuseReportResolved')[];
 | 
					          on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
 | 
				
			||||||
          url: string;
 | 
					          url: string;
 | 
				
			||||||
          secret: string;
 | 
					          secret: string;
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
| 
						 | 
					@ -10152,7 +10152,7 @@ export type operations = {
 | 
				
			||||||
      content: {
 | 
					      content: {
 | 
				
			||||||
        'application/json': {
 | 
					        'application/json': {
 | 
				
			||||||
          isActive?: boolean;
 | 
					          isActive?: boolean;
 | 
				
			||||||
          on?: ('abuseReport' | 'abuseReportResolved')[];
 | 
					          on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
| 
						 | 
					@ -10265,7 +10265,7 @@ export type operations = {
 | 
				
			||||||
          id: string;
 | 
					          id: string;
 | 
				
			||||||
          isActive: boolean;
 | 
					          isActive: boolean;
 | 
				
			||||||
          name: string;
 | 
					          name: string;
 | 
				
			||||||
          on: ('abuseReport' | 'abuseReportResolved')[];
 | 
					          on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
 | 
				
			||||||
          url: string;
 | 
					          url: string;
 | 
				
			||||||
          secret: string;
 | 
					          secret: string;
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue