diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 63ed1d595..2cb1c317d 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 000000000..9bb77e8e6 --- /dev/null +++ b/src/client/app/admin/views/abuse.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue index 9524a9854..5a1de2d76 100644 --- a/src/client/app/admin/views/index.vue +++ b/src/client/app/admin/views/index.vue @@ -27,6 +27,7 @@
  • {{ $t('emoji') }}
  • {{ $t('announcements') }}
  • {{ $t('hashtags') }}
  • +
  • {{ $t('abuse') }}
  • {{ $t('back-to-misskey') }} @@ -45,7 +46,7 @@
    -
    +
    @@ -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 000000000..a4a27142f --- /dev/null +++ b/src/client/app/common/views/components/user-menu.vue @@ -0,0 +1,157 @@ + + + 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 a856e74bf..e640caa58 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 @@ {{ user.followersCount | number }} {{ $t('followers') }} -
    - -
    @@ -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 b092a0003..c33ca84eb 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 @@ {{ user.notesCount | number }}{{ $t('posts') }} {{ user.followingCount | number }}{{ $t('following') }} {{ user.followersCount | number }}{{ $t('followers') }} -
    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 58afed400..22cbf6546 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 @@

    - - {{ $t('unmute') }} - {{ $t('mute') }} - - - {{ $t('unblock') }} - {{ $t('block') }} - - {{ $t('push-to-a-list') }} + {{ $t('menu') }}
    @@ -25,7 +17,7 @@ diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index 5f3feabb6..c475750cf 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -55,7 +55,6 @@ {{ user.followersCount | number }} {{ $t('followers') }} - @@ -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 000000000..1fe33f034 --- /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('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(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 000000000..c88174f13 --- /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 000000000..4d068a410 --- /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 000000000..25849acb4 --- /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(); +}));