From bcb5182e8686dcb517defe56a18324fb0ec72027 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 14 Jan 2023 10:48:11 +0900
Subject: [PATCH] =?UTF-8?q?Webhook=E3=81=AE=E4=BD=9C=E6=88=90=E5=8F=AF?=
 =?UTF-8?q?=E8=83=BD=E6=95=B0=E3=82=92=E8=A8=AD=E5=AE=9A=E5=8F=AF=E8=83=BD?=
 =?UTF-8?q?=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                    |  1 +
 locales/ja-JP.yml                               |  1 +
 packages/backend/src/core/RoleService.ts        |  3 +++
 .../server/api/endpoints/i/webhooks/create.ts   | 17 +++++++++++++++++
 .../frontend/src/pages/admin/roles.editor.vue   | 15 +++++++++++++++
 packages/frontend/src/pages/admin/roles.vue     |  9 +++++++++
 6 files changed, 46 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c25b98d27a..0872e875f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -76,6 +76,7 @@ You should also include the user name that made the change.
 - 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo
 - 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo
 - ハードワードミュートの最大文字数を設定可能に @syuilo
+- Webhookの作成可能数を設定可能に @syuilo
 - Server: signToActivityPubGet is set to true by default @syuilo
 - Server: improve syslog performance @syuilo
 - Server: Use undici instead of node-fetch and got @tamaina
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 1982681aed..f0291db54c 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -963,6 +963,7 @@ _role:
     driveCapacity: "ドライブ容量"
     antennaMax: "アンテナの作成可能数"
     wordMuteMax: "ワードミュートの最大文字数"
+    webhookMax: "Webhookの作成可能数"
   _condition:
     isLocal: "ローカルユーザー"
     isRemote: "リモートユーザー"
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index e7821ebd78..c639786ec6 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -22,6 +22,7 @@ export type RoleOptions = {
 	driveCapacityMb: number;
 	antennaLimit: number;
 	wordMuteLimit: number;
+	webhookLimit: number;
 };
 
 export const DEFAULT_ROLE: RoleOptions = {
@@ -33,6 +34,7 @@ export const DEFAULT_ROLE: RoleOptions = {
 	driveCapacityMb: 100,
 	antennaLimit: 5,
 	wordMuteLimit: 200,
+	webhookLimit: 3,
 };
 
 @Injectable()
@@ -203,6 +205,7 @@ export class RoleService implements OnApplicationShutdown {
 			driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')),
 			antennaLimit: Math.max(...getOptionValues('antennaLimit')),
 			wordMuteLimit: Math.max(...getOptionValues('wordMuteLimit')),
+			webhookLimit: Math.max(...getOptionValues('webhookLimit')),
 		};
 	}
 
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
index 584c2ba6a4..45cfd8161c 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -5,6 +5,7 @@ import type { WebhooksRepository } from '@/models/index.js';
 import { webhookEventTypes } from '@/models/entities/Webhook.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
 
 export const meta = {
 	tags: ['webhooks'],
@@ -12,6 +13,14 @@ export const meta = {
 	requireCredential: true,
 
 	kind: 'write:account',
+
+	errors: {
+		tooManyWebhooks: {
+			message: 'You cannot create webhook any more.',
+			code: 'TOO_MANY_WEBHOOKS',
+			id: '87a9bb19-111e-4e37-81d3-a3e7426453b0',
+		},
+	},
 } as const;
 
 export const paramDef = {
@@ -38,8 +47,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 		private idService: IdService,
 		private globalEventService: GlobalEventService,
+		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
+			const currentWebhooksCount = await this.webhooksRepository.countBy({
+				userId: me.id,
+			});
+			if (currentWebhooksCount > (await this.roleService.getUserRoleOptions(me.id)).webhookLimit) {
+				throw new ApiError(meta.errors.tooManyWebhooks);
+			}
+
 			const webhook = await this.webhooksRepository.insert({
 				id: this.idService.genId(),
 				createdAt: new Date(),
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 30bc6c238e..9d26707423 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -140,6 +140,18 @@
 					</MkInput>
 				</div>
 			</MkFolder>
+
+			<MkFolder>
+				<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
+				<template #suffix>{{ options_webhookLimit_useDefault ? i18n.ts._role.useBaseValue : (options_webhookLimit_value) }}</template>
+				<div class="_gaps">
+					<MkSwitch v-model="options_webhookLimit_useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
+					</MkSwitch>
+					<MkInput v-model="options_webhookLimit_value" :disabled="options_webhookLimit_useDefault" type="number" :readonly="readonly">
+					</MkInput>
+				</div>
+			</MkFolder>
 		</div>
 	</FormSlot>
 
@@ -209,6 +221,8 @@ let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefau
 let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? 0);
 let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true);
 let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? 0);
+let options_webhookLimit_useDefault = $ref(role?.options?.webhookLimit?.useDefault ?? true);
+let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? 0);
 
 if (_DEV_) {
 	watch($$(condFormula), () => {
@@ -226,6 +240,7 @@ function getOptions() {
 		driveCapacityMb: { useDefault: options_driveCapacityMb_useDefault, value: options_driveCapacityMb_value },
 		antennaLimit: { useDefault: options_antennaLimit_useDefault, value: options_antennaLimit_value },
 		wordMuteLimit: { useDefault: options_wordMuteLimit_useDefault, value: options_wordMuteLimit_value },
+		webhookLimit: { useDefault: options_webhookLimit_useDefault, value: options_webhookLimit_value },
 	};
 }
 
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 001800ea26..cde5142a63 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -71,6 +71,13 @@
 							</MkInput>
 						</MkFolder>
 
+						<MkFolder>
+							<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
+							<template #suffix>{{ options_webhookLimit }}</template>
+							<MkInput v-model="options_webhookLimit" type="number">
+							</MkInput>
+						</MkFolder>
+
 						<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
 					</div>
 				</MkFolder>
@@ -111,6 +118,7 @@ let options_canManageCustomEmojis = $ref(instance.baseRole.canManageCustomEmojis
 let options_driveCapacityMb = $ref(instance.baseRole.driveCapacityMb);
 let options_antennaLimit = $ref(instance.baseRole.antennaLimit);
 let options_wordMuteLimit = $ref(instance.baseRole.wordMuteLimit);
+let options_webhookLimit = $ref(instance.baseRole.webhookLimit);
 
 async function updateBaseRole() {
 	await os.apiWithDialog('admin/roles/update-default-role-override', {
@@ -123,6 +131,7 @@ async function updateBaseRole() {
 			driveCapacityMb: options_driveCapacityMb,
 			antennaLimit: options_antennaLimit,
 			wordMuteLimit: options_wordMuteLimit,
+			webhookLimit: options_webhookLimit,
 		},
 	});
 }