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 @@
+
+
+
+ {{ $t('title') }}
+
+
+
+
+
+ {{ $t('target') }}
+
+
+ {{ $t('reporter') }}
+
+
+
+ {{ $t('details') }}
+
+ {{ $t('remove-report') }}
+
+
+ {{ $t('@.load-more') }}
+
+
+
+
+
+
+
+
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') }}
@@ -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();
+}));