feat: 通報のカテゴリー化 (MisskeyIO#288)

This commit is contained in:
CyberRex 2023-12-28 12:16:34 +09:00 committed by GitHub
parent 8a8196aa09
commit 1478a6c4ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 171 additions and 9 deletions

22
locales/index.d.ts vendored
View file

@ -1190,6 +1190,28 @@ export interface Locale {
"decorate": string; "decorate": string;
"addMfmFunction": string; "addMfmFunction": string;
"enableQuickAddMfmFunction": 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": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;

View file

@ -1187,6 +1187,28 @@ seasonalScreenEffect: "季節に応じた画面の演出"
decorate: "デコる" decorate: "デコる"
addMfmFunction: "装飾を追加" addMfmFunction: "装飾を追加"
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
abuseReportCategory: "通報の種類"
selectCategory: "カテゴリを選択"
reportComplete: "通報完了"
blockThisUser: "このユーザーをブロックする"
muteThisUser: "このユーザーをミュートする"
_abuseReportMsgs:
rightsAbuseCantAccept: "申し訳ございません。権利侵害の通報は権利者ご本人からのみ受け付けております。"
_abuseReportCategory:
nsfw: "センシティブなコンテンツを含む投稿"
spam: "スパム"
explicit: "暴力もしくは攻撃的な投稿"
phishing: "フィッシングもしくは詐欺行為"
personalinfoleak: "本人もしくは他人の個人情報の漏えい"
selfharm: "自殺もしくは自害など生命に関わる問題"
criticalbreach: "重大な規約違反"
otherbreach: "その他の規約違反"
violationrights: "権利侵害もしくはなりすまし(本人)"
violationrightsother: "権利侵害(他人)"
notlike: "この人が気に入らない"
other: "その他"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"

View file

@ -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"`);
}
}

View file

@ -50,6 +50,7 @@ export class AbuseUserReportEntityService {
detail: true, detail: true,
}) : null, }) : null,
forwarded: report.forwarded, forwarded: report.forwarded,
category: report.category,
}); });
} }

View file

@ -67,6 +67,13 @@ export class MiAbuseUserReport {
}) })
public comment: string; public comment: string;
@Index()
@Column('varchar', {
length: 20, nullable: false,
default: 'other',
})
public category: string;
//#region Denormalized fields //#region Denormalized fields
@Index() @Index()
@Column('varchar', { @Column('varchar', {

View file

@ -12,6 +12,10 @@ export const packedAbuseUserReportSchema = {
format: 'id', format: 'id',
example: 'xxxxxxxxxx', example: 'xxxxxxxxxx',
}, },
category: {
type: 'string',
optional: false, nullable: false,
},
createdAt: { createdAt: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -74,6 +74,10 @@ export const meta = {
nullable: true, optional: true, nullable: true, optional: true,
ref: 'User', 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' }, reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
forwarded: { type: 'boolean', default: false }, forwarded: { type: 'boolean', default: false },
category: { type: 'string', nullable: true, default: null },
}, },
required: [], required: [],
} as const; } 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; 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(); const reports = await query.limit(ps.limit).getMany();
return await this.abuseUserReportEntityService.packMany(reports, me); return await this.abuseUserReportEntityService.packMany(reports, me);

View file

@ -48,6 +48,7 @@ export const paramDef = {
properties: { properties: {
userId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' },
comment: { type: 'string', minLength: 1, maxLength: 2048 }, comment: { type: 'string', minLength: 1, maxLength: 2048 },
category: { type: 'string', minLength: 1, maxLength: 20, default: 'other' },
}, },
required: ['userId', 'comment'], required: ['userId', 'comment'],
} as const; } as const;
@ -85,6 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
reporterId: me.id, reporterId: me.id,
reporterHost: null, reporterHost: null,
comment: ps.comment, comment: ps.comment,
category: ps.category,
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
this.queueService.createReportAbuseJob(report); this.queueService.createReportAbuseJob(report);

View file

@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.moderator }}: {{ i18n.ts.moderator }}:
<MkAcct :user="report.assignee"/> <MkAcct :user="report.assignee"/>
</div> </div>
<div v-if="report.category">カテゴリ: {{ i18n.t(`_abuseReportCategory.${report.category}`) }}</div>
<div><MkTime :time="report.createdAt"/></div> <div><MkTime :time="report.createdAt"/></div>
<div class="action"> <div class="action">
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved"> <MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <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> <template #header>
<i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i> <i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i>
<I18n :src="i18n.ts.reportAbuseOf" tag="span"> <I18n :src="i18n.ts.reportAbuseOf" tag="span">
@ -15,6 +15,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<MkSpacer :marginMin="20" :marginMax="28"> <MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps_m" :class="$style.root"> <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=""> <div class="">
<MkTextarea v-model="comment"> <MkTextarea v-model="comment">
<template #label>{{ i18n.ts.details }}</template> <template #label>{{ i18n.ts.details }}</template>
@ -22,7 +40,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkTextarea> </MkTextarea>
</div> </div>
<div class=""> <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>
</div> </div>
</MkSpacer> </MkSpacer>
@ -30,11 +65,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, shallowRef } from 'vue'; import { ref, shallowRef, Ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkSelect from '@/components/MkSelect.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -48,19 +84,59 @@ const emit = defineEmits<{
}>(); }>();
const uiWindow = shallowRef<InstanceType<typeof MkWindow>>(); const uiWindow = shallowRef<InstanceType<typeof MkWindow>>();
const uiWindow2 = shallowRef<InstanceType<typeof MkWindow>>();
const comment = ref(props.initialComment ?? ''); 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() { function send() {
os.apiWithDialog('users/report-abuse', { if (category.value === 'violationrightsother') {
userId: props.user.id,
comment: comment.value,
}, undefined).then(res => {
os.alert({ os.alert({
type: 'success', type: 'info',
text: i18n.ts.abuseReported, text: i18n.ts._abuseReportMsgs.rightsAbuseCantAccept
}); });
uiWindow.value?.close(); uiWindow.value?.close();
emit('closed'); 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> </script>