feat: 通報のカテゴリー化 (MisskeyIO#288)
This commit is contained in:
parent
8a8196aa09
commit
1478a6c4ba
22
locales/index.d.ts
vendored
22
locales/index.d.ts
vendored
@ -1190,6 +1190,28 @@ export interface Locale {
|
||||
"decorate": string;
|
||||
"addMfmFunction": string;
|
||||
"enableQuickAddMfmFunction": string;
|
||||
"abuseReportCategory": string;
|
||||
"selectCategory": string;
|
||||
"reportComplete": string;
|
||||
"blockThisUser": string;
|
||||
"muteThisUser": string;
|
||||
"_abuseReportMsgs": {
|
||||
"rightsAbuseCantAccept": string;
|
||||
};
|
||||
"_abuseReportCategory": {
|
||||
"nsfw": string;
|
||||
"spam": string;
|
||||
"explicit": string;
|
||||
"phishing": string;
|
||||
"personalinfoleak": string;
|
||||
"selfharm": string;
|
||||
"criticalbreach": string;
|
||||
"otherbreach": string;
|
||||
"violationrights": string;
|
||||
"violationrightsother": string;
|
||||
"notlike": string;
|
||||
"other": string;
|
||||
};
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
|
@ -1187,6 +1187,28 @@ seasonalScreenEffect: "季節に応じた画面の演出"
|
||||
decorate: "デコる"
|
||||
addMfmFunction: "装飾を追加"
|
||||
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
||||
abuseReportCategory: "通報の種類"
|
||||
selectCategory: "カテゴリを選択"
|
||||
reportComplete: "通報完了"
|
||||
blockThisUser: "このユーザーをブロックする"
|
||||
muteThisUser: "このユーザーをミュートする"
|
||||
|
||||
_abuseReportMsgs:
|
||||
rightsAbuseCantAccept: "申し訳ございません。権利侵害の通報は権利者ご本人からのみ受け付けております。"
|
||||
|
||||
_abuseReportCategory:
|
||||
nsfw: "センシティブなコンテンツを含む投稿"
|
||||
spam: "スパム"
|
||||
explicit: "暴力もしくは攻撃的な投稿"
|
||||
phishing: "フィッシングもしくは詐欺行為"
|
||||
personalinfoleak: "本人もしくは他人の個人情報の漏えい"
|
||||
selfharm: "自殺もしくは自害など生命に関わる問題"
|
||||
criticalbreach: "重大な規約違反"
|
||||
otherbreach: "その他の規約違反"
|
||||
violationrights: "権利侵害もしくはなりすまし(本人)"
|
||||
violationrightsother: "権利侵害(他人)"
|
||||
notlike: "この人が気に入らない"
|
||||
other: "その他"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
|
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class AbuseUserReportCategory1703250468098 {
|
||||
name = 'AbuseUserReportCategory1703250468098'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "category" character varying(20) NOT NULL DEFAULT 'other'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_5b9acc09094daeb8683e362778" ON "abuse_user_report" ("category") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_5b9acc09094daeb8683e362778"`);
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "category"`);
|
||||
}
|
||||
}
|
@ -50,6 +50,7 @@ export class AbuseUserReportEntityService {
|
||||
detail: true,
|
||||
}) : null,
|
||||
forwarded: report.forwarded,
|
||||
category: report.category,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,13 @@ export class MiAbuseUserReport {
|
||||
})
|
||||
public comment: string;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 20, nullable: false,
|
||||
default: 'other',
|
||||
})
|
||||
public category: string;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
|
@ -12,6 +12,10 @@ export const packedAbuseUserReportSchema = {
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -74,6 +74,10 @@ export const meta = {
|
||||
nullable: true, optional: true,
|
||||
ref: 'User',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -89,6 +93,7 @@ export const paramDef = {
|
||||
reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
|
||||
targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
|
||||
forwarded: { type: 'boolean', default: false },
|
||||
category: { type: 'string', nullable: true, default: null },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@ -120,6 +125,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break;
|
||||
}
|
||||
|
||||
if (ps.category) {
|
||||
query.andWhere('report.category = :category', { category: ps.category });
|
||||
}
|
||||
|
||||
const reports = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.abuseUserReportEntityService.packMany(reports, me);
|
||||
|
@ -48,6 +48,7 @@ export const paramDef = {
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
comment: { type: 'string', minLength: 1, maxLength: 2048 },
|
||||
category: { type: 'string', minLength: 1, maxLength: 20, default: 'other' },
|
||||
},
|
||||
required: ['userId', 'comment'],
|
||||
} as const;
|
||||
@ -85,6 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
reporterId: me.id,
|
||||
reporterHost: null,
|
||||
comment: ps.comment,
|
||||
category: ps.category,
|
||||
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
this.queueService.createReportAbuseJob(report);
|
||||
|
@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
{{ i18n.ts.moderator }}:
|
||||
<MkAcct :user="report.assignee"/>
|
||||
</div>
|
||||
<div v-if="report.category">カテゴリ: {{ i18n.t(`_abuseReportCategory.${report.category}`) }}</div>
|
||||
<div><MkTime :time="report.createdAt"/></div>
|
||||
<div class="action">
|
||||
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
|
||||
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')">
|
||||
<MkWindow v-if="page === 1" ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')">
|
||||
<template #header>
|
||||
<i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i>
|
||||
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
|
||||
@ -15,6 +15,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m" :class="$style.root">
|
||||
<div>
|
||||
<MkSelect v-model="category" :required="true">
|
||||
<template #label>{{ i18n.ts.abuseReportCategory }}</template>
|
||||
<option value="" selected disabled>{{ i18n.ts.selectCategory }}</option>
|
||||
<option value="nsfw">{{ i18n.ts._abuseReportCategory.nsfw }}</option>
|
||||
<option value="spam">{{ i18n.ts._abuseReportCategory.spam }}</option>
|
||||
<option value="explicit">{{ i18n.ts._abuseReportCategory.explicit }}</option>
|
||||
<option value="phishing">{{ i18n.ts._abuseReportCategory.phishing }}</option>
|
||||
<option value="personalinfoleak">{{ i18n.ts._abuseReportCategory.personalinfoleak }}</option>
|
||||
<option value="selfharm">{{ i18n.ts._abuseReportCategory.selfharm }}</option>
|
||||
<option value="criticalbreach">{{ i18n.ts._abuseReportCategory.criticalbreach }}</option>
|
||||
<option value="otherbreach">{{ i18n.ts._abuseReportCategory.otherbreach }}</option>
|
||||
<option value="violationrights">{{ i18n.ts._abuseReportCategory.violationrights }}</option>
|
||||
<option value="violationrightsother">{{ i18n.ts._abuseReportCategory.violationrightsother }}</option>
|
||||
<option value="notlike">{{ i18n.ts._abuseReportCategory.notlike }}</option>
|
||||
<option value="other">{{ i18n.ts._abuseReportCategory.other }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div class="">
|
||||
<MkTextarea v-model="comment">
|
||||
<template #label>{{ i18n.ts.details }}</template>
|
||||
@ -22,7 +40,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkTextarea>
|
||||
</div>
|
||||
<div class="">
|
||||
<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
|
||||
<MkButton primary full :disabled="comment.length === 0 || category.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkWindow>
|
||||
|
||||
<MkWindow v-if="page === 2" ref="uiWindow2" :initialWidth="450" :initialHeight="250" :canResize="true" @closed="emit('closed')">
|
||||
<template #header>
|
||||
<i class="ti ti-circle-check" style="margin-right: 0.5em;"></i>
|
||||
<span><MkAcct :user="props.user"/> {{ i18n.ts.reportComplete }}</span>
|
||||
</template>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m" :class="$style.root">
|
||||
<div>
|
||||
<p style="margin-bottom: 20px;">{{ i18n.ts.abuseReported }}</p>
|
||||
<MkButton :disabled="fullUserInfo?.isBlocking" @click="blockUser">{{ i18n.ts.blockThisUser }}</MkButton>
|
||||
<br>
|
||||
<MkButton :disabled="fullUserInfo?.isMuted" @click="muteUser">{{ i18n.ts.muteThisUser }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
@ -30,11 +65,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { ref, shallowRef, Ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
@ -48,19 +84,59 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const uiWindow = shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const uiWindow2 = shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const comment = ref(props.initialComment ?? '');
|
||||
const category = ref('');
|
||||
const page = ref(1);
|
||||
const fullUserInfo: Ref<Misskey.entities.UserDetailed | null> = ref(null);
|
||||
|
||||
function blockUser() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts.block,
|
||||
text: i18n.ts.blockConfirm,
|
||||
}).then((v) => {
|
||||
if (v.canceled) return;
|
||||
os.apiWithDialog('blocking/create', { userId: props.user.id }).then(refreshUserInfo);
|
||||
});
|
||||
}
|
||||
|
||||
function muteUser() {
|
||||
os.apiWithDialog('mute/create', { userId: props.user.id }).then(refreshUserInfo);
|
||||
}
|
||||
|
||||
function refreshUserInfo() {
|
||||
os.api('users/show', { userId: props.user.id })
|
||||
.then((res) => {
|
||||
fullUserInfo.value = res;
|
||||
});
|
||||
}
|
||||
|
||||
function send() {
|
||||
os.apiWithDialog('users/report-abuse', {
|
||||
userId: props.user.id,
|
||||
comment: comment.value,
|
||||
}, undefined).then(res => {
|
||||
if (category.value === 'violationrightsother') {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.ts.abuseReported,
|
||||
type: 'info',
|
||||
text: i18n.ts._abuseReportMsgs.rightsAbuseCantAccept
|
||||
});
|
||||
uiWindow.value?.close();
|
||||
emit('closed');
|
||||
return;
|
||||
}
|
||||
if (category.value === 'notlike') {
|
||||
uiWindow.value?.close();
|
||||
page.value = 2;
|
||||
}
|
||||
os.apiWithDialog('users/report-abuse', {
|
||||
userId: props.user.id,
|
||||
comment: comment.value,
|
||||
category: category.value,
|
||||
}, undefined).then(res => {
|
||||
os.api('users/show', { userId: props.user.id })
|
||||
.then((res) => {
|
||||
fullUserInfo.value = res;
|
||||
uiWindow.value?.close();
|
||||
page.value = 2;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user