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();
+}));