diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 63ed1d5956..2cb1c317db 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -380,6 +380,19 @@ common/views/components/note-menu.vue: delete-confirm: "この投稿を削除しますか?" remote: "投稿元で見る" +common/views/components/user-menu.vue: + mention: "メンション" + mute: "ミュート" + unmute: "ミュート解除" + block: "ブロック" + unblock: "ブロック解除" + push-to-list: "リストに追加" + select-list: "リストを選択してください" + list-pushed: "{user}を{list}に追加しました" + report-abuse: "スパムを報告" + report-abuse-detail: "どのような迷惑行為を行っていますか?" + report-abuse-reported: "管理者に報告されました。ご協力ありがとうございました。" + common/views/components/poll.vue: vote-to: "「{}」に投票する" vote-count: "{}票" @@ -1103,6 +1116,7 @@ admin/views/index.vue: federation: "連合" announcements: "お知らせ" hashtags: "ハッシュタグ" + abuse: "スパム報告" back-to-misskey: "Misskeyに戻る" admin/views/dashboard.vue: @@ -1114,6 +1128,13 @@ admin/views/dashboard.vue: this-instance: "このインスタンス" federated: "連合" +admin/views/abuse.vue: + title: "スパム報告" + target: "対象" + reporter: "報告者" + details: "詳細" + remove-report: "削除" + admin/views/instance.vue: instance: "インスタンス" instance-name: "インスタンス名" @@ -1384,20 +1405,12 @@ desktop/views/pages/user/user.profile.vue: stalk: "ストークする" stalking: "ストーキングしています" unstalk: "ストーク解除" - mute: "ミュートする" - muted: "ミュートしています" - unmute: "ミュート解除" - block: "ブロックする" - unblock: "ブロック解除" - block-confirm: "このユーザーをブロックしますか?" - push-to-a-list: "リストに追加" - list-pushed: "{user}を{list}に追加しました。" + menu: "メニュー" desktop/views/pages/user/user.header.vue: posts: "投稿" following: "フォロー" followers: "フォロワー" - mention: "メンション" is-bot: "このアカウントはBotです" years-old: "{age}歳" year: "年" @@ -1686,14 +1699,7 @@ mobile/views/pages/user.vue: overview: "概要" timeline: "タイムライン" media: "メディア" - mute: "ミュート" - unmute: "ミュート解除" - block: "ブロック" - unblock: "ブロック解除" years-old: "{age}歳" - push-to-list: "リストに追加" - select-list: "リストを選択してください" - list-pushed: "{user}を{list}に追加しました" mobile/views/pages/user/home.vue: recent-notes: "最近の投稿" @@ -1747,12 +1753,10 @@ deck/deck.user-column.vue: posts: "投稿" following: "フォロー" followers: "フォロワー" - mention: "メンション" images: "画像" activity: "アクティビティ" timeline: "タイムライン" pinned-notes: "ピン留めされた投稿" - push-to-a-list: "リストに追加" docs: edit-this-page-on-github: "間違いや改善点を見つけましたか?" diff --git a/src/client/app/admin/views/abuse.vue b/src/client/app/admin/views/abuse.vue new file mode 100644 index 0000000000..9bb77e8e6c --- /dev/null +++ b/src/client/app/admin/views/abuse.vue @@ -0,0 +1,87 @@ +<template> +<div class="wbjusose"> + <ui-card> + <div slot="title"><fa :icon="faExclamationCircle"/> {{ $t('title') }}</div> + <section class="fit-top"> + <sequential-entrance animation="entranceFromTop" delay="25"> + <div v-for="report in userReports" :key="report.id" class="haexwsjc"> + <ui-horizon-group inputs> + <ui-input :value="report.user | acct" type="text"> + <span>{{ $t('target') }}</span> + </ui-input> + <ui-input :value="report.reporter | acct" type="text"> + <span>{{ $t('reporter') }}</span> + </ui-input> + </ui-horizon-group> + <ui-textarea :value="report.comment" readonly> + <span>{{ $t('details') }}</span> + </ui-textarea> + <ui-button @click="removeReport(report)">{{ $t('remove-report') }}</ui-button> + </div> + </sequential-entrance> + <ui-button v-if="existMore" @click="fetchUserReports">{{ $t('@.load-more') }}</ui-button> + </section> + </ui-card> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../i18n'; +import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + i18n: i18n('admin/views/abuse.vue'), + + data() { + return { + limit: 10, + untilId: undefined, + userReports: [], + existMore: false, + faExclamationCircle + }; + }, + + mounted() { + this.fetchUserReports(); + }, + + methods: { + fetchUserReports() { + this.$root.api('admin/abuse-user-reports', { + untilId: this.untilId, + limit: this.limit + 1 + }).then(reports => { + if (reports.length == this.limit + 1) { + reports.pop(); + this.existMore = true; + } else { + this.existMore = false; + } + this.userReports = this.userReports.concat(reports); + this.untilId = this.userReports[this.userReports.length - 1].id; + }); + }, + + removeReport(report) { + this.$root.api('admin/remove-abuse-user-report', { + reportId: report.id + }).then(() => { + this.userReports = this.userReports.filter(r => r.id != report.id); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.wbjusose + @media (min-width 500px) + padding 16px + + .haexwsjc + padding-bottom 16px + border-bottom solid 1px var(--faceDivider) + +</style> diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue index 9524a98542..5a1de2d76a 100644 --- a/src/client/app/admin/views/index.vue +++ b/src/client/app/admin/views/index.vue @@ -27,6 +27,7 @@ <li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li> <li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li> <li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>{{ $t('hashtags') }}</li> + <li @click="nav('abuse')" :class="{ active: page == 'abuse' }"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</li> </ul> <div class="back-to-misskey"> <a href="/"><fa :icon="faArrowLeft"/> {{ $t('back-to-misskey') }}</a> @@ -45,7 +46,7 @@ <div v-if="page == 'announcements'"><x-announcements/></div> <div v-if="page == 'hashtags'"><x-hashtags/></div> <div v-if="page == 'drive'"><x-drive/></div> - <div v-if="page == 'update'"></div> + <div v-if="page == 'abuse'"><x-abuse/></div> </div> </main> </div> @@ -63,7 +64,8 @@ import XAnnouncements from "./announcements.vue"; import XHashtags from "./hashtags.vue"; import XUsers from "./users.vue"; import XDrive from "./drive.vue"; -import { faHeadset, faArrowLeft, faShareAlt } from '@fortawesome/free-solid-svg-icons'; +import XAbuse from "./abuse.vue"; +import { faHeadset, faArrowLeft, faShareAlt, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; import { faGrin } from '@fortawesome/free-regular-svg-icons'; // Detect the user agent @@ -81,6 +83,7 @@ export default Vue.extend({ XHashtags, XUsers, XDrive, + XAbuse, }, provide: { isMobile @@ -94,7 +97,8 @@ export default Vue.extend({ faGrin, faArrowLeft, faHeadset, - faShareAlt + faShareAlt, + faExclamationCircle }; }, methods: { diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue new file mode 100644 index 0000000000..a4a27142f9 --- /dev/null +++ b/src/client/app/common/views/components/user-menu.vue @@ -0,0 +1,157 @@ +<template> +<div style="position:initial"> + <mk-menu :source="source" :items="items" @closed="closed"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../i18n'; +import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; +import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + i18n: i18n('common/views/components/user-menu.vue'), + + props: ['user', 'source'], + + data() { + let menu = [{ + icon: ['fas', 'at'], + text: this.$t('mention'), + action: () => { + this.$post({ mention: this.user }); + } + }, null, { + icon: ['fas', 'list'], + text: this.$t('push-to-list'), + action: this.pushList + }, null, { + icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], + text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'), + action: this.toggleMute + }, { + icon: 'ban', + text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'), + action: this.toggleBlock + }, null, { + icon: faExclamationCircle, + text: this.$t('report-abuse'), + action: this.reportAbuse + }]; + + return { + items: menu + }; + }, + + methods: { + closed() { + this.$nextTick(() => { + this.destroyDom(); + }); + }, + + async pushList() { + const lists = await this.$root.api('users/lists/list'); + const { canceled, result: listId } = await this.$root.dialog({ + type: null, + title: this.$t('select-list'), + select: { + items: lists.map(list => ({ + value: list.id, text: list.title + })) + }, + showCancelButton: true + }); + if (canceled) return; + await this.$root.api('users/lists/push', { + listId: listId, + userId: this.user.id + }); + this.$root.dialog({ + type: 'success', + text: this.$t('list-pushed', { + user: this.user.name, + list: lists.find(l => l.id === listId).title + }) + }); + }, + + toggleMute() { + if (this.user.isMuted) { + this.$root.api('mute/delete', { + userId: this.user.id + }).then(() => { + this.user.isMuted = false; + }, () => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + } else { + this.$root.api('mute/create', { + userId: this.user.id + }).then(() => { + this.user.isMuted = true; + }, () => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + } + }, + + toggleBlock() { + if (this.user.isBlocking) { + this.$root.api('blocking/delete', { + userId: this.user.id + }).then(() => { + this.user.isBlocking = false; + }, () => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + } else { + this.$root.api('blocking/create', { + userId: this.user.id + }).then(() => { + this.user.isBlocking = true; + }, () => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + } + }, + + async reportAbuse() { + const reported = this.$t('report-abuse-reported'); // なぜか後で参照すると null になるので最初にメモリに確保しておく + const { canceled, result: comment } = await this.$root.dialog({ + title: this.$t('report-abuse-detail'), + input: true + }); + if (canceled) return; + this.$root.api('users/report-abuse', { + userId: this.user.id, + comment: comment + }).then(() => { + this.$root.dialog({ + type: 'success', + text: reported + }); + }, e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + } + } +}); +</script> diff --git a/src/client/app/desktop/views/pages/deck/deck.user-column.vue b/src/client/app/desktop/views/pages/deck/deck.user-column.vue index a856e74bf6..e640caa586 100644 --- a/src/client/app/desktop/views/pages/deck/deck.user-column.vue +++ b/src/client/app/desktop/views/pages/deck/deck.user-column.vue @@ -49,9 +49,6 @@ <b>{{ user.followersCount | number }}</b> <span>{{ $t('followers') }}</span> </div> - <div class="mention"> - <button @click="mention" :title="$t('mention')"><fa icon="at"/></button> - </div> </div> </div> <div class="pinned" v-if="user.pinnedNotes && user.pinnedNotes.length > 0"> @@ -100,8 +97,7 @@ import parseAcct from '../../../../../../misc/acct/parse'; import XColumn from './deck.column.vue'; import XNotes from './deck.notes.vue'; import XNote from '../../components/note.vue'; -import Menu from '../../../../common/views/components/menu.vue'; -import MkUserListsWindow from '../../components/user-lists-window.vue'; +import XUserMenu from '../../../../common/views/components/user-menu.vue'; import { concat } from '../../../../../../prelude/array'; import * as ApexCharts from 'apexcharts'; @@ -306,33 +302,10 @@ export default Vue.extend({ return promise; }, - mention() { - this.$post({ mention: this.user }); - }, - menu() { - let menu = [{ - icon: 'list', - text: this.$t('push-to-a-list'), - action: () => { - const w = this.$root.new(MkUserListsWindow); - w.$once('choosen', async list => { - w.close(); - await this.$root.api('users/lists/push', { - listId: list.id, - userId: this.user.id - }); - this.$root.dialog({ - type: 'success', - splash: true - }); - }); - } - }]; - - this.$root.new(Menu, { + this.$root.new(XUserMenu, { source: this.$refs.menu, - items: menu + user: this.user }); }, @@ -459,7 +432,7 @@ export default Vue.extend({ > .counts display grid - grid-template-columns 2fr 2fr 2fr 1fr + grid-template-columns 2fr 2fr 2fr margin-top 8px border-top solid var(--lineWidth) var(--faceDivider) @@ -476,9 +449,6 @@ export default Vue.extend({ font-size 80% opacity 0.7 - > .mention - display flex - > * > p.caption margin 0 diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue index b092a0003e..c33ca84ebc 100644 --- a/src/client/app/desktop/views/pages/user/user.header.vue +++ b/src/client/app/desktop/views/pages/user/user.header.vue @@ -36,7 +36,6 @@ <span class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</span> <router-link :to="user | userPage('following')" class="following clickable"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link> <router-link :to="user | userPage('followers')" class="followers clickable"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link> - <button @click="mention" :title="$t('mention')"><fa icon="at"/></button> </div> </div> </div> diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue index 58afed4001..22cbf6546f 100644 --- a/src/client/app/desktop/views/pages/user/user.profile.vue +++ b/src/client/app/desktop/views/pages/user/user.profile.vue @@ -9,15 +9,7 @@ </p> </div> <div class="action-form"> - <ui-button @click="user.isMuted ? unmute() : mute()" v-if="$store.state.i.id != user.id"> - <span v-if="user.isMuted"><fa icon="eye"/> {{ $t('unmute') }}</span> - <span v-else><fa :icon="['far', 'eye-slash']"/> {{ $t('mute') }}</span> - </ui-button> - <ui-button @click="user.isBlocking ? unblock() : block()" v-if="$store.state.i.id != user.id"> - <span v-if="user.isBlocking"><fa icon="ban"/> {{ $t('unblock') }}</span> - <span v-else><fa icon="ban"/> {{ $t('block') }}</span> - </ui-button> - <ui-button @click="list"><fa icon="list"/> {{ $t('push-to-a-list') }}</ui-button> + <ui-button @click="menu" ref="menu">{{ $t('menu') }}</ui-button> </div> </div> </template> @@ -25,7 +17,7 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../../../i18n'; -import MkUserListsWindow from '../../components/user-lists-window.vue'; +import XUserMenu from '../../../../common/views/components/user-menu.vue'; export default Vue.extend({ i18n: i18n('desktop/views/pages/user/user.profile.vue'), @@ -52,72 +44,12 @@ export default Vue.extend({ }); }, - mute() { - this.$root.api('mute/create', { - userId: this.user.id - }).then(() => { - this.user.isMuted = true; - }, () => { - alert('error'); + menu() { + this.$root.new(XUserMenu, { + source: this.$refs.menu.$el, + user: this.user }); }, - - unmute() { - this.$root.api('mute/delete', { - userId: this.user.id - }).then(() => { - this.user.isMuted = false; - }, () => { - alert('error'); - }); - }, - - block() { - this.$root.dialog({ - type: 'warning', - text: this.$t('block-confirm'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('blocking/create', { - userId: this.user.id - }).then(() => { - this.user.isBlocking = true; - }, () => { - alert('error'); - }); - }); - }, - - unblock() { - this.$root.api('blocking/delete', { - userId: this.user.id - }).then(() => { - this.user.isBlocking = false; - }, () => { - alert('error'); - }); - }, - - list() { - const w = this.$root.new(MkUserListsWindow); - w.$once('choosen', async list => { - w.close(); - await this.$root.api('users/lists/push', { - listId: list.id, - userId: this.user.id - }); - this.$root.dialog({ - type: 'success', - title: 'Done!', - text: this.$t('list-pushed', { - user: this.user.name, - list: list.title - }) - }); - }); - } } }); </script> diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index 5f3feabb6e..c475750cf2 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -55,7 +55,6 @@ <b>{{ user.followersCount | number }}</b> <i>{{ $t('followers') }}</i> </a> - <button @click="mention"><fa icon="at"/></button> </div> </div> </header> @@ -81,7 +80,7 @@ import i18n from '../../../i18n'; import * as age from 's-age'; import parseAcct from '../../../../../misc/acct/parse'; import Progress from '../../../common/scripts/loading'; -import Menu from '../../../common/views/components/menu.vue'; +import XUserMenu from '../../../common/views/components/user-menu.vue'; import XHome from './user/home.vue'; export default Vue.extend({ @@ -127,88 +126,10 @@ export default Vue.extend({ }); }, - mention() { - this.$post({ mention: this.user }); - }, - menu() { - let menu = [{ - icon: ['fas', 'list'], - text: this.$t('push-to-list'), - action: async () => { - const lists = await this.$root.api('users/lists/list'); - const { canceled, result: listId } = await this.$root.dialog({ - type: null, - title: this.$t('select-list'), - select: { - items: lists.map(list => ({ - value: list.id, text: list.title - })) - }, - showCancelButton: true - }); - if (canceled) return; - await this.$root.api('users/lists/push', { - listId: listId, - userId: this.user.id - }); - this.$root.dialog({ - type: 'success', - text: this.$t('list-pushed', { - user: this.user.name, - list: lists.find(l => l.id === listId).title - }) - }); - } - }, null, { - icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], - text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'), - action: () => { - if (this.user.isMuted) { - this.$root.api('mute/delete', { - userId: this.user.id - }).then(() => { - this.user.isMuted = false; - }, () => { - alert('error'); - }); - } else { - this.$root.api('mute/create', { - userId: this.user.id - }).then(() => { - this.user.isMuted = true; - }, () => { - alert('error'); - }); - } - } - }, { - icon: 'ban', - text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'), - action: () => { - if (this.user.isBlocking) { - this.$root.api('blocking/delete', { - userId: this.user.id - }).then(() => { - this.user.isBlocking = false; - }, () => { - alert('error'); - }); - } else { - this.$root.api('blocking/create', { - userId: this.user.id - }).then(() => { - this.user.isBlocking = true; - }, () => { - alert('error'); - }); - } - } - }]; - - this.$root.new(Menu, { + this.$root.new(XUserMenu, { source: this.$refs.menu, - items: menu + user: this.user }); }, } diff --git a/src/models/abuse-user-report.ts b/src/models/abuse-user-report.ts new file mode 100644 index 0000000000..1fe33f0342 --- /dev/null +++ b/src/models/abuse-user-report.ts @@ -0,0 +1,52 @@ +import * as mongo from 'mongodb'; +const deepcopy = require('deepcopy'); +import db from '../db/mongodb'; +import isObjectId from '../misc/is-objectid'; +import { pack as packUser } from './user'; + +const AbuseUserReport = db.get<IAbuseUserReport>('abuseUserReports'); +AbuseUserReport.createIndex('userId'); +AbuseUserReport.createIndex('reporterId'); +AbuseUserReport.createIndex(['userId', 'reporterId'], { unique: true }); +export default AbuseUserReport; + +export interface IAbuseUserReport { + _id: mongo.ObjectID; + createdAt: Date; + userId: mongo.ObjectID; + reporterId: mongo.ObjectID; + comment: string; +} + +export const packMany = ( + reports: (string | mongo.ObjectID | IAbuseUserReport)[] +) => { + return Promise.all(reports.map(x => pack(x))); +}; + +export const pack = ( + report: any +) => new Promise<any>(async (resolve, reject) => { + let _report: any; + + if (isObjectId(report)) { + _report = await AbuseUserReport.findOne({ + _id: report + }); + } else if (typeof report === 'string') { + _report = await AbuseUserReport.findOne({ + _id: new mongo.ObjectID(report) + }); + } else { + _report = deepcopy(report); + } + + // Rename _id to id + _report.id = _report._id; + delete _report._id; + + _report.reporter = await packUser(_report.reporterId, null, { detail: true }); + _report.user = await packUser(_report.userId, null, { detail: true }); + + resolve(_report); +}); diff --git a/src/server/api/endpoints/admin/abuse-user-reports.ts b/src/server/api/endpoints/admin/abuse-user-reports.ts new file mode 100644 index 0000000000..c88174f13f --- /dev/null +++ b/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -0,0 +1,54 @@ +import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; +import Report, { packMany } from '../../../../models/abuse-user-report'; +import define from '../../define'; + +export const meta = { + requireCredential: true, + requireModerator: true, + + params: { + limit: { + validator: $.num.optional.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.type(ID).optional, + transform: transform, + }, + + untilId: { + validator: $.type(ID).optional, + transform: transform, + }, + } +}; + +export default define(meta, (ps) => new Promise(async (res, rej) => { + if (ps.sinceId && ps.untilId) { + return rej('cannot set sinceId and untilId'); + } + + const sort = { + _id: -1 + }; + const query = {} as any; + if (ps.sinceId) { + sort._id = 1; + query._id = { + $gt: ps.sinceId + }; + } else if (ps.untilId) { + query._id = { + $lt: ps.untilId + }; + } + + const reports = await Report + .find(query, { + limit: ps.limit, + sort: sort + }); + + res(await packMany(reports)); +})); diff --git a/src/server/api/endpoints/admin/remove-abuse-user-report.ts b/src/server/api/endpoints/admin/remove-abuse-user-report.ts new file mode 100644 index 0000000000..4d068a410e --- /dev/null +++ b/src/server/api/endpoints/admin/remove-abuse-user-report.ts @@ -0,0 +1,32 @@ +import $ from 'cafy'; +import ID, { transform } from '../../../../misc/cafy-id'; +import define from '../../define'; +import AbuseUserReport from '../../../../models/abuse-user-report'; + +export const meta = { + requireCredential: true, + requireModerator: true, + + params: { + reportId: { + validator: $.type(ID), + transform: transform + }, + } +}; + +export default define(meta, (ps) => new Promise(async (res, rej) => { + const report = await AbuseUserReport.findOne({ + _id: ps.reportId + }); + + if (report == null) { + return rej('report not found'); + } + + await AbuseUserReport.remove({ + _id: report._id + }); + + res(); +})); diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts new file mode 100644 index 0000000000..25849acb42 --- /dev/null +++ b/src/server/api/endpoints/users/report-abuse.ts @@ -0,0 +1,62 @@ +import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; +import define from '../../define'; +import User from '../../../../models/user'; +import AbuseUserReport from '../../../../models/abuse-user-report'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーを迷惑なユーザーであると報告します。' + }, + + requireCredential: true, + + params: { + userId: { + validator: $.type(ID), + transform: transform, + desc: { + 'ja-JP': '対象のユーザーのID', + 'en-US': 'Target user ID' + } + }, + + comment: { + validator: $.str.range(1, 3000), + desc: { + 'ja-JP': '迷惑行為の詳細' + } + }, + } +}; + +export default define(meta, (ps, me) => new Promise(async (res, rej) => { + // Lookup user + const user = await User.findOne({ + _id: ps.userId + }, { + fields: { + _id: true + } + }); + + if (user === null) { + return rej('user not found'); + } + + if (user._id.equals(me._id)) { + return rej('cannot report yourself'); + } + + if (user.isAdmin) { + return rej('cannot report admin'); + } + + await AbuseUserReport.insert({ + createdAt: new Date(), + userId: user._id, + reporterId: me._id, + comment: ps.comment + }); + + res(); +}));