From 61fae45390283aee7ac582aa5303aae863de0f7a Mon Sep 17 00:00:00 2001
From: おさむのひと <46447427+samunohito@users.noreply.github.com>
Date: Sat, 8 Jun 2024 15:34:19 +0900
Subject: feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする
 (#13758)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 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

* :art:

* Update QueueProcessorService.ts

---------

Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 .../src/components/MkSystemWebhookEditor.vue       | 217 +++++++++++++++++++++
 1 file changed, 217 insertions(+)
 create mode 100644 packages/frontend/src/components/MkSystemWebhookEditor.vue

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
new file mode 100644
index 0000000000..007d841f00
--- /dev/null
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -0,0 +1,217 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+	:width="450"
+	:height="590"
+	:canClose="true"
+	:withOkButton="false"
+	:okButtonDisabled="false"
+	@click="onCancelClicked"
+	@close="onCancelClicked"
+	@closed="onCancelClicked"
+>
+	<template #header>
+		{{ mode === 'create' ? i18n.ts._webhookSettings.createWebhook : i18n.ts._webhookSettings.modifyWebhook }}
+	</template>
+	<MkSpacer :marginMin="20" :marginMax="28">
+		<MkLoading v-if="loading !== 0"/>
+		<div v-else :class="$style.root" class="_gaps_m">
+			<MkInput v-model="title">
+				<template #label>{{ i18n.ts._webhookSettings.name }}</template>
+			</MkInput>
+			<MkInput v-model="url">
+				<template #label>URL</template>
+			</MkInput>
+			<MkInput v-model="secret">
+				<template #label>{{ i18n.ts._webhookSettings.secret }}</template>
+			</MkInput>
+			<MkFolder :defaultOpen="true">
+				<template #label>{{ i18n.ts._webhookSettings.events }}</template>
+
+				<div class="_gaps_s">
+					<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
+						<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
+					</MkSwitch>
+					<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
+						<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
+					</MkSwitch>
+				</div>
+			</MkFolder>
+
+			<MkSwitch v-model="isActive">
+				<template #label>{{ i18n.ts.enable }}</template>
+			</MkSwitch>
+
+			<div :class="$style.footer" class="_buttonsCenter">
+				<MkButton primary :disabled="disableSubmitButton" @click="onSubmitClicked">
+					<i class="ti ti-check"></i>
+					{{ i18n.ts.ok }}
+				</MkButton>
+				<MkButton @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
+			</div>
+		</div>
+	</MkSpacer>
+</MkModalWindow>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref, toRefs } from 'vue';
+import FormSection from '@/components/form/section.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import {
+	MkSystemWebhookEditorProps,
+	MkSystemWebhookResult,
+	SystemWebhookEventType,
+} from '@/components/MkSystemWebhookEditor.impl.js';
+import { i18n } from '@/i18n.js';
+import MkButton from '@/components/MkButton.vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import * as os from '@/os.js';
+
+type EventType = {
+	abuseReport: boolean;
+	abuseReportResolved: boolean;
+}
+
+const emit = defineEmits<{
+	(ev: 'submitted', result: MkSystemWebhookResult): void;
+	(ev: 'closed'): void;
+}>();
+
+const props = defineProps<MkSystemWebhookEditorProps>();
+
+const { mode, id, requiredEvents } = toRefs(props);
+
+const loading = ref<number>(0);
+
+const title = ref<string>('');
+const url = ref<string>('');
+const secret = ref<string>('');
+const events = ref<EventType>({
+	abuseReport: true,
+	abuseReportResolved: true,
+});
+const isActive = ref<boolean>(true);
+
+const disabledEvents = ref<EventType>({
+	abuseReport: false,
+	abuseReportResolved: false,
+});
+
+const disableSubmitButton = computed(() => {
+	if (!title.value) {
+		return true;
+	}
+	if (!url.value) {
+		return true;
+	}
+	if (!secret.value) {
+		return true;
+	}
+
+	return false;
+});
+
+async function onSubmitClicked() {
+	await loadingScope(async () => {
+		const params = {
+			isActive: isActive.value,
+			name: title.value,
+			url: url.value,
+			secret: secret.value,
+			on: Object.keys(events.value).filter(ev => events.value[ev as keyof EventType]) as SystemWebhookEventType[],
+		};
+
+		try {
+			switch (mode.value) {
+				case 'create': {
+					const result = await misskeyApi('admin/system-webhook/create', params);
+					emit('submitted', result);
+					break;
+				}
+				case 'edit': {
+					// eslint-disable-next-line
+					const result = await misskeyApi('admin/system-webhook/update', { id: id.value!, ...params });
+					emit('submitted', result);
+					break;
+				}
+			}
+			// eslint-disable-next-line
+		} catch (ex: any) {
+			const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
+			await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
+			emit('closed');
+		}
+	});
+}
+
+function onCancelClicked() {
+	emit('closed');
+}
+
+async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
+	loading.value++;
+	try {
+		return await fn();
+	} finally {
+		loading.value--;
+	}
+}
+
+onMounted(async () => {
+	await loadingScope(async () => {
+		switch (mode.value) {
+			case 'edit': {
+				if (!id.value) {
+					throw new Error('id is required');
+				}
+
+				try {
+					const res = await misskeyApi('admin/system-webhook/show', { id: id.value });
+
+					title.value = res.name;
+					url.value = res.url;
+					secret.value = res.secret;
+					isActive.value = res.isActive;
+					for (const ev of Object.keys(events.value)) {
+						events.value[ev] = res.on.includes(ev as SystemWebhookEventType);
+					}
+					// eslint-disable-next-line
+				} catch (ex: any) {
+					const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
+					await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
+					emit('closed');
+				}
+				break;
+			}
+		}
+
+		for (const ev of requiredEvents.value ?? []) {
+			disabledEvents.value[ev] = true;
+		}
+	});
+});
+</script>
+
+<style module lang="scss">
+.root {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: stretch;
+}
+
+.footer {
+	display: flex;
+	justify-content: center;
+	align-items: flex-end;
+	margin-top: 20px;
+}
+</style>
-- 
cgit v1.2.3-freya


