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;
|
"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;
|
||||||
|
|
|
@ -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: "既存ユーザーのみ"
|
||||||
|
|
|
@ -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,
|
detail: true,
|
||||||
}) : null,
|
}) : null,
|
||||||
forwarded: report.forwarded,
|
forwarded: report.forwarded,
|
||||||
|
category: report.category,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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', {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue