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>
|
@@ -0,0 +1,307 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="490"
|
||||
:withOkButton="false"
|
||||
:okButtonDisabled="false"
|
||||
@close="onCancelClicked"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<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 :class="$style.root" class="_gaps_m">
|
||||
<MkInput v-model="title">
|
||||
<template #label>{{ i18n.ts.title }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="method">
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template>
|
||||
<option value="email">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option>
|
||||
<option value="webhook">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option>
|
||||
<template #caption>
|
||||
{{ methodCaption }}
|
||||
</template>
|
||||
</MkSelect>
|
||||
<div>
|
||||
<MkSelect v-if="method === 'email'" v-model="userId">
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedUser }}</template>
|
||||
<option v-for="user in moderators" :key="user.id" :value="user.id">
|
||||
{{ user.name ? `${user.name}(${user.username})` : user.username }}
|
||||
</option>
|
||||
</MkSelect>
|
||||
<div v-else-if="method === 'webhook'" :class="$style.systemWebhook">
|
||||
<MkSelect v-model="systemWebhookId" style="flex: 1">
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template>
|
||||
<option v-for="webhook in systemWebhooks" :key="webhook.id ?? undefined" :value="webhook.id">
|
||||
{{ webhook.name }}
|
||||
</option>
|
||||
</MkSelect>
|
||||
<MkButton rounded @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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkDivider/>
|
||||
|
||||
<MkSwitch v-model="isActive">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, toRefs } from 'vue';
|
||||
import { entities } from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { MkSystemWebhookResult, showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkDivider from '@/components/MkDivider.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
type NotificationRecipientMethod = 'email' | 'webhook';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'submitted'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'create' | 'edit';
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
const { mode, id } = toRefs(props);
|
||||
|
||||
const loading = ref<number>(0);
|
||||
|
||||
const title = ref<string>('');
|
||||
const method = ref<NotificationRecipientMethod>('email');
|
||||
const userId = ref<string | null>(null);
|
||||
const systemWebhookId = ref<string | null>(null);
|
||||
const isActive = ref<boolean>(true);
|
||||
|
||||
const moderators = ref<entities.User[]>([]);
|
||||
const systemWebhooks = ref<(entities.SystemWebhook | { id: null, name: string })[]>([]);
|
||||
|
||||
const methodCaption = computed(() => {
|
||||
switch (method.value) {
|
||||
case 'email': {
|
||||
return i18n.ts._abuseReport._notificationRecipient._recipientType._captions.mail;
|
||||
}
|
||||
case 'webhook': {
|
||||
return i18n.ts._abuseReport._notificationRecipient._recipientType._captions.webhook;
|
||||
}
|
||||
default: {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const disableSubmitButton = computed(() => {
|
||||
if (!title.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (method.value) {
|
||||
case 'email': {
|
||||
return userId.value === null;
|
||||
}
|
||||
case 'webhook': {
|
||||
return systemWebhookId.value === null;
|
||||
}
|
||||
default: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function onSubmitClicked() {
|
||||
await loadingScope(async () => {
|
||||
const _userId = (method.value === 'email') ? userId.value : null;
|
||||
const _systemWebhookId = (method.value === 'webhook') ? systemWebhookId.value : null;
|
||||
const params = {
|
||||
isActive: isActive.value,
|
||||
name: title.value,
|
||||
method: method.value,
|
||||
userId: _userId ?? undefined,
|
||||
systemWebhookId: _systemWebhookId ?? undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
switch (mode.value) {
|
||||
case 'create': {
|
||||
await misskeyApi('admin/abuse-report/notification-recipient/create', params);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await misskeyApi('admin/abuse-report/notification-recipient/update', { id: id.value!, ...params });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit('submitted');
|
||||
// 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 onEditSystemWebhookClicked() {
|
||||
let result: MkSystemWebhookResult | null;
|
||||
if (systemWebhookId.value === null) {
|
||||
result = await showSystemWebhookEditorDialog({
|
||||
mode: 'create',
|
||||
});
|
||||
} else {
|
||||
result = await showSystemWebhookEditorDialog({
|
||||
mode: 'edit',
|
||||
id: systemWebhookId.value,
|
||||
});
|
||||
}
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchSystemWebhooks();
|
||||
systemWebhookId.value = result.id ?? null;
|
||||
}
|
||||
|
||||
async function fetchSystemWebhooks() {
|
||||
await loadingScope(async () => {
|
||||
systemWebhooks.value = [
|
||||
{ id: null, name: i18n.ts.createNew },
|
||||
...await misskeyApi('admin/system-webhook/list', { }),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchModerators() {
|
||||
await loadingScope(async () => {
|
||||
const users = Array.of<entities.User>();
|
||||
for (; ;) {
|
||||
const res = await misskeyApi('admin/show-users', {
|
||||
limit: 100,
|
||||
state: 'adminOrModerator',
|
||||
origin: 'local',
|
||||
offset: users.length,
|
||||
});
|
||||
|
||||
if (res.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
users.push(...res);
|
||||
}
|
||||
|
||||
moderators.value = users;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
|
||||
loading.value++;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
loading.value--;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadingScope(async () => {
|
||||
await fetchModerators();
|
||||
await fetchSystemWebhooks();
|
||||
|
||||
if (mode.value === 'edit') {
|
||||
if (!id.value) {
|
||||
throw new Error('id is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await misskeyApi('admin/abuse-report/notification-recipient/show', { id: id.value });
|
||||
|
||||
title.value = res.name;
|
||||
method.value = res.method;
|
||||
userId.value = res.userId ?? null;
|
||||
systemWebhookId.value = res.systemWebhookId ?? null;
|
||||
isActive.value = res.isActive;
|
||||
// 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');
|
||||
}
|
||||
} else {
|
||||
userId.value = moderators.value[0]?.id ?? null;
|
||||
systemWebhookId.value = systemWebhooks.value[0]?.id ?? null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.systemWebhook {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
min-width: 2.5em;
|
||||
min-height: 2.5em;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,114 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root" class="_panel _gaps_s">
|
||||
<div :class="$style.rightDivider" style="width: 80px;"><span :class="`ti ${methodIcon}`"/> {{ methodName }}</div>
|
||||
<div :class="$style.rightDivider" style="flex: 0.5">{{ entity.name }}</div>
|
||||
<div :class="$style.rightDivider" style="flex: 1">
|
||||
<div v-if="method === 'email' && user">
|
||||
{{
|
||||
`${i18n.ts._abuseReport._notificationRecipient.notifiedUser}: ` + ((user.name) ? `${user.name}(${user.username})` : user.username)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="method === 'webhook' && systemWebhook">
|
||||
{{ `${i18n.ts._abuseReport._notificationRecipient.notifiedWebhook}: ` + systemWebhook.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.recipientButtons" style="margin-left: auto">
|
||||
<button :class="$style.recipientButton" @click="onEditButtonClicked()">
|
||||
<span class="ti ti-settings"/>
|
||||
</button>
|
||||
<button :class="$style.recipientButton" @click="onDeleteButtonClicked()">
|
||||
<span class="ti ti-trash"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { entities } from 'misskey-js';
|
||||
import { computed, toRefs } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'edit', id: entities.AbuseReportNotificationRecipient['id']): void;
|
||||
(ev: 'delete', id: entities.AbuseReportNotificationRecipient['id']): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
entity: entities.AbuseReportNotificationRecipient;
|
||||
}>();
|
||||
|
||||
const { entity } = toRefs(props);
|
||||
|
||||
const method = computed(() => entity.value.method);
|
||||
const user = computed(() => entity.value.user);
|
||||
const systemWebhook = computed(() => entity.value.systemWebhook);
|
||||
const methodIcon = computed(() => {
|
||||
switch (entity.value.method) {
|
||||
case 'email':
|
||||
return 'ti-mail';
|
||||
case 'webhook':
|
||||
return 'ti-webhook';
|
||||
default:
|
||||
return 'ti-help';
|
||||
}
|
||||
});
|
||||
const methodName = computed(() => {
|
||||
switch (entity.value.method) {
|
||||
case 'email':
|
||||
return i18n.ts._abuseReport._notificationRecipient._recipientType.mail;
|
||||
case 'webhook':
|
||||
return i18n.ts._abuseReport._notificationRecipient._recipientType.webhook;
|
||||
default:
|
||||
return '不明';
|
||||
}
|
||||
});
|
||||
|
||||
function onEditButtonClicked() {
|
||||
emit('edit', entity.value.id);
|
||||
}
|
||||
|
||||
function onDeleteButtonClicked() {
|
||||
emit('delete', entity.value.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.rightDivider {
|
||||
border-right: 0.5px solid var(--divider);
|
||||
}
|
||||
|
||||
.recipientButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: -4;
|
||||
}
|
||||
|
||||
.recipientButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
box-sizing: border-box;
|
||||
margin-top: -2px;
|
||||
margin-bottom: -2px;
|
||||
padding: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--buttonBg);
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,176 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<XHeader :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
|
||||
<MkSpacer :contentMax="900">
|
||||
<div :class="$style.root" class="_gaps_m">
|
||||
<div :class="$style.addButton">
|
||||
<MkButton primary @click="onAddButtonClicked">
|
||||
<span class="ti ti-plus"/> {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div :class="$style.subMenus" class="_gaps_s">
|
||||
<MkSelect v-model="filterMethod" style="flex: 1">
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template>
|
||||
<option :value="null">-</option>
|
||||
<option :value="'email'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option>
|
||||
<option :value="'webhook'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option>
|
||||
</MkSelect>
|
||||
<MkInput v-model="filterText" type="search" style="flex: 1">
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.keywords }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkDivider/>
|
||||
|
||||
<div :class="$style.recipients" class="_gaps_s">
|
||||
<XRecipient
|
||||
v-for="r in filteredRecipients"
|
||||
:key="r.id"
|
||||
:entity="r"
|
||||
@edit="onEditButtonClicked"
|
||||
@delete="onDeleteButtonClicked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { entities } from 'misskey-js';
|
||||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import XRecipient from './notification-recipient.item.vue';
|
||||
import XHeader from '@/pages/admin/_header_.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import MkDivider from '@/components/MkDivider.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const recipients = ref<entities.AbuseReportNotificationRecipient[]>([]);
|
||||
|
||||
const filterMethod = ref<string | null>(null);
|
||||
const filterText = ref<string>('');
|
||||
|
||||
const filteredRecipients = computed(() => {
|
||||
const method = filterMethod.value;
|
||||
const text = filterText.value.trim().length === 0 ? null : filterText.value;
|
||||
|
||||
return recipients.value.filter(it => {
|
||||
if (method ?? text) {
|
||||
if (text) {
|
||||
const keywords = [it.name, it.systemWebhook?.name, it.user?.name, it.user?.username];
|
||||
if (keywords.filter(k => k?.includes(text)).length !== 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (method) {
|
||||
return it.method.includes(method);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
const headerActions = computed(() => []);
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
async function onAddButtonClicked() {
|
||||
await showEditor('create');
|
||||
}
|
||||
|
||||
async function onEditButtonClicked(id: string) {
|
||||
await showEditor('edit', id);
|
||||
}
|
||||
|
||||
async function onDeleteButtonClicked(id: string) {
|
||||
const res = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts._abuseReport._notificationRecipient.deleteConfirm,
|
||||
});
|
||||
if (!res.canceled) {
|
||||
await misskeyApi('admin/abuse-report/notification-recipient/delete', { id: id });
|
||||
await fetchRecipients();
|
||||
}
|
||||
}
|
||||
|
||||
async function showEditor(mode: 'create' | 'edit', id?: string) {
|
||||
const { dispose, needLoad } = await new Promise<{ dispose: () => void, needLoad: boolean }>(async resolve => {
|
||||
const res = await os.popup(
|
||||
defineAsyncComponent(() => import('./notification-recipient.editor.vue')),
|
||||
{
|
||||
mode,
|
||||
id,
|
||||
},
|
||||
{
|
||||
submitted: async () => {
|
||||
resolve({ dispose: res.dispose, needLoad: true });
|
||||
},
|
||||
closed: () => {
|
||||
resolve({ dispose: res.dispose, needLoad: false });
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
dispose();
|
||||
|
||||
if (needLoad) {
|
||||
await fetchRecipients();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRecipients() {
|
||||
const result = await misskeyApi('admin/abuse-report/notification-recipient/list', {
|
||||
method: ['email', 'webhook'],
|
||||
});
|
||||
|
||||
recipients.value = result.sort((a, b) => (a.method + a.id).localeCompare(b.method + b.id));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchRecipients();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.subMenus {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.recipients {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
</style>
|
@@ -7,30 +7,33 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div>
|
||||
<div class="reports">
|
||||
<div class="">
|
||||
<div class="inputs" style="display: flex;">
|
||||
<MkSelect v-model="state" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="unresolved">{{ i18n.ts.unresolved }}</option>
|
||||
<option value="resolved">{{ i18n.ts.resolved }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ i18n.ts.reporteeOrigin }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ i18n.ts.reporterOrigin }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<!-- TODO
|
||||
<div :class="$style.root" class="_gaps">
|
||||
<div :class="$style.subMenus" class="_gaps">
|
||||
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ "通知設定" }}</MkButton>
|
||||
</div>
|
||||
|
||||
<div :class="$style.inputs" class="_gaps">
|
||||
<MkSelect v-model="state" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="unresolved">{{ i18n.ts.unresolved }}</option>
|
||||
<option value="resolved">{{ i18n.ts.resolved }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ i18n.ts.reporteeOrigin }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ i18n.ts.reporterOrigin }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
|
||||
<!-- TODO
|
||||
<div class="inputs" style="display: flex; padding-top: 1.2em;">
|
||||
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" :spellcheck="false">
|
||||
<span>{{ i18n.ts.username }}</span>
|
||||
@@ -41,11 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
-->
|
||||
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
@@ -60,6 +61,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
||||
import XAbuseReport from '@/components/MkAbuseReport.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const reports = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
@@ -80,7 +82,7 @@ const pagination = {
|
||||
};
|
||||
|
||||
function resolved(reportId) {
|
||||
reports.value.removeItem(reportId);
|
||||
reports.value?.removeItem(reportId);
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
@@ -92,3 +94,26 @@ definePageMetadata(() => ({
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.subMenus {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
@@ -214,6 +214,11 @@ const menuDef = computed(() => [{
|
||||
text: i18n.ts.externalServices,
|
||||
to: '/admin/external-services',
|
||||
active: currentPage.value?.route.name === 'external-services',
|
||||
}, {
|
||||
icon: 'ti ti-webhook',
|
||||
text: 'Webhook',
|
||||
to: '/admin/system-webhook',
|
||||
active: currentPage.value?.route.name === 'system-webhook',
|
||||
}, {
|
||||
icon: 'ti ti-adjustments',
|
||||
text: i18n.ts.other,
|
||||
|
@@ -8,9 +8,35 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>
|
||||
<b
|
||||
:class="{
|
||||
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type),
|
||||
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
|
||||
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type)
|
||||
[$style.logGreen]: [
|
||||
'createRole',
|
||||
'addCustomEmoji',
|
||||
'createGlobalAnnouncement',
|
||||
'createUserAnnouncement',
|
||||
'createAd',
|
||||
'createInvitation',
|
||||
'createAvatarDecoration',
|
||||
'createSystemWebhook',
|
||||
'createAbuseReportNotificationRecipient',
|
||||
].includes(log.type),
|
||||
[$style.logYellow]: [
|
||||
'markSensitiveDriveFile',
|
||||
'resetPassword'
|
||||
].includes(log.type),
|
||||
[$style.logRed]: [
|
||||
'suspend',
|
||||
'deleteRole',
|
||||
'suspendRemoteInstance',
|
||||
'deleteGlobalAnnouncement',
|
||||
'deleteUserAnnouncement',
|
||||
'deleteCustomEmoji',
|
||||
'deleteNote',
|
||||
'deleteDriveFile',
|
||||
'deleteAd',
|
||||
'deleteAvatarDecoration',
|
||||
'deleteSystemWebhook',
|
||||
'deleteAbuseReportNotificationRecipient',
|
||||
].includes(log.type)
|
||||
}"
|
||||
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
||||
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
@@ -40,6 +66,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-else-if="log.type === 'createAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
|
||||
<span v-else-if="log.type === 'updateAvatarDecoration'">: {{ log.info.before.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
|
||||
<span v-else-if="log.type === 'createSystemWebhook'">: {{ log.info.webhook.name }}</span>
|
||||
<span v-else-if="log.type === 'updateSystemWebhook'">: {{ log.info.before.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteSystemWebhook'">: {{ log.info.webhook.name }}</span>
|
||||
<span v-else-if="log.type === 'createAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
|
||||
<span v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">: {{ log.info.before.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
|
||||
</template>
|
||||
<template #icon>
|
||||
<MkAvatar :user="log.user" :class="$style.avatar"/>
|
||||
@@ -116,6 +148,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateSystemWebhook'">
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<details>
|
||||
<summary>raw</summary>
|
||||
|
117
packages/frontend/src/pages/admin/system-webhook.item.vue
Normal file
117
packages/frontend/src/pages/admin/system-webhook.item.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.main">
|
||||
<span :class="$style.icon">
|
||||
<i v-if="!entity.isActive" class="ti ti-player-pause"/>
|
||||
<i v-else-if="entity.latestStatus === null" class="ti ti-circle"/>
|
||||
<i
|
||||
v-else-if="[200, 201, 204].includes(entity.latestStatus)"
|
||||
class="ti ti-check"
|
||||
:style="{ color: 'var(--success)' }"
|
||||
/>
|
||||
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"/>
|
||||
</span>
|
||||
<span :class="$style.text">{{ entity.name || entity.url }}</span>
|
||||
<span :class="$style.suffix">
|
||||
<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
|
||||
<button :class="$style.suffixButton" @click="onEditClick">
|
||||
<i class="ti ti-settings"></i>
|
||||
</button>
|
||||
<button :class="$style.suffixButton" @click="onDeleteClick">
|
||||
<i class="ti ti-trash"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { entities } from 'misskey-js';
|
||||
import { toRefs } from 'vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'edit', value: entities.SystemWebhook): void;
|
||||
(ev: 'delete', value: entities.SystemWebhook): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
entity: entities.SystemWebhook;
|
||||
}>();
|
||||
|
||||
const { entity } = toRefs(props);
|
||||
|
||||
function onEditClick() {
|
||||
emit('edit', entity.value);
|
||||
}
|
||||
|
||||
function onDeleteClick() {
|
||||
emit('delete', entity.value);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 14px;
|
||||
background: var(--buttonBg);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: var(--buttonHoverBg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
background: var(--buttonHoverBg);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 0.75em;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
|
||||
.text {
|
||||
flex-shrink: 1;
|
||||
white-space: normal;
|
||||
padding-right: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.suffix {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gaps: 4px;
|
||||
margin-left: auto;
|
||||
margin-right: -8px;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.suffixButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
margin-top: -8px;
|
||||
margin-bottom: -8px;
|
||||
padding: 8px;
|
||||
|
||||
&:hover {
|
||||
background: var(--buttonBg);
|
||||
}
|
||||
}
|
||||
</style>
|
96
packages/frontend/src/pages/admin/system-webhook.vue
Normal file
96
packages/frontend/src/pages/admin/system-webhook.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<XHeader :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps_m">
|
||||
<MkButton :class="$style.linkButton" full @click="onCreateWebhookClicked">
|
||||
{{ i18n.ts._webhookSettings.createWebhook }}
|
||||
</MkButton>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps">
|
||||
<XItem v-for="item in webhooks" :key="item.id" :entity="item" @edit="onEditButtonClicked" @delete="onDeleteButtonClicked"/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { entities } from 'misskey-js';
|
||||
import XItem from './system-webhook.item.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import XHeader from '@/pages/admin/_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const webhooks = ref<entities.SystemWebhook[]>([]);
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
async function onCreateWebhookClicked() {
|
||||
await showSystemWebhookEditorDialog({
|
||||
mode: 'create',
|
||||
});
|
||||
|
||||
await fetchWebhooks();
|
||||
}
|
||||
|
||||
async function onEditButtonClicked(webhook: entities.SystemWebhook) {
|
||||
await showSystemWebhookEditorDialog({
|
||||
mode: 'edit',
|
||||
id: webhook.id,
|
||||
});
|
||||
|
||||
await fetchWebhooks();
|
||||
}
|
||||
|
||||
async function onDeleteButtonClicked(webhook: entities.SystemWebhook) {
|
||||
const result = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts._webhookSettings.deleteConfirm,
|
||||
});
|
||||
if (!result.canceled) {
|
||||
await misskeyApi('admin/system-webhook/delete', {
|
||||
id: webhook.id,
|
||||
});
|
||||
await fetchWebhooks();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWebhooks() {
|
||||
const result = await misskeyApi('admin/system-webhook/list', {});
|
||||
webhooks.value = result.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchWebhooks();
|
||||
});
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: 'SystemWebhook',
|
||||
icon: 'ti ti-webhook',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.linkButton {
|
||||
text-align: left;
|
||||
padding: 10px 18px;
|
||||
}
|
||||
</style>
|
@@ -471,6 +471,14 @@ const routes: RouteDef[] = [{
|
||||
path: '/invites',
|
||||
name: 'invites',
|
||||
component: page(() => import('@/pages/admin/invites.vue')),
|
||||
}, {
|
||||
path: '/abuse-report-notification-recipient',
|
||||
name: 'abuse-report-notification-recipient',
|
||||
component: page(() => import('@/pages/admin/abuse-report/notification-recipient.vue')),
|
||||
}, {
|
||||
path: '/system-webhook',
|
||||
name: 'system-webhook',
|
||||
component: page(() => import('@/pages/admin/system-webhook.vue')),
|
||||
}, {
|
||||
path: '/',
|
||||
component: page(() => import('@/pages/_empty_.vue')),
|
||||
|
Reference in New Issue
Block a user