feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする (#13758)
* feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする * モデログに対応&エンドポイントを単一オブジェクトでのサポートに変更(API経由で大量に作るシチュエーションもないと思うので) * fix spdx * fix migration * fix migration * fix models * add e2e webhook * tweak * fix modlog * fix bugs * add tests and fix bugs * add tests and fix bugs * add tests * fix path * regenerate locale * 混入除去 * 混入除去 * add abuseReportResolved * fix pnpm-lock.yaml * add abuseReportResolved test * fix bugs * fix ui * add tests * fix CHANGELOG.md * add tests * add RoleService.getModeratorIds tests * WebhookServiceをUserとSystemに分割 * fix CHANGELOG.md * fix test * insertOneを使う用に * fix * regenerate locales * revert version * separate webhook job queue * fix * 🎨 * Update QueueProcessorService.ts --------- Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:type="type"
|
||||
:name="name"
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
@click="emit('click', $event)"
|
||||
@mousedown="onMousedown"
|
||||
>
|
||||
@@ -55,6 +56,7 @@ const props = defineProps<{
|
||||
asLike?: boolean;
|
||||
name?: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
32
packages/frontend/src/components/MkDivider.vue
Normal file
32
packages/frontend/src/components/MkDivider.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="default" :style="[
|
||||
marginTopBottom ? { marginTop: marginTopBottom, marginBottom: marginTopBottom } : {},
|
||||
marginLeftRight ? { marginLeft: marginLeftRight, marginRight: marginLeftRight } : {},
|
||||
borderStyle ? { borderStyle: borderStyle } : {},
|
||||
borderWidth ? { borderWidth: borderWidth } : {},
|
||||
borderColor ? { borderColor: borderColor } : {},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
marginTopBottom?: string;
|
||||
marginLeftRight?: string;
|
||||
borderStyle?: string;
|
||||
borderWidth?: string;
|
||||
borderColor?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.default {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
</style>
|
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@keydown.enter="toggle"
|
||||
>
|
||||
<XButton :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
||||
<span :class="$style.body">
|
||||
<span v-if="!noBody" :class="$style.body">
|
||||
<!-- TODO: 無名slotの方は廃止 -->
|
||||
<span :class="$style.label">
|
||||
<span @click="toggle">
|
||||
@@ -34,16 +34,19 @@ const props = defineProps<{
|
||||
modelValue: boolean | Ref<boolean>;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
noBody?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', v: boolean): void;
|
||||
(ev: 'change', v: boolean): void;
|
||||
}>();
|
||||
|
||||
const checked = toRefs(props).modelValue;
|
||||
const toggle = () => {
|
||||
if (props.disabled) return;
|
||||
emit('update:modelValue', !checked.value);
|
||||
emit('change', !checked.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved';
|
||||
|
||||
export type MkSystemWebhookEditorProps = {
|
||||
mode: 'create' | 'edit';
|
||||
id?: string;
|
||||
requiredEvents?: SystemWebhookEventType[];
|
||||
};
|
||||
|
||||
export type MkSystemWebhookResult = {
|
||||
id?: string;
|
||||
isActive: boolean;
|
||||
name: string;
|
||||
on: SystemWebhookEventType[];
|
||||
url: string;
|
||||
secret: string;
|
||||
};
|
||||
|
||||
export async function showSystemWebhookEditorDialog(props: MkSystemWebhookEditorProps): Promise<MkSystemWebhookResult | null> {
|
||||
const { dispose, result } = await new Promise<{ dispose: () => void, result: MkSystemWebhookResult | null }>(async resolve => {
|
||||
const res = await os.popup(
|
||||
defineAsyncComponent(() => import('@/components/MkSystemWebhookEditor.vue')),
|
||||
props,
|
||||
{
|
||||
submitted: (ev: MkSystemWebhookResult) => {
|
||||
resolve({ dispose: res.dispose, result: ev });
|
||||
},
|
||||
closed: () => {
|
||||
resolve({ dispose: res.dispose, result: null });
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
dispose();
|
||||
|
||||
return result;
|
||||
}
|
217
packages/frontend/src/components/MkSystemWebhookEditor.vue
Normal file
217
packages/frontend/src/components/MkSystemWebhookEditor.vue
Normal file
@@ -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>
|
Reference in New Issue
Block a user