From 22c4e9d7ecf5a659832ea32244e46a40627bfc8f Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sat, 27 Jul 2024 18:09:57 +0900
Subject: fix(frontend): modalが正しく閉じられていないのを修正 (#14307)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend): modalが正しく閉じられていないのを修正

* Update packages/frontend/src/components/MkSystemWebhookEditor.vue

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 .../src/components/MkSystemWebhookEditor.impl.ts   | 13 +++++-----
 .../src/components/MkSystemWebhookEditor.vue       | 22 ++++++++++------
 .../frontend/src/components/MkUserSelectDialog.vue |  8 +++---
 .../abuse-report/notification-recipient.editor.vue | 29 ++++++++++++++--------
 .../admin/abuse-report/notification-recipient.vue  | 15 +++++------
 5 files changed, 53 insertions(+), 34 deletions(-)

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
index 76f54e8d37..69b8edd85a 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
@@ -24,22 +24,23 @@ export type MkSystemWebhookResult = {
 };
 
 export async function showSystemWebhookEditorDialog(props: MkSystemWebhookEditorProps): Promise<MkSystemWebhookResult | null> {
-	const { dispose, result } = await new Promise<{ dispose: () => void, result: MkSystemWebhookResult | null }>(async resolve => {
-		const { dispose: _dispose } = os.popup(
+	const { result } = await new Promise<{ result: MkSystemWebhookResult | null }>(async resolve => {
+		const { dispose } = os.popup(
 			defineAsyncComponent(() => import('@/components/MkSystemWebhookEditor.vue')),
 			props,
 			{
 				submitted: (ev: MkSystemWebhookResult) => {
-					resolve({ dispose: _dispose, result: ev });
+					resolve({ result: ev });
+				},
+				canceled: () => {
+					resolve({ result: null });
 				},
 				closed: () => {
-					resolve({ dispose: _dispose, result: null });
+					dispose();
 				},
 			},
 		);
 	});
 
-	dispose();
-
 	return result;
 }
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index 007d841f00..3e6a015018 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -5,6 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkModalWindow
+	ref="dialogEl"
 	:width="450"
 	:height="590"
 	:canClose="true"
@@ -12,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:okButtonDisabled="false"
 	@click="onCancelClicked"
 	@close="onCancelClicked"
-	@closed="onCancelClicked"
+	@closed="emit('closed')"
 >
 	<template #header>
 		{{ mode === 'create' ? i18n.ts._webhookSettings.createWebhook : i18n.ts._webhookSettings.modifyWebhook }}
@@ -59,8 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted, ref, toRefs } from 'vue';
-import FormSection from '@/components/form/section.vue';
+import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
 import MkInput from '@/components/MkInput.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import {
@@ -82,9 +82,12 @@ type EventType = {
 
 const emit = defineEmits<{
 	(ev: 'submitted', result: MkSystemWebhookResult): void;
+	(ev: 'canceled'): void;
 	(ev: 'closed'): void;
 }>();
 
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+
 const props = defineProps<MkSystemWebhookEditorProps>();
 
 const { mode, id, requiredEvents } = toRefs(props);
@@ -133,12 +136,14 @@ async function onSubmitClicked() {
 			switch (mode.value) {
 				case 'create': {
 					const result = await misskeyApi('admin/system-webhook/create', params);
+					dialogEl.value?.close();
 					emit('submitted', result);
 					break;
 				}
 				case 'edit': {
 					// eslint-disable-next-line
 					const result = await misskeyApi('admin/system-webhook/update', { id: id.value!, ...params });
+					dialogEl.value?.close();
 					emit('submitted', result);
 					break;
 				}
@@ -147,13 +152,15 @@ async function onSubmitClicked() {
 		} catch (ex: any) {
 			const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
 			await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
-			emit('closed');
+			dialogEl.value?.close();
+			emit('canceled');
 		}
 	});
 }
 
 function onCancelClicked() {
-	emit('closed');
+	dialogEl.value?.close();
+	emit('canceled');
 }
 
 async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
@@ -183,11 +190,12 @@ onMounted(async () => {
 					for (const ev of Object.keys(events.value)) {
 						events.value[ev] = res.on.includes(ev as SystemWebhookEventType);
 					}
-					// eslint-disable-next-line
+					// eslint-disable-next-line @typescript-eslint/no-explicit-any
 				} catch (ex: any) {
 					const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
 					await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
-					emit('closed');
+					dialogEl.value?.close();
+					emit('canceled');
 				}
 				break;
 			}
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index cc3ea78ffe..cbb40924f6 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkInput from '@/components/MkInput.vue';
 import FormSplit from '@/components/form/split.vue';
@@ -91,7 +91,7 @@ const host = ref('');
 const users = ref<Misskey.entities.UserLite[]>([]);
 const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
 const selected = ref<Misskey.entities.UserLite | null>(null);
-const dialogEl = ref();
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
 
 function search() {
 	if (username.value === '' && host.value === '') {
@@ -122,7 +122,7 @@ async function ok() {
 	});
 	emit('ok', user);
 
-	dialogEl.value.close();
+	dialogEl.value?.close();
 
 	// 最近使ったユーザー更新
 	let recents = defaultStore.state.recentlyUsedUsers;
@@ -133,7 +133,7 @@ async function ok() {
 
 function cancel() {
 	emit('cancel');
-	dialogEl.value.close();
+	dialogEl.value?.close();
 }
 
 onMounted(() => {
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
index ffe9c620d6..015109f54e 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkModalWindow
-	ref="dialog"
+	ref="dialogEl"
 	:width="400"
 	:height="490"
 	:withOkButton="false"
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, ref, toRefs } from 'vue';
+import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
 import { entities } from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -88,6 +88,7 @@ type NotificationRecipientMethod = 'email' | 'webhook';
 
 const emit = defineEmits<{
 	(ev: 'submitted'): void;
+	(ev: 'canceled'): void;
 	(ev: 'closed'): void;
 }>();
 
@@ -98,6 +99,8 @@ const props = defineProps<{
 
 const { mode, id } = toRefs(props);
 
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+
 const loading = ref<number>(0);
 
 const title = ref<string>('');
@@ -166,18 +169,21 @@ async function onSubmitClicked() {
 				}
 			}
 
+			dialogEl.value?.close();
 			emit('submitted');
-			// eslint-disable-next-line
+			// eslint-disable-next-line @typescript-eslint/no-explicit-any
 		} catch (ex: any) {
 			const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
 			await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
-			emit('closed');
+			dialogEl.value?.close();
+			emit('canceled');
 		}
 	});
 }
 
 function onCancelClicked() {
-	emit('closed');
+	dialogEl.value?.close();
+	emit('canceled');
 }
 
 async function onEditSystemWebhookClicked() {
@@ -262,7 +268,8 @@ onMounted(async () => {
 			} catch (ex: any) {
 				const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
 				await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
-				emit('closed');
+				dialogEl.value?.close();
+				emit('canceled');
 			}
 		} else {
 			userId.value = moderators.value[0]?.id ?? null;
@@ -296,11 +303,13 @@ onMounted(async () => {
 	gap: 8px;
 
 	button {
-		width: 2.5em;
-		height: 2.5em;
-		min-width: 2.5em;
-		min-height: 2.5em;
+		min-width: 0;
+		min-height: 0;
+		width: 34px;
+		height: 34px;
+		flex-shrink: 0;
 		box-sizing: border-box;
+		margin: 1px 0;
 		padding: 6px;
 	}
 }
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
index 93800873f9..f5249261be 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
@@ -108,26 +108,27 @@ async function onDeleteButtonClicked(id: string) {
 }
 
 async function showEditor(mode: 'create' | 'edit', id?: string) {
-	const { dispose, needLoad } = await new Promise<{ dispose: () => void, needLoad: boolean }>(async resolve => {
-		const { dispose: _dispose } = os.popup(
+	const { needLoad } = await new Promise<{ needLoad: boolean }>(async resolve => {
+		const { dispose } = os.popup(
 			defineAsyncComponent(() => import('./notification-recipient.editor.vue')),
 			{
 				mode,
 				id,
 			},
 			{
-				submitted: async () => {
-					resolve({ dispose: _dispose, needLoad: true });
+				submitted: () => {
+					resolve({ needLoad: true });
+				},
+				canceled: () => {
+					resolve({ needLoad: false });
 				},
 				closed: () => {
-					resolve({ dispose: _dispose, needLoad: false });
+					dispose();
 				},
 			},
 		);
 	});
 
-	dispose();
-
 	if (needLoad) {
 		await fetchRecipients();
 	}
-- 
cgit v1.2.3-freya


From 72bc78974657b22ab6b1f5a36f6144c294e36de3 Mon Sep 17 00:00:00 2001
From: おさむのひと <46447427+samunohito@users.noreply.github.com>
Date: Mon, 29 Jul 2024 21:31:32 +0900
Subject: feature: ユーザ作成時にSystemWebhookを発信できるようにする (#14321)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

* fix CHANGELOG.md
---
 CHANGELOG.md                                       |   1 +
 locales/index.d.ts                                 |   4 +
 locales/ja-JP.yml                                  |   1 +
 .../src/core/AbuseReportNotificationService.ts     |   2 +-
 packages/backend/src/core/SignupService.ts         |   5 +-
 packages/backend/src/core/UserService.ts           |  24 +++-
 packages/backend/src/models/SystemWebhook.ts       |   2 +
 packages/backend/test/e2e/synalio/abuse-report.ts  |  61 ++--------
 packages/backend/test/e2e/synalio/user-create.ts   | 130 +++++++++++++++++++++
 packages/backend/test/utils.ts                     |  55 ++++++++-
 .../src/components/MkSystemWebhookEditor.vue       |   6 +
 packages/misskey-js/src/autogen/types.ts           |   8 +-
 12 files changed, 237 insertions(+), 62 deletions(-)
 create mode 100644 packages/backend/test/e2e/synalio/user-create.ts

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2e9c8e258..17c233e358 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
 - Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
 - Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
   - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
+- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
 - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
 - Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
 - Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 55c65f2aed..2b340ecbb5 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -9392,6 +9392,10 @@ export interface Locale extends ILocale {
              * ユーザーからの通報を処理したとき
              */
             "abuseReportResolved": string;
+            /**
+             * ユーザーが作成されたとき
+             */
+            "userCreated": string;
         };
         /**
          * Webhookを削除しますか？
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 3ca4b46682..f0f849fb38 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2491,6 +2491,7 @@ _webhookSettings:
   _systemEvents:
     abuseReport: "ユーザーから通報があったとき"
     abuseReportResolved: "ユーザーからの通報を処理したとき"
+    userCreated: "ユーザーが作成されたとき"
   deleteConfirm: "Webhookを削除しますか？"
 
 _abuseReport:
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
index 42e5931212..7be5335885 100644
--- a/packages/backend/src/core/AbuseReportNotificationService.ts
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -44,7 +44,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 
 	/**
 	 * 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
-	 * 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
+	 * 通知先ユーザは{@link getModeratorIds}の取得結果に依る.
 	 *
 	 * @see RoleService.getModeratorIds
 	 * @see GlobalEventService.publishAdminStream
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index 5522ecd6cc..de45898328 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -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 {
@@ -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,
@@ -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 };
 	}
diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts
index 72fa4d928d..9b1961c631 100644
--- a/packages/backend/src/core/UserService.ts
+++ b/packages/backend/src/core/UserService.ts
@@ -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,
 	) {
 	}
 
@@ -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,
+			);
+		}
+	}
 }
diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts
index 86fb323d1d..d6c27eae51 100644
--- a/packages/backend/src/models/SystemWebhook.ts
+++ b/packages/backend/src/models/SystemWebhook.ts
@@ -12,6 +12,8 @@ export const systemWebhookEventTypes = [
 	'abuseReport',
 	// 通報を処理したとき
 	'abuseReportResolved',
+	// ユーザが作成された時
+	'userCreated',
 ] as const;
 export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
 
diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts
index b0cc3d13ec..6ce6e47781 100644
--- a/packages/backend/test/e2e/synalio/abuse-report.ts
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -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',
diff --git a/packages/backend/test/e2e/synalio/user-create.ts b/packages/backend/test/e2e/synalio/user-create.ts
new file mode 100644
index 0000000000..cb0f68dfea
--- /dev/null
+++ b/packages/backend/test/e2e/synalio/user-create.ts
@@ -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();
+		});
+	});
+});
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index e70befeebe..26de19eaf1 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -12,13 +12,14 @@ import WebSocket, { ClientOptions } from 'ws';
 import fetch, { File, RequestInit, type Headers } from 'node-fetch';
 import { DataSource } from 'typeorm';
 import { JSDOM } from 'jsdom';
-import { DEFAULT_POLICIES } from '@/core/RoleService.js';
-import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
+import { type Response } from 'node-fetch';
+import Fastify from 'fastify';
 import { entities } from '../src/postgres.js';
 import { loadConfig } from '../src/config.js';
 import type * as misskey from 'misskey-js';
-import { type Response } from 'node-fetch';
-import { ApiError } from "@/server/api/error.js";
+import { DEFAULT_POLICIES } from '@/core/RoleService.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';
 
@@ -27,11 +28,23 @@ export interface UserToken {
 	bearer?: boolean;
 }
 
+export type SystemWebhookPayload = {
+	server: string;
+	hookId: string;
+	eventId: string;
+	createdAt: string;
+	type: string;
+	body: any;
+}
+
 const config = loadConfig();
 export const port = config.port;
 export const origin = config.url;
 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 => {
 	return `token=${me.token};`;
 };
@@ -645,3 +658,37 @@ export async function sendEnvResetRequest() {
 export function castAsError(obj: Record<string, unknown>): { 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;
+}
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index 3e6a015018..0beb0a8163 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -40,6 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
 						<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
 					</MkSwitch>
+					<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
+						<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
+					</MkSwitch>
 				</div>
 			</MkFolder>
 
@@ -78,6 +81,7 @@ import * as os from '@/os.js';
 type EventType = {
 	abuseReport: boolean;
 	abuseReportResolved: boolean;
+	userCreated: boolean;
 }
 
 const emit = defineEmits<{
@@ -100,12 +104,14 @@ const secret = ref<string>('');
 const events = ref<EventType>({
 	abuseReport: true,
 	abuseReportResolved: true,
+	userCreated: true,
 });
 const isActive = ref<boolean>(true);
 
 const disabledEvents = ref<EventType>({
 	abuseReport: false,
 	abuseReportResolved: false,
+	userCreated: false,
 });
 
 const disableSubmitButton = computed(() => {
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index b2b8938baa..bf35d0f421 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4970,7 +4970,7 @@ export type components = {
       latestSentAt: string | null;
       latestStatus: number | null;
       name: string;
-      on: ('abuseReport' | 'abuseReportResolved')[];
+      on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
       url: string;
       secret: string;
     };
@@ -10042,7 +10042,7 @@ export type operations = {
         'application/json': {
           isActive: boolean;
           name: string;
-          on: ('abuseReport' | 'abuseReportResolved')[];
+          on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
           url: string;
           secret: string;
         };
@@ -10152,7 +10152,7 @@ export type operations = {
       content: {
         'application/json': {
           isActive?: boolean;
-          on?: ('abuseReport' | 'abuseReportResolved')[];
+          on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
         };
       };
     };
@@ -10265,7 +10265,7 @@ export type operations = {
           id: string;
           isActive: boolean;
           name: string;
-          on: ('abuseReport' | 'abuseReportResolved')[];
+          on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
           url: string;
           secret: string;
         };
-- 
cgit v1.2.3-freya


From 1a79f0dc2adfef82da63136ed4b7ae0182006d29 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 30 Jul 2024 09:47:31 +0900
Subject: :art:

---
 .../src/components/MkSystemWebhookEditor.vue       | 91 ++++++++++++----------
 1 file changed, 48 insertions(+), 43 deletions(-)

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index 0beb0a8163..f6ce2ccc9f 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -18,47 +18,49 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header>
 		{{ mode === 'create' ? i18n.ts._webhookSettings.createWebhook : i18n.ts._webhookSettings.modifyWebhook }}
 	</template>
-	<MkSpacer :marginMin="20" :marginMax="28">
-		<MkLoading v-if="loading !== 0"/>
-		<div v-else :class="$style.root" class="_gaps_m">
-			<MkInput v-model="title">
-				<template #label>{{ i18n.ts._webhookSettings.name }}</template>
-			</MkInput>
-			<MkInput v-model="url">
-				<template #label>URL</template>
-			</MkInput>
-			<MkInput v-model="secret">
-				<template #label>{{ i18n.ts._webhookSettings.secret }}</template>
-			</MkInput>
-			<MkFolder :defaultOpen="true">
-				<template #label>{{ i18n.ts._webhookSettings.events }}</template>
-
-				<div class="_gaps_s">
-					<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
-						<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
-					</MkSwitch>
-					<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
-						<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
-					</MkSwitch>
-					<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
-						<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
-					</MkSwitch>
-				</div>
-			</MkFolder>
-
-			<MkSwitch v-model="isActive">
-				<template #label>{{ i18n.ts.enable }}</template>
-			</MkSwitch>
-
-			<div :class="$style.footer" class="_buttonsCenter">
-				<MkButton primary :disabled="disableSubmitButton" @click="onSubmitClicked">
-					<i class="ti ti-check"></i>
-					{{ i18n.ts.ok }}
-				</MkButton>
-				<MkButton @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
+
+	<div>
+		<MkSpacer :marginMin="20" :marginMax="28">
+			<MkLoading v-if="loading !== 0"/>
+			<div v-else :class="$style.root" class="_gaps_m">
+				<MkInput v-model="title">
+					<template #label>{{ i18n.ts._webhookSettings.name }}</template>
+				</MkInput>
+				<MkInput v-model="url">
+					<template #label>URL</template>
+				</MkInput>
+				<MkInput v-model="secret">
+					<template #label>{{ i18n.ts._webhookSettings.secret }}</template>
+				</MkInput>
+				<MkFolder :defaultOpen="true">
+					<template #label>{{ i18n.ts._webhookSettings.events }}</template>
+
+					<div class="_gaps_s">
+						<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
+							<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
+						</MkSwitch>
+						<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
+							<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
+						</MkSwitch>
+						<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
+							<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
+						</MkSwitch>
+					</div>
+				</MkFolder>
+
+				<MkSwitch v-model="isActive">
+					<template #label>{{ i18n.ts.enable }}</template>
+				</MkSwitch>
 			</div>
+		</MkSpacer>
+		<div :class="$style.footer" class="_buttonsCenter">
+			<MkButton primary rounded :disabled="disableSubmitButton" @click="onSubmitClicked">
+				<i class="ti ti-check"></i>
+				{{ i18n.ts.ok }}
+			</MkButton>
+			<MkButton rounded @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
 		</div>
-	</MkSpacer>
+	</div>
 </MkModalWindow>
 </template>
 
@@ -223,9 +225,12 @@ onMounted(async () => {
 }
 
 .footer {
-	display: flex;
-	justify-content: center;
-	align-items: flex-end;
-	margin-top: 20px;
+	position: sticky;
+	bottom: 0;
+	left: 0;
+	padding: 12px;
+	border-top: solid 0.5px var(--divider);
+	-webkit-backdrop-filter: var(--blur, blur(15px));
+	backdrop-filter: var(--blur, blur(15px));
 }
 </style>
-- 
cgit v1.2.3-freya


From f0ec68c3cf0381acb518f5518b4f30c123cd1815 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 30 Jul 2024 16:56:07 +0900
Subject: :art:

---
 packages/frontend/src/components/MkSystemWebhookEditor.vue | 1 +
 packages/frontend/src/pages/emoji-edit-dialog.vue          | 1 +
 2 files changed, 2 insertions(+)

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index f6ce2ccc9f..a141b4ff74 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -230,6 +230,7 @@ onMounted(async () => {
 	left: 0;
 	padding: 12px;
 	border-top: solid 0.5px var(--divider);
+	background: var(--acrylicBg);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 16769ef360..d0a6bba47b 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -243,6 +243,7 @@ async function del() {
 	left: 0;
 	padding: 12px;
 	border-top: solid 0.5px var(--divider);
+	background: var(--acrylicBg);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
-- 
cgit v1.2.3-freya


From 63f9c271ca9958abb00d879be01a045d25d261d1 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 30 Jul 2024 20:58:25 +0900
Subject: :art:

---
 .../src/components/MkSystemWebhookEditor.vue       |  5 ++-
 .../abuse-report/notification-recipient.editor.vue | 43 ++++++++++++----------
 packages/frontend/src/pages/emoji-edit-dialog.vue  |  5 ++-
 3 files changed, 30 insertions(+), 23 deletions(-)

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index a141b4ff74..cad2e99e4d 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -19,8 +19,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		{{ mode === 'create' ? i18n.ts._webhookSettings.createWebhook : i18n.ts._webhookSettings.modifyWebhook }}
 	</template>
 
-	<div>
-		<MkSpacer :marginMin="20" :marginMax="28">
+	<div style="display: flex; flex-direction: column; min-height: 100%;">
+		<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
 			<MkLoading v-if="loading !== 0"/>
 			<div v-else :class="$style.root" class="_gaps_m">
 				<MkInput v-model="title">
@@ -226,6 +226,7 @@ onMounted(async () => {
 
 .footer {
 	position: sticky;
+	z-index: 10000;
 	bottom: 0;
 	left: 0;
 	padding: 12px;
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
index 015109f54e..827e22e8ae 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -16,8 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header>
 		{{ mode === 'create' ? i18n.ts._abuseReport._notificationRecipient.createRecipient : i18n.ts._abuseReport._notificationRecipient.modifyRecipient }}
 	</template>
-	<div v-if="loading === 0">
-		<MkSpacer :marginMin="20" :marginMax="28">
+	<div v-if="loading === 0" style="display: flex; flex-direction: column; min-height: 100%;">
+		<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
 			<div :class="$style.root" class="_gaps_m">
 				<MkInput v-model="title">
 					<template #label>{{ i18n.ts.title }}</template>
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 								{{ webhook.name }}
 							</option>
 						</MkSelect>
-						<MkButton rounded @click="onEditSystemWebhookClicked">
+						<MkButton rounded :class="$style.systemWebhookEditButton" @click="onEditSystemWebhookClicked">
 							<span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"/>
 							<span v-else class="ti ti-settings" style="line-height: normal"/>
 						</MkButton>
@@ -60,8 +60,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkSpacer>
 
 		<div :class="$style.footer" class="_buttonsCenter">
-			<MkButton primary :disabled="disableSubmitButton" @click="onSubmitClicked"><i class="ti ti-check"></i> {{ i18n.ts.ok }}</MkButton>
-			<MkButton @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
+			<MkButton primary rounded :disabled="disableSubmitButton" @click="onSubmitClicked"><i class="ti ti-check"></i> {{ i18n.ts.ok }}</MkButton>
+			<MkButton rounded @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
 		</div>
 	</div>
 	<div v-else>
@@ -289,10 +289,15 @@ onMounted(async () => {
 }
 
 .footer {
-	display: flex;
-	justify-content: center;
-	align-items: flex-end;
-	margin-top: 20px;
+	position: sticky;
+	z-index: 10000;
+	bottom: 0;
+	left: 0;
+	padding: 12px;
+	border-top: solid 0.5px var(--divider);
+	background: var(--acrylicBg);
+	-webkit-backdrop-filter: var(--blur, blur(15px));
+	backdrop-filter: var(--blur, blur(15px));
 }
 
 .systemWebhook {
@@ -301,16 +306,16 @@ onMounted(async () => {
 	justify-content: stretch;
 	align-items: flex-end;
 	gap: 8px;
+}
 
-	button {
-		min-width: 0;
-		min-height: 0;
-		width: 34px;
-		height: 34px;
-		flex-shrink: 0;
-		box-sizing: border-box;
-		margin: 1px 0;
-		padding: 6px;
-	}
+.systemWebhookEditButton {
+	min-width: 0;
+	min-height: 0;
+	width: 34px;
+	height: 34px;
+	flex-shrink: 0;
+	box-sizing: border-box;
+	margin: 1px 0;
+	padding: 6px;
 }
 </style>
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index d0a6bba47b..853c1d6b0b 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template v-if="emoji" #header>:{{ emoji.name }}:</template>
 	<template v-else #header>New emoji</template>
 
-	<div>
-		<MkSpacer :marginMin="20" :marginMax="28">
+	<div style="display: flex; flex-direction: column; min-height: 100%;">
+		<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
 			<div class="_gaps_m">
 				<div v-if="imgUrl != null" :class="$style.imgs">
 					<div style="background: #000;" :class="$style.imgContainer">
@@ -239,6 +239,7 @@ async function del() {
 
 .footer {
 	position: sticky;
+	z-index: 10000;
 	bottom: 0;
 	left: 0;
 	padding: 12px;
-- 
cgit v1.2.3-freya


From d63b854f96d9437f9764f9170c3ed3537cc98a2c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 31 Jul 2024 08:12:35 +0900
Subject: tweak localization

---
 locales/ja-JP.yml                                          | 2 +-
 packages/frontend/src/components/MkSystemWebhookEditor.vue | 2 +-
 packages/frontend/src/pages/settings/webhook.edit.vue      | 2 +-
 packages/frontend/src/pages/settings/webhook.new.vue       | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 522ad7e22a..b493183974 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2492,7 +2492,7 @@ _webhookSettings:
   modifyWebhook: "Webhookを編集"
   name: "名前"
   secret: "シークレット"
-  events: "Webhookを実行するタイミング"
+  trigger: "トリガー"
   active: "有効"
   _events:
     follow: "フォローしたとき"
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index cad2e99e4d..f5c7a3160b 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<template #label>{{ i18n.ts._webhookSettings.secret }}</template>
 				</MkInput>
 				<MkFolder :defaultOpen="true">
-					<template #label>{{ i18n.ts._webhookSettings.events }}</template>
+					<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
 
 					<div class="_gaps_s">
 						<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index e9fb1e471e..058ef69c35 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkInput>
 
 	<FormSection>
-		<template #label>{{ i18n.ts._webhookSettings.events }}</template>
+		<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
 
 		<div class="_gaps_s">
 			<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue
index 5bf85e48f4..d62357caaf 100644
--- a/packages/frontend/src/pages/settings/webhook.new.vue
+++ b/packages/frontend/src/pages/settings/webhook.new.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkInput>
 
 	<FormSection>
-		<template #label>{{ i18n.ts._webhookSettings.events }}</template>
+		<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
 
 		<div class="_gaps_s">
 			<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
-- 
cgit v1.2.3-freya

