From df38c2f485937d72c495d3195804830b09aa3e09 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Wed, 4 Apr 2018 20:29:26 +0900
Subject: [PATCH 01/15] Extract http request from post delivery job

---
 src/post/distribute.ts             |  96 +++++++++++++++++++++++++--
 src/processor/http/deliver-post.ts | 100 ++++-------------------------
 2 files changed, 104 insertions(+), 92 deletions(-)

diff --git a/src/post/distribute.ts b/src/post/distribute.ts
index 49c6eb22d..ad699d6b8 100644
--- a/src/post/distribute.ts
+++ b/src/post/distribute.ts
@@ -1,8 +1,11 @@
+import Channel from '../models/channel';
+import ChannelWatching from '../models/channel-watching';
+import Following from '../models/following';
 import Mute from '../models/mute';
 import Post, { pack } from '../models/post';
 import Watching from '../models/post-watching';
-import User from '../models/user';
-import stream from '../publishers/stream';
+import User, { isLocalUser } from '../models/user';
+import stream, { publishChannelStream } from '../publishers/stream';
 import notify from '../publishers/notify';
 import pushSw from '../publishers/push-sw';
 import queue from '../queue';
@@ -21,10 +24,6 @@ export default async (user, mentions, post) => {
 				latestPost: post._id
 			}
 		}),
-		new Promise((resolve, reject) => queue.create('http', {
-			type: 'deliverPost',
-			id: post._id,
-		}).save(error => error ? reject(error) : resolve())),
 	] as Array<Promise<any>>;
 
 	function addMention(promisedMentionee, reason) {
@@ -50,6 +49,91 @@ export default async (user, mentions, post) => {
 		}));
 	}
 
+	// タイムラインへの投稿
+	if (!post.channelId) {
+		promises.push(
+			// Publish event to myself's stream
+			promisedPostObj.then(postObj => {
+				stream(post.userId, 'post', postObj);
+			}),
+
+			Promise.all([
+				User.findOne({ _id: post.userId }),
+
+				// Fetch all followers
+				Following.aggregate([{
+					$lookup: {
+						from: 'users',
+						localField: 'followerId',
+						foreignField: '_id',
+						as: 'follower'
+					}
+				}, {
+					$match: {
+						followeeId: post.userId
+					}
+				}], {
+					_id: false
+				})
+			]).then(([user, followers]) => Promise.all(followers.map(following => {
+				if (isLocalUser(following.follower)) {
+					// Publish event to followers stream
+					return promisedPostObj.then(postObj => {
+						stream(following.followerId, 'post', postObj);
+					});
+				}
+
+				return new Promise((resolve, reject) => {
+					queue.create('http', {
+						type: 'deliverPost',
+						fromId: user._id,
+						toId: following.followerId,
+						postId: post._id
+					}).save(error => {
+						if (error) {
+							reject(error);
+						} else {
+							resolve();
+						}
+					});
+				});
+			})))
+		);
+	}
+
+	// チャンネルへの投稿
+	if (post.channelId) {
+		promises.push(
+			// Increment channel index(posts count)
+			Channel.update({ _id: post.channelId }, {
+				$inc: {
+					index: 1
+				}
+			}),
+
+			// Publish event to channel
+			promisedPostObj.then(postObj => {
+				publishChannelStream(post.channelId, 'post', postObj);
+			}),
+
+			Promise.all([
+				promisedPostObj,
+
+				// Get channel watchers
+				ChannelWatching.find({
+					channelId: post.channelId,
+					// 削除されたドキュメントは除く
+					deletedAt: { $exists: false }
+				})
+			]).then(([postObj, watches]) => {
+				// チャンネルの視聴者(のタイムライン)に配信
+				watches.forEach(w => {
+					stream(w.userId, 'post', postObj);
+				});
+			})
+		);
+	}
+
 	// If has in reply to post
 	if (post.replyId) {
 		promises.push(
diff --git a/src/processor/http/deliver-post.ts b/src/processor/http/deliver-post.ts
index c00ab912c..48ad4f95a 100644
--- a/src/processor/http/deliver-post.ts
+++ b/src/processor/http/deliver-post.ts
@@ -1,93 +1,21 @@
-import Channel from '../../models/channel';
-import Following from '../../models/following';
-import ChannelWatching from '../../models/channel-watching';
-import Post, { pack } from '../../models/post';
-import User, { isLocalUser } from '../../models/user';
-import stream, { publishChannelStream } from '../../publishers/stream';
+import Post from '../../models/post';
+import User, { IRemoteUser } from '../../models/user';
 import context from '../../remote/activitypub/renderer/context';
 import renderCreate from '../../remote/activitypub/renderer/create';
 import renderNote from '../../remote/activitypub/renderer/note';
 import request from '../../remote/request';
 
-export default ({ data }) => Post.findOne({ _id: data.id }).then(post => {
-	const promisedPostObj = pack(post);
-	const promises = [];
+export default async ({ data }) => {
+	const promisedTo = User.findOne({ _id: data.toId }) as Promise<IRemoteUser>;
+	const [from, post] = await Promise.all([
+		User.findOne({ _id: data.fromId }),
+		Post.findOne({ _id: data.postId })
+	]);
+	const note = await renderNote(from, post);
+	const to = await promisedTo;
+	const create = renderCreate(note);
 
-	// タイムラインへの投稿
-	if (!post.channelId) {
-		promises.push(
-			// Publish event to myself's stream
-			promisedPostObj.then(postObj => {
-				stream(post.userId, 'post', postObj);
-			}),
+	create['@context'] = context;
 
-			Promise.all([
-				User.findOne({ _id: post.userId }),
-
-				// Fetch all followers
-				Following.aggregate([{
-					$lookup: {
-						from: 'users',
-						localField: 'followerId',
-						foreignField: '_id',
-						as: 'follower'
-					}
-				}, {
-					$match: {
-						followeeId: post.userId
-					}
-				}], {
-					_id: false
-				})
-			]).then(([user, followers]) => Promise.all(followers.map(following => {
-				if (isLocalUser(following.follower)) {
-					// Publish event to followers stream
-					return promisedPostObj.then(postObj => {
-						stream(following.followerId, 'post', postObj);
-					});
-				}
-
-				return renderNote(user, post).then(note => {
-					const create = renderCreate(note);
-					create['@context'] = context;
-					return request(user, following.follower[0].account.inbox, create);
-				});
-			})))
-		);
-	}
-
-	// チャンネルへの投稿
-	if (post.channelId) {
-		promises.push(
-			// Increment channel index(posts count)
-			Channel.update({ _id: post.channelId }, {
-				$inc: {
-					index: 1
-				}
-			}),
-
-			// Publish event to channel
-			promisedPostObj.then(postObj => {
-				publishChannelStream(post.channelId, 'post', postObj);
-			}),
-
-			Promise.all([
-				promisedPostObj,
-
-				// Get channel watchers
-				ChannelWatching.find({
-					channelId: post.channelId,
-					// 削除されたドキュメントは除く
-					deletedAt: { $exists: false }
-				})
-			]).then(([postObj, watches]) => {
-				// チャンネルの視聴者(のタイムライン)に配信
-				watches.forEach(w => {
-					stream(w.userId, 'post', postObj);
-				});
-			})
-		);
-	}
-
-	return Promise.all(promises);
-});
+	return request(from, to.account.inbox, create);
+};

From 1b6bae72c2aa19141133bbaf6939a4a5dded03b1 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Wed, 4 Apr 2018 21:56:04 +0900
Subject: [PATCH 02/15] Make HTTP request first in follow processor

---
 src/processor/http/follow.ts | 118 +++++++++++++++++------------------
 1 file changed, 59 insertions(+), 59 deletions(-)

diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index 8bf890efb..ed36fa18d 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -1,4 +1,4 @@
-import User, { isLocalUser, pack as packUser } from '../../models/user';
+import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../models/user';
 import Following from '../../models/following';
 import FollowingLog from '../../models/following-log';
 import FollowedLog from '../../models/followed-log';
@@ -7,63 +7,63 @@ import notify from '../../publishers/notify';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/follow';
 import request from '../../remote/request';
+import Logger from '../../utils/logger';
 
-export default ({ data }) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
-	const promisedFollower = User.findOne({ _id: followerId });
-	const promisedFollowee = User.findOne({ _id: followeeId });
-
-	return Promise.all([
-		// Increment following count
-		User.update(followerId, {
-			$inc: {
-				followingCount: 1
-			}
-		}),
-
-		promisedFollower.then(({ followingCount }) => FollowingLog.insert({
-			createdAt: data.following.createdAt,
-			userId: followerId,
-			count: followingCount + 1
-		})),
-
-		// Increment followers count
-		User.update({ _id: followeeId }, {
-			$inc: {
-				followersCount: 1
-			}
-		}),
-
-		promisedFollowee.then(({ followersCount }) => FollowedLog.insert({
-			createdAt: data.following.createdAt,
-			userId: followerId,
-			count: followersCount + 1
-		})),
-
-		// Notify
-		promisedFollowee.then(followee => followee.host === null ?
-			notify(followeeId, followerId, 'follow') : null),
-
-		// Publish follow event
-		Promise.all([promisedFollower, promisedFollowee]).then(([follower, followee]) => {
-			let followerEvent;
-			let followeeEvent;
-
-			if (isLocalUser(follower)) {
-				followerEvent = packUser(followee, follower)
-					.then(packed => event(follower._id, 'follow', packed));
-			}
-
-			if (isLocalUser(followee)) {
-				followeeEvent = packUser(follower, followee)
-					.then(packed => event(followee._id, 'followed', packed));
-			} else if (isLocalUser(follower)) {
-				const rendered = render(follower, followee);
-				rendered['@context'] = context;
-
-				followeeEvent = request(follower, followee.account.inbox, rendered);
-			}
-
-			return Promise.all([followerEvent, followeeEvent]);
-		})
+export default async ({ data }) => {
+	const { followerId, followeeId } = await Following.findOne({ _id: data.following });
+	const [follower, followee] = await Promise.all([
+		User.findOne({ _id: followerId }),
+		User.findOne({ _id: followeeId })
 	]);
-});
+
+	if (isLocalUser(follower) && isRemoteUser(followee)) {
+		const rendered = render(follower, followee);
+		rendered['@context'] = context;
+
+		await request(follower, followee.account.inbox, rendered);
+	}
+
+	try {
+		await Promise.all([
+			// Increment following count
+			User.update(followerId, {
+				$inc: {
+					followingCount: 1
+				}
+			}),
+
+			FollowingLog.insert({
+				createdAt: data.following.createdAt,
+				userId: followerId,
+				count: follower.followingCount + 1
+			}),
+
+			// Increment followers count
+			User.update({ _id: followeeId }, {
+				$inc: {
+					followersCount: 1
+				}
+			}),
+
+			FollowedLog.insert({
+				createdAt: data.following.createdAt,
+				userId: followerId,
+				count: followee.followersCount + 1
+			}),
+
+			// Publish follow event
+			isLocalUser(follower) && packUser(followee, follower)
+				.then(packed => event(follower._id, 'follow', packed)),
+
+			isLocalUser(followee) && Promise.all([
+				packUser(follower, followee)
+					.then(packed => event(followee._id, 'followed', packed)),
+
+				// Notify
+				isLocalUser(followee) && notify(followeeId, followerId, 'follow')
+			])
+		]);
+	} catch (error) {
+		Logger.error(error.toString());
+	}
+};

From 86b1345c17482e188be2138f0247a4b204f2abc1 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Wed, 4 Apr 2018 22:05:12 +0900
Subject: [PATCH 03/15] Make HTTP request first in unfollow job

---
 src/processor/http/unfollow.ts | 81 ++++++++++++++++++----------------
 1 file changed, 44 insertions(+), 37 deletions(-)

diff --git a/src/processor/http/unfollow.ts b/src/processor/http/unfollow.ts
index d3d5f2246..fbfd7b342 100644
--- a/src/processor/http/unfollow.ts
+++ b/src/processor/http/unfollow.ts
@@ -1,56 +1,63 @@
 import FollowedLog from '../../models/followed-log';
 import Following from '../../models/following';
 import FollowingLog from '../../models/following-log';
-import User, { isRemoteUser, pack as packUser } from '../../models/user';
+import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../models/user';
 import stream from '../../publishers/stream';
 import renderFollow from '../../remote/activitypub/renderer/follow';
 import renderUndo from '../../remote/activitypub/renderer/undo';
 import context from '../../remote/activitypub/renderer/context';
 import request from '../../remote/request';
+import Logger from '../../utils/logger';
 
 export default async ({ data }) => {
-	// Delete following
-	const following = await Following.findOneAndDelete({ _id: data.id });
+	const following = await Following.findOne({ _id: data.id });
 	if (following === null) {
 		return;
 	}
 
-	const promisedFollower = User.findOne({ _id: following.followerId });
-	const promisedFollowee = User.findOne({ _id: following.followeeId });
+	const [follower, followee] = await Promise.all([
+		User.findOne({ _id: following.followerId }),
+		User.findOne({ _id: following.followeeId })
+	]);
 
-	await Promise.all([
-		// Decrement following count
-		User.update({ _id: following.followerId }, { $inc: { followingCount: -1 } }),
-		promisedFollower.then(({ followingCount }) => FollowingLog.insert({
-			createdAt: new Date(),
-			userId: following.followerId,
-			count: followingCount - 1
-		})),
+	if (isLocalUser(follower) && isRemoteUser(followee)) {
+		const undo = renderUndo(renderFollow(follower, followee));
+		undo['@context'] = context;
 
-		// Decrement followers count
-		User.update({ _id: following.followeeId }, { $inc: { followersCount: -1 } }),
-		promisedFollowee.then(({ followersCount }) => FollowedLog.insert({
-			createdAt: new Date(),
-			userId: following.followeeId,
-			count: followersCount - 1
-		})),
+		await request(follower, followee.account.inbox, undo);
+	}
+
+	try {
+		await Promise.all([
+			// Delete following
+			Following.findOneAndDelete({ _id: data.id }),
+
+			// Decrement following count
+			User.update({ _id: follower._id }, { $inc: { followingCount: -1 } }),
+			FollowingLog.insert({
+				createdAt: new Date(),
+				userId: follower._id,
+				count: follower.followingCount - 1
+			}),
+
+			// Decrement followers count
+			User.update({ _id: followee._id }, { $inc: { followersCount: -1 } }),
+			FollowedLog.insert({
+				createdAt: new Date(),
+				userId: followee._id,
+				count: followee.followersCount - 1
+			})
+		]);
+
+		if (isLocalUser(follower)) {
+			return;
+		}
+
+		const promisedPackedUser = packUser(followee, follower);
 
 		// Publish follow event
-		Promise.all([promisedFollower, promisedFollowee]).then(async ([follower, followee]) => {
-			if (isRemoteUser(follower)) {
-				return;
-			}
-
-			const promisedPackedUser = packUser(followee, follower);
-
-			if (isRemoteUser(followee)) {
-				const undo = renderUndo(renderFollow(follower, followee));
-				undo['@context'] = context;
-
-				await request(follower, followee.account.inbox, undo);
-			}
-
-			stream(follower._id, 'unfollow', promisedPackedUser);
-		})
-	]);
+		stream(follower._id, 'unfollow', promisedPackedUser);
+	} catch (error) {
+		Logger.error(error.toString());
+	}
 };

From d7c13b975f55c85b695b72a3ded3d5de97227414 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Wed, 4 Apr 2018 22:45:55 +0900
Subject: [PATCH 04/15] Retry HTTP requests

---
 src/following/distribute.ts                   | 42 +++++++++++++++++++
 src/index.ts                                  |  2 +-
 src/post/distribute.ts                        |  4 +-
 src/processor/http/perform-activitypub.ts     |  7 ----
 src/processor/index.ts                        | 18 --------
 src/queue.ts                                  | 10 -----
 src/queue/index.ts                            | 38 +++++++++++++++++
 .../processors}/db/delete-post-dependents.ts  | 12 +++---
 .../processors}/db/index.ts                   |  0
 .../processors}/http/deliver-post.ts          | 12 +++---
 .../processors}/http/follow.ts                | 20 ++++-----
 .../processors}/http/index.ts                 |  0
 .../processors/http/perform-activitypub.ts    |  7 ++++
 .../processors}/http/process-inbox.ts         | 10 ++---
 .../processors}/http/report-github-failure.ts |  4 +-
 .../processors}/http/unfollow.ts              | 20 ++++-----
 src/remote/activitypub/act/follow.ts          |  4 +-
 src/remote/activitypub/act/undo/unfollow.ts   |  4 +-
 src/remote/activitypub/delete/post.ts         |  4 +-
 src/remote/activitypub/resolve-person.ts      |  4 +-
 src/server/activitypub/inbox.ts               |  4 +-
 src/server/api/endpoints/following/create.ts  |  4 +-
 src/server/api/endpoints/following/delete.ts  |  4 +-
 src/server/api/service/github.ts              |  4 +-
 24 files changed, 145 insertions(+), 93 deletions(-)
 create mode 100644 src/following/distribute.ts
 delete mode 100644 src/processor/http/perform-activitypub.ts
 delete mode 100644 src/processor/index.ts
 delete mode 100644 src/queue.ts
 create mode 100644 src/queue/index.ts
 rename src/{processor => queue/processors}/db/delete-post-dependents.ts (59%)
 rename src/{processor => queue/processors}/db/index.ts (100%)
 rename src/{processor => queue/processors}/http/deliver-post.ts (55%)
 rename src/{processor => queue/processors}/http/follow.ts (74%)
 rename src/{processor => queue/processors}/http/index.ts (100%)
 create mode 100644 src/queue/processors/http/perform-activitypub.ts
 rename src/{processor => queue/processors}/http/process-inbox.ts (76%)
 rename src/{processor => queue/processors}/http/report-github-failure.ts (85%)
 rename src/{processor => queue/processors}/http/unfollow.ts (71%)

diff --git a/src/following/distribute.ts b/src/following/distribute.ts
new file mode 100644
index 000000000..10ff98881
--- /dev/null
+++ b/src/following/distribute.ts
@@ -0,0 +1,42 @@
+import User, { pack as packUser } from '../models/user';
+import FollowingLog from '../models/following-log';
+import FollowedLog from '../models/followed-log';
+import event from '../publishers/stream';
+import notify from '../publishers/notify';
+
+export default async (follower, followee) => Promise.all([
+	// Increment following count
+	User.update(follower._id, {
+		$inc: {
+			followingCount: 1
+		}
+	}),
+
+	FollowingLog.insert({
+		createdAt: new Date(),
+		userId: followee._id,
+		count: follower.followingCount + 1
+	}),
+
+	// Increment followers count
+	User.update({ _id: followee._id }, {
+		$inc: {
+			followersCount: 1
+		}
+	}),
+
+	FollowedLog.insert({
+		createdAt: new Date(),
+		userId: follower._id,
+		count: followee.followersCount + 1
+	}),
+
+	followee.host === null && Promise.all([
+		// Notify
+		notify(followee.id, follower.id, 'follow'),
+
+		// Publish follow event
+		packUser(follower, followee)
+			.then(packed => event(followee._id, 'followed', packed))
+	])
+]);
diff --git a/src/index.ts b/src/index.ts
index 29c4f3431..21fb2f553 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -99,7 +99,7 @@ async function workerMain(opt) {
 
 	if (!opt['only-server']) {
 		// start processor
-		require('./processor').default();
+		require('./queue').process();
 	}
 
 	// Send a 'ready' message to parent process
diff --git a/src/post/distribute.ts b/src/post/distribute.ts
index ad699d6b8..f748a620c 100644
--- a/src/post/distribute.ts
+++ b/src/post/distribute.ts
@@ -8,7 +8,7 @@ import User, { isLocalUser } from '../models/user';
 import stream, { publishChannelStream } from '../publishers/stream';
 import notify from '../publishers/notify';
 import pushSw from '../publishers/push-sw';
-import queue from '../queue';
+import { createHttp } from '../queue';
 import watch from './watch';
 
 export default async (user, mentions, post) => {
@@ -84,7 +84,7 @@ export default async (user, mentions, post) => {
 				}
 
 				return new Promise((resolve, reject) => {
-					queue.create('http', {
+					createHttp({
 						type: 'deliverPost',
 						fromId: user._id,
 						toId: following.followerId,
diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts
deleted file mode 100644
index 963e532fe..000000000
--- a/src/processor/http/perform-activitypub.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import User from '../../models/user';
-import act from '../../remote/activitypub/act';
-import Resolver from '../../remote/activitypub/resolver';
-
-export default ({ data }) => User.findOne({ _id: data.actor })
-	.then(actor => act(new Resolver(), actor, data.outbox))
-	.then(Promise.all);
diff --git a/src/processor/index.ts b/src/processor/index.ts
deleted file mode 100644
index 172048dda..000000000
--- a/src/processor/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import queue from '../queue';
-import db from './db';
-import http from './http';
-
-export default () => {
-	queue.process('db', db);
-
-	/*
-		256 is the default concurrency limit of Mozilla Firefox and Google
-		Chromium.
-
-		a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google
-		https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff
-		Network.http.max-connections - MozillaZine Knowledge Base
-		http://kb.mozillazine.org/Network.http.max-connections
-	*/
-	queue.process('http', 256, http);
-};
diff --git a/src/queue.ts b/src/queue.ts
deleted file mode 100644
index 08ea13c2a..000000000
--- a/src/queue.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createQueue } from 'kue';
-import config from './config';
-
-export default createQueue({
-	redis: {
-		port: config.redis.port,
-		host: config.redis.host,
-		auth: config.redis.pass
-	}
-});
diff --git a/src/queue/index.ts b/src/queue/index.ts
new file mode 100644
index 000000000..f90754a56
--- /dev/null
+++ b/src/queue/index.ts
@@ -0,0 +1,38 @@
+import { createQueue } from 'kue';
+import config from '../config';
+import db from './processors/db';
+import http from './processors/http';
+
+const queue = createQueue({
+	redis: {
+		port: config.redis.port,
+		host: config.redis.host,
+		auth: config.redis.pass
+	}
+});
+
+export function createHttp(data) {
+	return queue
+		.create('http', data)
+		.attempts(16)
+		.backoff({ delay: 16384, type: 'exponential' });
+}
+
+export function createDb(data) {
+	return queue.create('db', data);
+}
+
+export function process() {
+	queue.process('db', db);
+
+	/*
+		256 is the default concurrency limit of Mozilla Firefox and Google
+		Chromium.
+
+		a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google
+		https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff
+		Network.http.max-connections - MozillaZine Knowledge Base
+		http://kb.mozillazine.org/Network.http.max-connections
+	*/
+	queue.process('http', 256, http);
+}
diff --git a/src/processor/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts
similarity index 59%
rename from src/processor/db/delete-post-dependents.ts
rename to src/queue/processors/db/delete-post-dependents.ts
index 879c41ec9..6de21eb05 100644
--- a/src/processor/db/delete-post-dependents.ts
+++ b/src/queue/processors/db/delete-post-dependents.ts
@@ -1,9 +1,9 @@
-import Favorite from '../../models/favorite';
-import Notification from '../../models/notification';
-import PollVote from '../../models/poll-vote';
-import PostReaction from '../../models/post-reaction';
-import PostWatching from '../../models/post-watching';
-import Post from '../../models/post';
+import Favorite from '../../../models/favorite';
+import Notification from '../../../models/notification';
+import PollVote from '../../../models/poll-vote';
+import PostReaction from '../../../models/post-reaction';
+import PostWatching from '../../../models/post-watching';
+import Post from '../../../models/post';
 
 export default async ({ data }) => Promise.all([
 	Favorite.remove({ postId: data._id }),
diff --git a/src/processor/db/index.ts b/src/queue/processors/db/index.ts
similarity index 100%
rename from src/processor/db/index.ts
rename to src/queue/processors/db/index.ts
diff --git a/src/processor/http/deliver-post.ts b/src/queue/processors/http/deliver-post.ts
similarity index 55%
rename from src/processor/http/deliver-post.ts
rename to src/queue/processors/http/deliver-post.ts
index 48ad4f95a..e743fc5f6 100644
--- a/src/processor/http/deliver-post.ts
+++ b/src/queue/processors/http/deliver-post.ts
@@ -1,9 +1,9 @@
-import Post from '../../models/post';
-import User, { IRemoteUser } from '../../models/user';
-import context from '../../remote/activitypub/renderer/context';
-import renderCreate from '../../remote/activitypub/renderer/create';
-import renderNote from '../../remote/activitypub/renderer/note';
-import request from '../../remote/request';
+import Post from '../../../models/post';
+import User, { IRemoteUser } from '../../../models/user';
+import context from '../../../remote/activitypub/renderer/context';
+import renderCreate from '../../../remote/activitypub/renderer/create';
+import renderNote from '../../../remote/activitypub/renderer/note';
+import request from '../../../remote/request';
 
 export default async ({ data }) => {
 	const promisedTo = User.findOne({ _id: data.toId }) as Promise<IRemoteUser>;
diff --git a/src/processor/http/follow.ts b/src/queue/processors/http/follow.ts
similarity index 74%
rename from src/processor/http/follow.ts
rename to src/queue/processors/http/follow.ts
index ed36fa18d..4cb72828e 100644
--- a/src/processor/http/follow.ts
+++ b/src/queue/processors/http/follow.ts
@@ -1,13 +1,13 @@
-import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../models/user';
-import Following from '../../models/following';
-import FollowingLog from '../../models/following-log';
-import FollowedLog from '../../models/followed-log';
-import event from '../../publishers/stream';
-import notify from '../../publishers/notify';
-import context from '../../remote/activitypub/renderer/context';
-import render from '../../remote/activitypub/renderer/follow';
-import request from '../../remote/request';
-import Logger from '../../utils/logger';
+import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user';
+import Following from '../../../models/following';
+import FollowingLog from '../../../models/following-log';
+import FollowedLog from '../../../models/followed-log';
+import event from '../../../publishers/stream';
+import notify from '../../../publishers/notify';
+import context from '../../../remote/activitypub/renderer/context';
+import render from '../../../remote/activitypub/renderer/follow';
+import request from '../../../remote/request';
+import Logger from '../../../utils/logger';
 
 export default async ({ data }) => {
 	const { followerId, followeeId } = await Following.findOne({ _id: data.following });
diff --git a/src/processor/http/index.ts b/src/queue/processors/http/index.ts
similarity index 100%
rename from src/processor/http/index.ts
rename to src/queue/processors/http/index.ts
diff --git a/src/queue/processors/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts
new file mode 100644
index 000000000..7b84400d5
--- /dev/null
+++ b/src/queue/processors/http/perform-activitypub.ts
@@ -0,0 +1,7 @@
+import User from '../../../models/user';
+import act from '../../../remote/activitypub/act';
+import Resolver from '../../../remote/activitypub/resolver';
+
+export default ({ data }) => User.findOne({ _id: data.actor })
+	.then(actor => act(new Resolver(), actor, data.outbox))
+	.then(Promise.all);
diff --git a/src/processor/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
similarity index 76%
rename from src/processor/http/process-inbox.ts
rename to src/queue/processors/http/process-inbox.ts
index f102f8d6b..de1dbd2f9 100644
--- a/src/processor/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -1,9 +1,9 @@
 import { verifySignature } from 'http-signature';
-import parseAcct from '../../acct/parse';
-import User, { IRemoteUser } from '../../models/user';
-import act from '../../remote/activitypub/act';
-import resolvePerson from '../../remote/activitypub/resolve-person';
-import Resolver from '../../remote/activitypub/resolver';
+import parseAcct from '../../../acct/parse';
+import User, { IRemoteUser } from '../../../models/user';
+import act from '../../../remote/activitypub/act';
+import resolvePerson from '../../../remote/activitypub/resolve-person';
+import Resolver from '../../../remote/activitypub/resolver';
 
 export default async ({ data }): Promise<void> => {
 	const keyIdLower = data.signature.keyId.toLowerCase();
diff --git a/src/processor/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts
similarity index 85%
rename from src/processor/http/report-github-failure.ts
rename to src/queue/processors/http/report-github-failure.ts
index 4f6f5ccee..21683ba3c 100644
--- a/src/processor/http/report-github-failure.ts
+++ b/src/queue/processors/http/report-github-failure.ts
@@ -1,6 +1,6 @@
 import * as request from 'request-promise-native';
-import User from '../../models/user';
-const createPost = require('../../server/api/endpoints/posts/create');
+import User from '../../../models/user';
+const createPost = require('../../../server/api/endpoints/posts/create');
 
 export default async ({ data }) => {
 	const asyncBot = User.findOne({ _id: data.userId });
diff --git a/src/processor/http/unfollow.ts b/src/queue/processors/http/unfollow.ts
similarity index 71%
rename from src/processor/http/unfollow.ts
rename to src/queue/processors/http/unfollow.ts
index fbfd7b342..801a3612a 100644
--- a/src/processor/http/unfollow.ts
+++ b/src/queue/processors/http/unfollow.ts
@@ -1,13 +1,13 @@
-import FollowedLog from '../../models/followed-log';
-import Following from '../../models/following';
-import FollowingLog from '../../models/following-log';
-import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../models/user';
-import stream from '../../publishers/stream';
-import renderFollow from '../../remote/activitypub/renderer/follow';
-import renderUndo from '../../remote/activitypub/renderer/undo';
-import context from '../../remote/activitypub/renderer/context';
-import request from '../../remote/request';
-import Logger from '../../utils/logger';
+import FollowedLog from '../../../models/followed-log';
+import Following from '../../../models/following';
+import FollowingLog from '../../../models/following-log';
+import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user';
+import stream from '../../../publishers/stream';
+import renderFollow from '../../../remote/activitypub/renderer/follow';
+import renderUndo from '../../../remote/activitypub/renderer/undo';
+import context from '../../../remote/activitypub/renderer/context';
+import request from '../../../remote/request';
+import Logger from '../../../utils/logger';
 
 export default async ({ data }) => {
 	const following = await Following.findOne({ _id: data.id });
diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index 23fa41df8..222a257e1 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -3,7 +3,7 @@ import parseAcct from '../../../acct/parse';
 import Following, { IFollowing } from '../../../models/following';
 import User from '../../../models/user';
 import config from '../../../config';
-import queue from '../../../queue';
+import { createHttp } from '../../../queue';
 import context from '../renderer/context';
 import renderAccept from '../renderer/accept';
 import request from '../../request';
@@ -44,7 +44,7 @@ export default async (resolver: Resolver, actor, activity, distribute) => {
 		followerId: actor._id,
 		followeeId: followee._id
 	}).then(following => new Promise((resolve, reject) => {
-		queue.create('http', {
+		createHttp({
 			type: 'follow',
 			following: following._id
 		}).save(error => {
diff --git a/src/remote/activitypub/act/undo/unfollow.ts b/src/remote/activitypub/act/undo/unfollow.ts
index c17e06e8a..4f15d9a3e 100644
--- a/src/remote/activitypub/act/undo/unfollow.ts
+++ b/src/remote/activitypub/act/undo/unfollow.ts
@@ -1,7 +1,7 @@
-import queue from '../../../../queue';
+import { createHttp } from '../../../../queue';
 
 export default ({ $id }) => new Promise((resolve, reject) => {
-	queue.create('http', { type: 'unfollow', id: $id }).save(error => {
+	createHttp({ type: 'unfollow', id: $id }).save(error => {
 		if (error) {
 			reject(error);
 		} else {
diff --git a/src/remote/activitypub/delete/post.ts b/src/remote/activitypub/delete/post.ts
index f6c816647..59ae8c2b9 100644
--- a/src/remote/activitypub/delete/post.ts
+++ b/src/remote/activitypub/delete/post.ts
@@ -1,10 +1,10 @@
 import Post from '../../../models/post';
-import queue from '../../../queue';
+import { createDb } from '../../../queue';
 
 export default async ({ $id }) => {
 	const promisedDeletion = Post.findOneAndDelete({ _id: $id });
 
-	await new Promise((resolve, reject) => queue.create('db', {
+	await new Promise((resolve, reject) => createDb({
 		type: 'deletePostDependents',
 		id: $id
 	}).delay(65536).save(error => error ? reject(error) : resolve()));
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 59be65908..2cf3ad32d 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -1,7 +1,7 @@
 import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
 import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
-import queue from '../../queue';
+import { createHttp } from '../../queue';
 import webFinger from '../webfinger';
 import create from './create';
 import Resolver from './resolver';
@@ -69,7 +69,7 @@ export default async (value, verifier?: string) => {
 		},
 	});
 
-	queue.create('http', {
+	createHttp({
 		type: 'performActivityPub',
 		actor: user._id,
 		outbox
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 5de843385..0907823b2 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -1,7 +1,7 @@
 import * as bodyParser from 'body-parser';
 import * as express from 'express';
 import { parseRequest } from 'http-signature';
-import queue from '../../queue';
+import { createHttp } from '../../queue';
 
 const app = express();
 
@@ -22,7 +22,7 @@ app.post('/@:user/inbox', bodyParser.json({
 		return res.sendStatus(401);
 	}
 
-	queue.create('http', {
+	createHttp({
 		type: 'processInbox',
 		inbox: req.body,
 		signature,
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index e56859521..9ccbe2017 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../../../models/user';
 import Following from '../../../../models/following';
-import queue from '../../../../queue';
+import { createHttp } from '../../../../queue';
 
 /**
  * Follow a user
@@ -56,7 +56,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		followeeId: followee._id
 	});
 
-	queue.create('http', { type: 'follow', following: _id }).save();
+	createHttp({ type: 'follow', following: _id }).save();
 
 	// Send response
 	res();
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index bf21bf0cb..0684b8750 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../../../models/user';
 import Following from '../../../../models/following';
-import queue from '../../../../queue';
+import { createHttp } from '../../../../queue';
 
 /**
  * Unfollow a user
@@ -49,7 +49,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('already not following');
 	}
 
-	queue.create('http', {
+	createHttp({
 		type: 'unfollow',
 		id: exist._id
 	}).save(error => {
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index 4fd59c2a9..5fc4a92f5 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -3,7 +3,7 @@ import * as express from 'express';
 //const crypto = require('crypto');
 import User from '../../../models/user';
 import config from '../../../config';
-import queue from '../../../queue';
+import { createHttp } from '../../../queue';
 
 module.exports = async (app: express.Application) => {
 	if (config.github_bot == null) return;
@@ -42,7 +42,7 @@ module.exports = async (app: express.Application) => {
 				const commit = event.commit;
 				const parent = commit.parents[0];
 
-				queue.create('http', {
+				createHttp({
 					type: 'gitHubFailureReport',
 					userId: bot._id,
 					parentUrl: parent.url,

From e330ac1934516807757afe2d2760fa21b27006e6 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 01:04:44 +0900
Subject: [PATCH 05/15] Let unhandled rejection handler handle rejections in
 jobs

---
 .../processors/db/delete-post-dependents.ts   |  4 +-
 src/queue/processors/db/index.ts              |  2 +-
 src/queue/processors/http/deliver-post.ts     | 28 ++++---
 src/queue/processors/http/follow.ts           | 79 +++++++++----------
 src/queue/processors/http/index.ts            |  2 +-
 .../processors/http/perform-activitypub.ts    |  5 +-
 src/queue/processors/http/process-inbox.ts    | 55 +++++++------
 .../processors/http/report-github-failure.ts  | 39 +++++----
 src/queue/processors/http/unfollow.ts         | 31 +++++---
 9 files changed, 134 insertions(+), 111 deletions(-)

diff --git a/src/queue/processors/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts
index 6de21eb05..fb6617e95 100644
--- a/src/queue/processors/db/delete-post-dependents.ts
+++ b/src/queue/processors/db/delete-post-dependents.ts
@@ -5,7 +5,7 @@ import PostReaction from '../../../models/post-reaction';
 import PostWatching from '../../../models/post-watching';
 import Post from '../../../models/post';
 
-export default async ({ data }) => Promise.all([
+export default ({ data }, done) => Promise.all([
 	Favorite.remove({ postId: data._id }),
 	Notification.remove({ postId: data._id }),
 	PollVote.remove({ postId: data._id }),
@@ -19,4 +19,4 @@ export default async ({ data }) => Promise.all([
 		}),
 		Post.remove({ repostId: data._id })
 	]))
-]);
+]).then(() => done(), done);
diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts
index 75838c099..468ec442a 100644
--- a/src/queue/processors/db/index.ts
+++ b/src/queue/processors/db/index.ts
@@ -4,4 +4,4 @@ const handlers = {
   deletePostDependents
 };
 
-export default (job, done) => handlers[job.data.type](job).then(() => done(), done);
+export default (job, done) => handlers[job.data.type](job, done);
diff --git a/src/queue/processors/http/deliver-post.ts b/src/queue/processors/http/deliver-post.ts
index e743fc5f6..8107c8bf7 100644
--- a/src/queue/processors/http/deliver-post.ts
+++ b/src/queue/processors/http/deliver-post.ts
@@ -5,17 +5,23 @@ import renderCreate from '../../../remote/activitypub/renderer/create';
 import renderNote from '../../../remote/activitypub/renderer/note';
 import request from '../../../remote/request';
 
-export default async ({ data }) => {
-	const promisedTo = User.findOne({ _id: data.toId }) as Promise<IRemoteUser>;
-	const [from, post] = await Promise.all([
-		User.findOne({ _id: data.fromId }),
-		Post.findOne({ _id: data.postId })
-	]);
-	const note = await renderNote(from, post);
-	const to = await promisedTo;
-	const create = renderCreate(note);
+export default async ({ data }, done) => {
+	try {
+		const promisedTo = User.findOne({ _id: data.toId }) as Promise<IRemoteUser>;
+		const [from, post] = await Promise.all([
+			User.findOne({ _id: data.fromId }),
+			Post.findOne({ _id: data.postId })
+		]);
+		const note = await renderNote(from, post);
+		const to = await promisedTo;
+		const create = renderCreate(note);
 
-	create['@context'] = context;
+		create['@context'] = context;
 
-	return request(from, to.account.inbox, create);
+		await request(from, to.account.inbox, create);
+	} catch (error) {
+		done(error);
+	}
+
+	done();
 };
diff --git a/src/queue/processors/http/follow.ts b/src/queue/processors/http/follow.ts
index 4cb72828e..ba1cc3118 100644
--- a/src/queue/processors/http/follow.ts
+++ b/src/queue/processors/http/follow.ts
@@ -7,10 +7,8 @@ import notify from '../../../publishers/notify';
 import context from '../../../remote/activitypub/renderer/context';
 import render from '../../../remote/activitypub/renderer/follow';
 import request from '../../../remote/request';
-import Logger from '../../../utils/logger';
 
-export default async ({ data }) => {
-	const { followerId, followeeId } = await Following.findOne({ _id: data.following });
+export default ({ data }, done) => Following.findOne({ _id: data.following }).then(async ({ followerId, followeeId }) => {
 	const [follower, followee] = await Promise.all([
 		User.findOne({ _id: followerId }),
 		User.findOne({ _id: followeeId })
@@ -23,47 +21,46 @@ export default async ({ data }) => {
 		await request(follower, followee.account.inbox, rendered);
 	}
 
-	try {
-		await Promise.all([
-			// Increment following count
-			User.update(followerId, {
-				$inc: {
-					followingCount: 1
-				}
-			}),
+	return [follower, followee];
+}).then(([follower, followee]) => Promise.all([
+	// Increment following count
+	User.update(follower._id, {
+		$inc: {
+			followingCount: 1
+		}
+	}),
 
-			FollowingLog.insert({
-				createdAt: data.following.createdAt,
-				userId: followerId,
-				count: follower.followingCount + 1
-			}),
+	FollowingLog.insert({
+		createdAt: data.following.createdAt,
+		userId: follower._id,
+		count: follower.followingCount + 1
+	}),
 
-			// Increment followers count
-			User.update({ _id: followeeId }, {
-				$inc: {
-					followersCount: 1
-				}
-			}),
+	// Increment followers count
+	User.update({ _id: followee._id }, {
+		$inc: {
+			followersCount: 1
+		}
+	}),
 
-			FollowedLog.insert({
-				createdAt: data.following.createdAt,
-				userId: followerId,
-				count: followee.followersCount + 1
-			}),
+	FollowedLog.insert({
+		createdAt: data.following.createdAt,
+		userId: follower._id,
+		count: followee.followersCount + 1
+	}),
 
-			// Publish follow event
-			isLocalUser(follower) && packUser(followee, follower)
-				.then(packed => event(follower._id, 'follow', packed)),
+	// Publish follow event
+	isLocalUser(follower) && packUser(followee, follower)
+		.then(packed => event(follower._id, 'follow', packed)),
 
-			isLocalUser(followee) && Promise.all([
-				packUser(follower, followee)
-					.then(packed => event(followee._id, 'followed', packed)),
+	isLocalUser(followee) && Promise.all([
+		packUser(follower, followee)
+			.then(packed => event(followee._id, 'followed', packed)),
 
-				// Notify
-				isLocalUser(followee) && notify(followeeId, followerId, 'follow')
-			])
-		]);
-	} catch (error) {
-		Logger.error(error.toString());
-	}
-};
+		// Notify
+		isLocalUser(followee) && notify(followee._id, follower._id, 'follow')
+	])
+]).then(() => done(), error => {
+	done();
+	throw error;
+}), done);
diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts
index 8f9aa717c..0ea79305c 100644
--- a/src/queue/processors/http/index.ts
+++ b/src/queue/processors/http/index.ts
@@ -14,4 +14,4 @@ const handlers = {
   unfollow
 };
 
-export default (job, done) => handlers[job.data.type](job).then(() => done(), done);
+export default (job, done) => handlers[job.data.type](job, done);
diff --git a/src/queue/processors/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts
index 7b84400d5..ae70c0f0b 100644
--- a/src/queue/processors/http/perform-activitypub.ts
+++ b/src/queue/processors/http/perform-activitypub.ts
@@ -2,6 +2,7 @@ import User from '../../../models/user';
 import act from '../../../remote/activitypub/act';
 import Resolver from '../../../remote/activitypub/resolver';
 
-export default ({ data }) => User.findOne({ _id: data.actor })
+export default ({ data }, done) => User.findOne({ _id: data.actor })
 	.then(actor => act(new Resolver(), actor, data.outbox))
-	.then(Promise.all);
+	.then(Promise.all)
+	.then(() => done(), done);
diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index de1dbd2f9..88fbb9737 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -5,35 +5,40 @@ import act from '../../../remote/activitypub/act';
 import resolvePerson from '../../../remote/activitypub/resolve-person';
 import Resolver from '../../../remote/activitypub/resolver';
 
-export default async ({ data }): Promise<void> => {
-	const keyIdLower = data.signature.keyId.toLowerCase();
-	let user;
+export default async ({ data }, done) => {
+	try {
+		const keyIdLower = data.signature.keyId.toLowerCase();
+		let user;
 
-	if (keyIdLower.startsWith('acct:')) {
-		const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
-		if (host === null) {
-			throw 'request was made by local user';
+		if (keyIdLower.startsWith('acct:')) {
+			const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
+			if (host === null) {
+				done();
+				return;
+			}
+
+			user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser;
+		} else {
+			user = await User.findOne({
+				host: { $ne: null },
+				'account.publicKey.id': data.signature.keyId
+			}) as IRemoteUser;
+
+			if (user === null) {
+				user = await resolvePerson(data.signature.keyId);
+			}
 		}
 
-		user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser;
-	} else {
-		user = await User.findOne({
-			host: { $ne: null },
-			'account.publicKey.id': data.signature.keyId
-		}) as IRemoteUser;
-
-		if (user === null) {
-			user = await resolvePerson(data.signature.keyId);
+		if (user === null || !verifySignature(data.signature, user.account.publicKey.publicKeyPem)) {
+			done();
+			return;
 		}
+
+		await Promise.all(await act(new Resolver(), user, data.inbox, true));
+	} catch (error) {
+		done(error);
+		return;
 	}
 
-	if (user === null) {
-		throw 'failed to resolve user';
-	}
-
-	if (!verifySignature(data.signature, user.account.publicKey.publicKeyPem)) {
-		throw 'signature verification failed';
-	}
-
-	await Promise.all(await act(new Resolver(), user, data.inbox, true));
+	done();
 };
diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts
index 21683ba3c..af9659bda 100644
--- a/src/queue/processors/http/report-github-failure.ts
+++ b/src/queue/processors/http/report-github-failure.ts
@@ -2,23 +2,30 @@ import * as request from 'request-promise-native';
 import User from '../../../models/user';
 const createPost = require('../../../server/api/endpoints/posts/create');
 
-export default async ({ data }) => {
-	const asyncBot = User.findOne({ _id: data.userId });
+export default async ({ data }, done) => {
+	try {
+		const asyncBot = User.findOne({ _id: data.userId });
 
-	// Fetch parent status
-	const parentStatuses = await request({
-		url: `${data.parentUrl}/statuses`,
-		headers: {
-			'User-Agent': 'misskey'
-		},
-		json: true
-	});
+		// Fetch parent status
+		const parentStatuses = await request({
+			url: `${data.parentUrl}/statuses`,
+			headers: {
+				'User-Agent': 'misskey'
+			},
+			json: true
+		});
 
-	const parentState = parentStatuses[0].state;
-	const stillFailed = parentState == 'failure' || parentState == 'error';
-	const text = stillFailed ?
-		`**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` :
-		`**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`;
+		const parentState = parentStatuses[0].state;
+		const stillFailed = parentState == 'failure' || parentState == 'error';
+		const text = stillFailed ?
+			`**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` :
+			`**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`;
 
-	createPost({ text }, await asyncBot);
+		createPost({ text }, await asyncBot);
+	} catch (error) {
+		done(error);
+		return;
+	}
+
+	done();
 };
diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts
index 801a3612a..dc50e946c 100644
--- a/src/queue/processors/http/unfollow.ts
+++ b/src/queue/processors/http/unfollow.ts
@@ -7,24 +7,31 @@ import renderFollow from '../../../remote/activitypub/renderer/follow';
 import renderUndo from '../../../remote/activitypub/renderer/undo';
 import context from '../../../remote/activitypub/renderer/context';
 import request from '../../../remote/request';
-import Logger from '../../../utils/logger';
 
-export default async ({ data }) => {
+export default async ({ data }, done) => {
 	const following = await Following.findOne({ _id: data.id });
 	if (following === null) {
+		done();
 		return;
 	}
 
-	const [follower, followee] = await Promise.all([
-		User.findOne({ _id: following.followerId }),
-		User.findOne({ _id: following.followeeId })
-	]);
+	let follower, followee;
 
-	if (isLocalUser(follower) && isRemoteUser(followee)) {
-		const undo = renderUndo(renderFollow(follower, followee));
-		undo['@context'] = context;
+	try {
+		[follower, followee] = await Promise.all([
+			User.findOne({ _id: following.followerId }),
+			User.findOne({ _id: following.followeeId })
+		]);
 
-		await request(follower, followee.account.inbox, undo);
+		if (isLocalUser(follower) && isRemoteUser(followee)) {
+			const undo = renderUndo(renderFollow(follower, followee));
+			undo['@context'] = context;
+
+			await request(follower, followee.account.inbox, undo);
+		}
+	} catch (error) {
+		done(error);
+		return;
 	}
 
 	try {
@@ -57,7 +64,7 @@ export default async ({ data }) => {
 
 		// Publish follow event
 		stream(follower._id, 'unfollow', promisedPackedUser);
-	} catch (error) {
-		Logger.error(error.toString());
+	} finally {
+		done();
 	}
 };

From a5715ecc1b73d3e3a950c392fa3a466dee810248 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 01:07:55 +0900
Subject: [PATCH 06/15] Do not declare two variables in a statement

---
 src/queue/processors/http/unfollow.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts
index dc50e946c..d62eb280d 100644
--- a/src/queue/processors/http/unfollow.ts
+++ b/src/queue/processors/http/unfollow.ts
@@ -15,7 +15,8 @@ export default async ({ data }, done) => {
 		return;
 	}
 
-	let follower, followee;
+	let follower;
+	let followee;
 
 	try {
 		[follower, followee] = await Promise.all([

From 168b0730b46fd283b900b553dd2eede2aa4c7dac Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 01:24:39 +0900
Subject: [PATCH 07/15] Implement Mention object

---
 src/post/create.ts                         | 14 ++------------
 src/queue/processors/http/process-inbox.ts |  2 +-
 src/remote/activitypub/create.ts           | 12 +++++++++++-
 src/remote/activitypub/resolve-person.ts   |  5 ++---
 src/remote/resolve-user.ts                 |  3 ++-
 src/server/api/endpoints/posts/create.ts   | 10 +++++++---
 6 files changed, 25 insertions(+), 21 deletions(-)

diff --git a/src/post/create.ts b/src/post/create.ts
index ecea37382..4ad1503e0 100644
--- a/src/post/create.ts
+++ b/src/post/create.ts
@@ -1,8 +1,6 @@
-import parseAcct from '../acct/parse';
 import Post from '../models/post';
-import User from '../models/user';
 
-export default async (post, reply, repost, atMentions) => {
+export default async (post, reply, repost, mentions) => {
 	post.mentions = [];
 
 	function addMention(mentionee) {
@@ -36,15 +34,7 @@ export default async (post, reply, repost, atMentions) => {
 		post._repost = null;
 	}
 
-	await Promise.all(atMentions.map(async mention => {
-		// Fetch mentioned user
-		// SELECT _id
-		const { _id } = await User
-			.findOne(parseAcct(mention), { _id: true });
-
-		// Add mention
-		addMention(_id);
-	}));
+	await Promise.all(mentions.map(({ _id }) => addMention(_id)));
 
 	return Post.insert(post);
 };
diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index 88fbb9737..7eeaa19f8 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -25,7 +25,7 @@ export default async ({ data }, done) => {
 			}) as IRemoteUser;
 
 			if (user === null) {
-				user = await resolvePerson(data.signature.keyId);
+				user = await resolvePerson(new Resolver(), data.signature.keyId);
 			}
 		}
 
diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 97c72860f..710d56fd3 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -7,6 +7,7 @@ import { IRemoteUser } from '../../models/user';
 import uploadFromUrl from '../../drive/upload-from-url';
 import createPost from '../../post/create';
 import distributePost from '../../post/distribute';
+import resolvePerson from './resolve-person';
 import Resolver from './resolver';
 const createDOMPurify = require('dompurify');
 
@@ -53,6 +54,15 @@ class Creator {
 				.map(({ object }) => object.$id);
 
 		const { window } = new JSDOM(note.content);
+		const mentions = [];
+
+		for (const { href, type } of note.tags) {
+			switch (type) {
+			case 'Mention':
+				mentions.push(resolvePerson(resolver, href));
+				break;
+			}
+		}
 
 		const inserted = await createPost({
 			channelId: undefined,
@@ -69,7 +79,7 @@ class Creator {
 			viaMobile: false,
 			geo: undefined,
 			uri: note.id
-		}, null, null, []);
+		}, null, null, await Promise.all(mentions));
 
 		const promises = [];
 
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 2cf3ad32d..7ed01e322 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -4,14 +4,13 @@ import User, { validateUsername, isValidName, isValidDescription } from '../../m
 import { createHttp } from '../../queue';
 import webFinger from '../webfinger';
 import create from './create';
-import Resolver from './resolver';
 
 async function isCollection(collection) {
 	return ['Collection', 'OrderedCollection'].includes(collection.type);
 }
 
-export default async (value, verifier?: string) => {
-	const { resolver, object } = await new Resolver().resolveOne(value);
+export default async (parentResolver, value, verifier?: string) => {
+	const { resolver, object } = parentResolver.resolveOne(value);
 
 	if (
 		object === null ||
diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
index 48219e8cb..097ed6673 100644
--- a/src/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -1,6 +1,7 @@
 import { toUnicode, toASCII } from 'punycode';
 import User from '../models/user';
 import resolvePerson from './activitypub/resolve-person';
+import Resolver from './activitypub/resolver';
 import webFinger from './webfinger';
 
 export default async (username, host, option) => {
@@ -19,7 +20,7 @@ export default async (username, host, option) => {
 			throw new Error();
 		}
 
-		user = await resolvePerson(self.href, acctLower);
+		user = await resolvePerson(new Resolver(), self.href, acctLower);
 	}
 
 	return user;
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 03af7ee76..47897626f 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -3,12 +3,13 @@
  */
 import $ from 'cafy';
 import deepEqual = require('deep-equal');
+import parseAcct from '../../../../acct/parse';
 import renderAcct from '../../../../acct/render';
 import config from '../../../../config';
 import html from '../../../../text/html';
 import parse from '../../../../text/parse';
 import Post, { IPost, isValidText, isValidCw } from '../../../../models/post';
-import { ILocalUser } from '../../../../models/user';
+import User, { ILocalUser } from '../../../../models/user';
 import Channel, { IChannel } from '../../../../models/channel';
 import DriveFile from '../../../../models/drive-file';
 import create from '../../../../post/create';
@@ -267,7 +268,10 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 			.filter(t => t.type == 'mention')
 			.map(renderAcct)
 			// Drop dupulicates
-			.filter((v, i, s) => s.indexOf(v) == i);
+			.filter((v, i, s) => s.indexOf(v) == i)
+			// Fetch mentioned user
+			// SELECT _id
+			.map(mention => User.findOne(parseAcct(mention), { _id: true }));
 	}
 
 	// 投稿を作成
@@ -286,7 +290,7 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 		viaMobile: viaMobile,
 		visibility,
 		geo
-	}, reply, repost, atMentions);
+	}, reply, repost, await Promise.all(atMentions));
 
 	const postObj = await distribute(user, post.mentions, post);
 

From f44a7389e2b67e524c953843fcb4e007349c76c2 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 01:29:56 +0900
Subject: [PATCH 08/15] Implement Hashtag object

---
 src/remote/activitypub/create.ts | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 710d56fd3..31e9dba86 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -55,9 +55,16 @@ class Creator {
 
 		const { window } = new JSDOM(note.content);
 		const mentions = [];
+		const tags = [];
 
-		for (const { href, type } of note.tags) {
+		for (const { href, name, type } of note.tags) {
 			switch (type) {
+			case 'Hashtag':
+				if (name.startsWith('#')) {
+					tags.push(name.slice(1));
+				}
+				break;
+
 			case 'Mention':
 				mentions.push(resolvePerson(resolver, href));
 				break;
@@ -78,7 +85,8 @@ class Creator {
 			appId: null,
 			viaMobile: false,
 			geo: undefined,
-			uri: note.id
+			uri: note.id,
+			tags
 		}, null, null, await Promise.all(mentions));
 
 		const promises = [];

From ced6b9a76f71e95a376dc4cb37b6b5617a0321d9 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 01:57:18 +0900
Subject: [PATCH 09/15] Handle inReplyTo property

---
 src/remote/activitypub/create.ts | 25 +++++++++++++++++--------
 1 file changed, 17 insertions(+), 8 deletions(-)

diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 31e9dba86..3bc0c66f3 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -48,11 +48,6 @@ class Creator {
 			throw new Error();
 		}
 
-		const mediaIds = 'attachment' in note &&
-			(await Promise.all(await this.create(resolver, note.attachment)))
-				.filter(media => media !== null && media.object.$ref === 'driveFiles.files')
-				.map(({ object }) => object.$id);
-
 		const { window } = new JSDOM(note.content);
 		const mentions = [];
 		const tags = [];
@@ -71,13 +66,27 @@ class Creator {
 			}
 		}
 
+		const [mediaIds, reply] = await Promise.all([
+			'attachment' in note && this.create(resolver, note.attachment)
+				.then(collection => Promise.all(collection))
+				.then(collection => collection
+					.filter(media => media !== null && media.object.$ref === 'driveFiles.files')
+					.map(({ object }: IResult) => object.$id)),
+
+			'inReplyTo' in note && this.create(resolver, note.inReplyTo)
+				.then(collection => Promise.all(collection.map(promise => promise.then(result => {
+					if (result !== null && result.object.$ref === 'posts') {
+						throw result.object;
+					}
+				}, () => { }))))
+				.then(() => null, ({ $id }) => Post.findOne({ _id: $id }))
+		]);
+
 		const inserted = await createPost({
 			channelId: undefined,
 			index: undefined,
 			createdAt: new Date(note.published),
 			mediaIds,
-			replyId: undefined,
-			repostId: undefined,
 			poll: undefined,
 			text: window.document.body.textContent,
 			textHtml: note.content && createDOMPurify(window).sanitize(note.content),
@@ -87,7 +96,7 @@ class Creator {
 			geo: undefined,
 			uri: note.id,
 			tags
-		}, null, null, await Promise.all(mentions));
+		}, reply, null, await Promise.all(mentions));
 
 		const promises = [];
 

From 068c0b4cceaa9461cc87a6d44abebdfc429d1861 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 21:16:14 +0900
Subject: [PATCH 10/15] Add a missing await expression

---
 src/remote/activitypub/resolve-person.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 7ed01e322..a7c0020dd 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -10,7 +10,7 @@ async function isCollection(collection) {
 }
 
 export default async (parentResolver, value, verifier?: string) => {
-	const { resolver, object } = parentResolver.resolveOne(value);
+	const { resolver, object } = await parentResolver.resolveOne(value);
 
 	if (
 		object === null ||

From f0e8e6392b5ef99488ea0bbecbf9029e30ef0cfa Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 6 Apr 2018 01:36:34 +0900
Subject: [PATCH 11/15] Allow name property of user to be null

---
 src/client/app/ch/tags/channel.tag            | 10 ++++++--
 .../common/scripts/compose-notification.ts    | 13 ++++++-----
 .../common/views/components/autocomplete.vue  |  4 +++-
 .../app/common/views/components/messaging.vue |  6 +++--
 .../views/components/welcome-timeline.vue     |  4 +++-
 .../views/components/followers-window.vue     | 11 +++++++--
 .../views/components/following-window.vue     | 11 +++++++--
 .../views/components/friends-maker.vue        |  4 +++-
 .../components/messaging-room-window.vue      |  6 ++++-
 .../views/components/notifications.vue        | 16 +++++++------
 .../views/components/post-detail.sub.vue      |  6 ++++-
 .../desktop/views/components/post-detail.vue  | 11 +++++++--
 .../desktop/views/components/post-preview.vue |  6 ++++-
 .../views/components/posts.post.sub.vue       |  6 ++++-
 .../desktop/views/components/posts.post.vue   |  6 ++++-
 .../views/components/settings.mute.vue        |  6 +++--
 .../views/components/settings.profile.vue     |  4 ++--
 .../desktop/views/components/ui.header.vue    |  9 +++++++-
 .../views/components/users-list.item.vue      |  6 ++++-
 .../desktop/views/pages/messaging-room.vue    |  3 ++-
 .../pages/user/user.followers-you-know.vue    |  6 +++--
 .../desktop/views/pages/user/user.header.vue  |  6 ++++-
 .../app/desktop/views/pages/user/user.vue     |  3 ++-
 .../views/widgets/channel.channel.post.vue    |  6 ++++-
 .../app/desktop/views/widgets/profile.vue     |  9 +++++++-
 .../app/desktop/views/widgets/users.vue       |  4 +++-
 .../views/components/notification-preview.vue | 23 +++++++++++++------
 .../mobile/views/components/notification.vue  | 15 ++++++++----
 .../app/mobile/views/components/post-card.vue |  6 ++++-
 .../views/components/post-detail.sub.vue      |  6 ++++-
 .../mobile/views/components/post-detail.vue   | 11 +++++++--
 .../mobile/views/components/post-preview.vue  |  6 ++++-
 .../app/mobile/views/components/post.sub.vue  |  6 ++++-
 .../app/mobile/views/components/post.vue      | 11 +++++++--
 .../app/mobile/views/components/ui.header.vue |  8 ++++++-
 .../app/mobile/views/components/ui.nav.vue    |  8 ++++++-
 .../app/mobile/views/components/user-card.vue |  6 ++++-
 .../mobile/views/components/user-preview.vue  |  6 ++++-
 .../app/mobile/views/pages/followers.vue      | 10 ++++++--
 .../app/mobile/views/pages/following.vue      | 10 ++++++--
 .../app/mobile/views/pages/messaging-room.vue | 10 ++++++--
 .../mobile/views/pages/profile-setting.vue    |  4 ++--
 .../app/mobile/views/pages/settings.vue       |  8 ++++++-
 src/client/app/mobile/views/pages/user.vue    | 11 +++++----
 .../pages/user/home.followers-you-know.vue    |  8 ++++++-
 .../app/mobile/views/widgets/profile.vue      | 10 +++++++-
 src/models/user.ts                            |  6 ++---
 src/othello/ai/back.ts                        | 17 +++++++-------
 src/renderers/get-notification-summary.ts     | 15 ++++++------
 src/renderers/get-user-summary.ts             |  3 ++-
 src/server/api/bot/core.ts                    |  7 +++---
 src/server/api/bot/interfaces/line.ts         |  5 ++--
 52 files changed, 311 insertions(+), 107 deletions(-)

diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag
index 1ebc3cceb..4856728de 100644
--- a/src/client/app/ch/tags/channel.tag
+++ b/src/client/app/ch/tags/channel.tag
@@ -165,7 +165,7 @@
 <mk-channel-post>
 	<header>
 		<a class="index" @click="reply">{ post.index }:</a>
-		<a class="name" href={ _URL_ + '/@' + acct }><b>{ post.user.name }</b></a>
+		<a class="name" href={ _URL_ + '/@' + acct }><b>{ getUserName(post.user) }</b></a>
 		<mk-time time={ post.createdAt }/>
 		<mk-time time={ post.createdAt } mode="detail"/>
 		<span>ID:<i>{ acct }</i></span>
@@ -230,10 +230,12 @@
 	</style>
 	<script lang="typescript">
 		import getAcct from '../../../../acct/render';
+		import getUserName from '../../../../renderers/get-user-name';
 
 		this.post = this.opts.post;
 		this.form = this.opts.form;
 		this.acct = getAcct(this.post.user);
+		this.name = getUserName(this.post.user);
 
 		this.reply = () => {
 			this.form.update({
@@ -244,7 +246,7 @@
 </mk-channel-post>
 
 <mk-channel-form>
-	<p v-if="reply"><b>&gt;&gt;{ reply.index }</b> ({ reply.user.name }): <a @click="clearReply">[x]</a></p>
+	<p v-if="reply"><b>&gt;&gt;{ reply.index }</b> ({ getUserName(reply.user) }): <a @click="clearReply">[x]</a></p>
 	<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea>
 	<div class="actions">
 		<button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button>
@@ -286,6 +288,8 @@
 
 	</style>
 	<script lang="typescript">
+		import getUserName from '../../../../renderers/get-user-name';
+
 		this.mixin('api');
 
 		this.channel = this.opts.channel;
@@ -373,6 +377,8 @@
 				}
 			});
 		};
+
+		this.getUserName = getUserName;
 	</script>
 </mk-channel-form>
 
diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
index ebc15952f..e99d50296 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/app/common/scripts/compose-notification.ts
@@ -1,5 +1,6 @@
 import getPostSummary from '../../../../renderers/get-post-summary';
 import getReactionEmoji from '../../../../renderers/get-reaction-emoji';
+import getUserName from '../../../../renderers/get-user-name';
 
 type Notification = {
 	title: string;
@@ -21,35 +22,35 @@ export default function(type, data): Notification {
 
 		case 'mention':
 			return {
-				title: `${data.user.name}さんから:`,
+				title: `${getUserName(data.user)}さんから:`,
 				body: getPostSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'reply':
 			return {
-				title: `${data.user.name}さんから返信:`,
+				title: `${getUserName(data.user)}さんから返信:`,
 				body: getPostSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'quote':
 			return {
-				title: `${data.user.name}さんが引用:`,
+				title: `${getUserName(data.user)}さんが引用:`,
 				body: getPostSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'reaction':
 			return {
-				title: `${data.user.name}: ${getReactionEmoji(data.reaction)}:`,
+				title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`,
 				body: getPostSummary(data.post),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'unread_messaging_message':
 			return {
-				title: `${data.user.name}さんからメッセージ:`,
+				title: `${getUserName(data.user)}さんからメッセージ:`,
 				body: data.text, // TODO: getMessagingMessageSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
@@ -57,7 +58,7 @@ export default function(type, data): Notification {
 		case 'othello_invited':
 			return {
 				title: '対局への招待があります',
-				body: `${data.parent.name}さんから`,
+				body: `${getUserName(data.parent)}さんから`,
 				icon: data.parent.avatarUrl + '?thumbnail&size=64'
 			};
 
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index 38eaf8650..8837fde6b 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -3,7 +3,7 @@
 	<ol class="users" ref="suggests" v-if="users.length > 0">
 		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
 			<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
-			<span class="name">{{ user.name }}</span>
+			<span class="name">{{ getUserName(user) }}</span>
 			<span class="username">@{{ getAcct(user) }}</span>
 		</li>
 	</ol>
@@ -22,6 +22,7 @@ import Vue from 'vue';
 import * as emojilib from 'emojilib';
 import contains from '../../../common/scripts/contains';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 const lib = Object.entries(emojilib.lib).filter((x: any) => {
 	return x[1].category != 'flags';
@@ -107,6 +108,7 @@ export default Vue.extend({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		exec() {
 			this.select = -1;
 			if (this.$refs.suggests) {
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 4ab3e46e8..9b1449daa 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -14,7 +14,7 @@
 					tabindex="-1"
 				>
 					<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
-					<span class="name">{{ user.name }}</span>
+					<span class="name">{{ getUserName(user) }}</span>
 					<span class="username">@{{ getAcct(user) }}</span>
 				</li>
 			</ol>
@@ -33,7 +33,7 @@
 				<div>
 					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/>
 					<header>
-						<span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span>
+						<span class="name">{{ getUserName(isMe(message) ? message.recipient : message.user) }}</span>
 						<span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span>
 						<mk-time :time="message.createdAt"/>
 					</header>
@@ -52,6 +52,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: {
@@ -94,6 +95,7 @@ export default Vue.extend({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		isMe(message) {
 			return message.userId == (this as any).os.i.id;
 		},
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index ef0c7beaa..bfb4b2bbe 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -6,7 +6,7 @@
 		</router-link>
 		<div class="body">
 			<header>
-				<router-link class="name" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">{{ post.user.name }}</router-link>
+				<router-link class="name" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">{{ getUserName(post.user) }}</router-link>
 				<span class="username">@{{ getAcct(post.user) }}</span>
 				<div class="info">
 					<router-link class="created-at" :to="`/@${getAcct(post.user)}/${post.id}`">
@@ -25,6 +25,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -38,6 +39,7 @@ export default Vue.extend({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		fetch(cb?) {
 			this.fetching = true;
 			(this as any).api('posts', {
diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
index 623971fa3..d37ca745a 100644
--- a/src/client/app/desktop/views/components/followers-window.vue
+++ b/src/client/app/desktop/views/components/followers-window.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロワー
 	</span>
 	<mk-followers :user="user"/>
 </mk-window>
@@ -9,8 +9,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getUserName from '../../../../../renderers/get-user-name';
+
 export default Vue.extend({
-	props: ['user']
+	props: ['user'],
+	computed {
+		name() {
+			return getUserName(this.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
index 612847b38..cbd8ec5f9 100644
--- a/src/client/app/desktop/views/components/following-window.vue
+++ b/src/client/app/desktop/views/components/following-window.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロー
 	</span>
 	<mk-following :user="user"/>
 </mk-window>
@@ -9,8 +9,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getUserName from '../../../../../renderers/get-user-name';
+
 export default Vue.extend({
-	props: ['user']
+	props: ['user'],
+	computed: {
+		name() {
+			return getUserName(this.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index 351e9e1c5..acc4542d9 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -7,7 +7,7 @@
 				<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ user.name }}</router-link>
+				<router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ getUserName(user) }}</router-link>
 				<p class="username">@{{ getAcct(user) }}</p>
 			</div>
 			<mk-follow-button :user="user"/>
@@ -23,6 +23,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -38,6 +39,7 @@ export default Vue.extend({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		fetch() {
 			this.fetching = true;
 			this.users = [];
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index f29f9b74e..7f8c35c2f 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
-	<span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user.name }}</span>
+	<span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ name }}</span>
 	<mk-messaging-room :user="user" :class="$style.content"/>
 </mk-window>
 </template>
@@ -9,10 +9,14 @@
 import Vue from 'vue';
 import { url } from '../../../config';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
+		name(): string {
+			return getUserName(this.user);
+		},
 		popout(): string {
 			return `${url}/i/messaging/${getAcct(this.user)}`;
 		}
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index c5ab284df..d8b8eab21 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -11,7 +11,7 @@
 					<div class="text">
 						<p>
 							<mk-reaction-icon :reaction="notification.reaction"/>
-							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link>
 						</p>
 						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
@@ -24,7 +24,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
 						</p>
 						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
@@ -37,7 +37,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:quote-left%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
 						</p>
 						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
@@ -48,7 +48,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:user-plus%
-							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link>
 						</p>
 					</div>
 				</template>
@@ -58,7 +58,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:reply%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
 						</p>
 						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
@@ -69,7 +69,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:at%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
 						</p>
 						<a class="post-preview" :href="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
 					</div>
@@ -79,7 +79,7 @@
 						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
-						<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
+						<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</a></p>
 						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
 						</router-link>
@@ -104,6 +104,7 @@
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
 import getPostSummary from '../../../../../renderers/get-post-summary';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -154,6 +155,7 @@ export default Vue.extend({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		fetchMoreNotifications() {
 			this.fetchingMoreNotifications = true;
 
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
index 59bc9ce0c..496003eb8 100644
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ b/src/client/app/desktop/views/components/post-detail.sub.vue
@@ -6,7 +6,7 @@
 	<div class="main">
 		<header>
 			<div class="left">
-				<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
+				<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ getUserName(post.user) }}</router-link>
 				<span class="username">@{{ acct }}</span>
 			</div>
 			<div class="right">
@@ -29,6 +29,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
@@ -36,6 +37,9 @@ export default Vue.extend({
 		acct() {
 			return getAcct(this.post.user);
 		},
+		name() {
+			return getUserName(this.post.user);
+		},
 		title(): string {
 			return dateStringify(this.post.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index 8000ce2e6..1a3c0d1b6 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -22,7 +22,7 @@
 				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
-			<router-link class="name" :href="`/@${acct}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :href="`/@${acct}`">{{ getUserName(post.user) }}</router-link>
 			がRepost
 		</p>
 	</div>
@@ -31,7 +31,7 @@
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<header>
-			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
+			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link>
 			<span class="username">@{{ pAcct }}</span>
 			<router-link class="time" :to="`/@${pAcct}/${p.id}`">
 				<mk-time :time="p.createdAt"/>
@@ -79,6 +79,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
@@ -133,9 +134,15 @@ export default Vue.extend({
 		acct(): string {
 			return getAcct(this.post.user);
 		},
+		name(): string {
+			return getUserName(this.post.user);
+		},
 		pAcct(): string {
 			return getAcct(this.p.user);
 		},
+		pName(): string {
+			return getUserName(this.p.user);
+		},
 		urls(): string[] {
 			if (this.p.text) {
 				const ast = parse(this.p.text);
diff --git a/src/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue
index 7129f67b3..99d9442d9 100644
--- a/src/client/app/desktop/views/components/post-preview.vue
+++ b/src/client/app/desktop/views/components/post-preview.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.createdAt"/>
@@ -22,6 +22,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
@@ -29,6 +30,9 @@ export default Vue.extend({
 		acct() {
 			return getAcct(this.post.user);
 		},
+		name() {
+			return getUserName(this.post.user);
+		},
 		title(): string {
 			return dateStringify(this.post.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue
index dffecb89c..a9cd0a927 100644
--- a/src/client/app/desktop/views/components/posts.post.sub.vue
+++ b/src/client/app/desktop/views/components/posts.post.sub.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.createdAt"/>
@@ -22,6 +22,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
@@ -29,6 +30,9 @@ export default Vue.extend({
 		acct() {
 			return getAcct(this.post.user);
 		},
+		name(): string {
+			return getUserName(this.post.user);
+		},
 		title(): string {
 			return dateStringify(this.post.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index 9a13dd687..17fe33042 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -10,7 +10,7 @@
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</a>
+			<a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ getUserName(post.user) }}</a>
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="post.createdAt"/>
@@ -86,6 +86,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
@@ -124,6 +125,9 @@ export default Vue.extend({
 		acct(): string {
 			return getAcct(this.p.user);
 		},
+		name(): string {
+			return getUserName(this.p.user);
+		},
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue
index c87f973fa..6bdc76653 100644
--- a/src/client/app/desktop/views/components/settings.mute.vue
+++ b/src/client/app/desktop/views/components/settings.mute.vue
@@ -5,7 +5,7 @@
 	</div>
 	<div class="users" v-if="users.length != 0">
 		<div v-for="user in users" :key="user.id">
-			<p><b>{{ user.name }}</b> @{{ getAcct(user) }}</p>
+			<p><b>{{ getUserName(user) }}</b> @{{ getAcct(user) }}</p>
 		</div>
 	</div>
 </div>
@@ -14,6 +14,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -23,7 +24,8 @@ export default Vue.extend({
 		};
 	},
 	methods: {
-		getAcct
+		getAcct,
+		getUserName
 	},
 	mounted() {
 		(this as any).api('mute/list').then(x => {
diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
index ba86286f8..28be48e0a 100644
--- a/src/client/app/desktop/views/components/settings.profile.vue
+++ b/src/client/app/desktop/views/components/settings.profile.vue
@@ -42,7 +42,7 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		this.name = (this as any).os.i.name;
+		this.name = (this as any).os.i.name || '';
 		this.location = (this as any).os.i.account.profile.location;
 		this.description = (this as any).os.i.description;
 		this.birthday = (this as any).os.i.account.profile.birthday;
@@ -53,7 +53,7 @@ export default Vue.extend({
 		},
 		save() {
 			(this as any).api('i/update', {
-				name: this.name,
+				name: this.name || null,
 				location: this.location || null,
 				description: this.description || null,
 				birthday: this.birthday || null
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 448d04d26..7d93847fa 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -4,7 +4,7 @@
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
 		<div class="main">
-			<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p>
+			<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p>
 			<div class="container" ref="mainContainer">
 				<div class="left">
 					<x-nav/>
@@ -33,7 +33,14 @@ import XNotifications from './ui.header.notifications.vue';
 import XPost from './ui.header.post.vue';
 import XClock from './ui.header.clock.vue';
 
+import getUserName from '../../../../../renderers/get-user-name';
+
 export default Vue.extend({
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	},
 	components: {
 		XNav,
 		XSearch,
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index 2d7d4dc72..c7a132ecf 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 		</header>
 		<div class="body">
@@ -20,12 +20,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
 		acct() {
 			return getAcct(this.user);
+		},
+		name() {
+			return getUserName(this.user);
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
index 1e61f3ce1..1cc8d8a77 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -8,6 +8,7 @@
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -34,7 +35,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = 'メッセージ: ' + this.user.name;
+				document.title = 'メッセージ: ' + getUserName(this.user);
 
 				Progress.done();
 			});
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index 7497acd0e..16625b689 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -4,7 +4,7 @@
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
 	<router-link v-for="user in users" :to="`/@${getAcct(user)}`" :key="user.id">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/>
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)" v-user-preview="user.id"/>
 	</router-link>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
@@ -14,6 +14,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../../acct/render';
+import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
@@ -24,7 +25,8 @@ export default Vue.extend({
 		};
 	},
 	method() {
-		getAcct
+		getAcct,
+		getUserName
 	},
 	mounted() {
 		(this as any).api('users/followers', {
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 d30f423d5..5c6746d5d 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -7,7 +7,7 @@
 	<div class="container">
 		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/>
 		<div class="title">
-			<p class="name">{{ user.name }}</p>
+			<p class="name">{{ name }}</p>
 			<p class="username">@{{ acct }}</p>
 			<p class="location" v-if="user.host === null && user.account.profile.location">%fa:map-marker%{{ user.account.profile.location }}</p>
 		</div>
@@ -23,12 +23,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../../acct/render';
+import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
 		acct() {
 			return getAcct(this.user);
+		},
+		name() {
+			return getUserName(this.user);
 		}
 	},
 	mounted() {
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index 02ddc2421..d07b462b5 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -10,6 +10,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import parseAcct from '../../../../../../acct/parse';
+import getUserName from '../../../../../../renderers/get-user-name';
 import Progress from '../../../../common/scripts/loading';
 import XHeader from './user.header.vue';
 import XHome from './user.home.vue';
@@ -44,7 +45,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 				Progress.done();
-				document.title = user.name + ' | Misskey';
+				document.title = getUserName(user) + ' | Misskey';
 			});
 		}
 	}
diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue
index e10e9c4f7..fa6d8c34a 100644
--- a/src/client/app/desktop/views/widgets/channel.channel.post.vue
+++ b/src/client/app/desktop/views/widgets/channel.channel.post.vue
@@ -2,7 +2,7 @@
 <div class="post">
 	<header>
 		<a class="index" @click="reply">{{ post.index }}:</a>
-		<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user.id"><b>{{ post.user.name }}</b></router-link>
+		<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user.id"><b>{{ name }}</b></router-link>
 		<span>ID:<i>{{ acct }}</i></span>
 	</header>
 	<div>
@@ -20,12 +20,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
 		acct() {
 			return getAcct(this.post.user);
+		},
+		name() {
+			return getUserName(this.post.user);
 		}
 	},
 	methods: {
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 83cd67b50..98e42222e 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -15,19 +15,26 @@
 		title="クリックでアバター編集"
 		v-user-preview="os.i.id"
 	/>
-	<router-link class="name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link>
+	<router-link class="name" :to="`/@${os.i.username}`">{{ name }}</router-link>
 	<p class="username">@{{ os.i.username }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import getUserName from '../../../../../renderers/get-user-name';
+
 export default define({
 	name: 'profile',
 	props: () => ({
 		design: 0
 	})
 }).extend({
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	},
 	methods: {
 		func() {
 			if (this.props.design == 2) {
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index 6f6a10157..a5dabb68f 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -11,7 +11,7 @@
 				<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ _user.name }}</router-link>
+				<router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ getUserName(_user) }}</router-link>
 				<p class="username">@{{ getAcct(_user) }}</p>
 			</div>
 			<mk-follow-button :user="_user"/>
@@ -24,6 +24,7 @@
 <script lang="ts">
 import define from '../../../common/define-widget';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 const limit = 3;
 
@@ -45,6 +46,7 @@ export default define({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		func() {
 			this.props.compact = !this.props.compact;
 		},
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index 77abd3c0e..0492c5d86 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -3,7 +3,7 @@
 	<template v-if="notification.type == 'reaction'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user.name }}</p>
+			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ name }}</p>
 			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -11,7 +11,7 @@
 	<template v-if="notification.type == 'repost'">
 		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:retweet%{{ notification.post.user.name }}</p>
+			<p>%fa:retweet%{{ posterName }}</p>
 			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -19,7 +19,7 @@
 	<template v-if="notification.type == 'quote'">
 		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:quote-left%{{ notification.post.user.name }}</p>
+			<p>%fa:quote-left%{{ posterName }}</p>
 			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
 		</div>
 	</template>
@@ -27,14 +27,14 @@
 	<template v-if="notification.type == 'follow'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:user-plus%{{ notification.user.name }}</p>
+			<p>%fa:user-plus%{{ name }}</p>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'reply'">
 		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:reply%{{ notification.post.user.name }}</p>
+			<p>%fa:reply%{{ posterName }}</p>
 			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
 		</div>
 	</template>
@@ -42,7 +42,7 @@
 	<template v-if="notification.type == 'mention'">
 		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:at%{{ notification.post.user.name }}</p>
+			<p>%fa:at%{{ posterName }}</p>
 			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
 		</div>
 	</template>
@@ -50,7 +50,7 @@
 	<template v-if="notification.type == 'poll_vote'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:chart-pie%{{ notification.user.name }}</p>
+			<p>%fa:chart-pie%{{ name }}</p>
 			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -60,9 +60,18 @@
 <script lang="ts">
 import Vue from 'vue';
 import getPostSummary from '../../../../../renderers/get-post-summary';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['notification'],
+	computed: {
+		name() {
+			return getUserName(this.notification.user);
+		},
+		posterName() {
+			return getUserName(this.notification.post.user);
+		}
+	},
 	data() {
 		return {
 			getPostSummary
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 189d7195f..62a0e6425 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -8,7 +8,7 @@
 		<div class="text">
 			<p>
 				<mk-reaction-icon :reaction="notification.reaction"/>
-				<router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
 			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post) }}
@@ -25,7 +25,7 @@
 		<div class="text">
 			<p>
 				%fa:retweet%
-				<router-link :to="`/@${acct}`">{{ notification.post.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ getUserName(notification.post.user) }}</router-link>
 			</p>
 			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
@@ -45,7 +45,7 @@
 		<div class="text">
 			<p>
 				%fa:user-plus%
-				<router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
 		</div>
 	</div>
@@ -66,7 +66,7 @@
 		<div class="text">
 			<p>
 				%fa:chart-pie%
-				<router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
 			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
@@ -80,12 +80,19 @@
 import Vue from 'vue';
 import getPostSummary from '../../../../../renderers/get-post-summary';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['notification'],
 	computed: {
 		acct() {
 			return getAcct(this.notification.user);
+		},
+		name() {
+			return getUserName(this.notification.user);
+		},
+		posterName() {
+ 			return getUserName(this.notification.post.user);
 		}
 	},
 	data() {
diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue
index 38d4522d2..68a42ef24 100644
--- a/src/client/app/mobile/views/components/post-card.vue
+++ b/src/client/app/mobile/views/components/post-card.vue
@@ -2,7 +2,7 @@
 <div class="mk-post-card">
 	<a :href="`/@${acct}/${post.id}`">
 		<header>
-			<img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ post.user.name }}</h3>
+			<img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ name }}</h3>
 		</header>
 		<div>
 			{{ text }}
@@ -16,6 +16,7 @@
 import Vue from 'vue';
 import summary from '../../../../../renderers/get-post-summary';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
@@ -23,6 +24,9 @@ export default Vue.extend({
 		acct() {
 			return getAcct(this.post.user);
 		},
+		name() {
+			return getUserName(this.post.user);
+		},
 		text(): string {
 			return summary(this.post);
 		}
diff --git a/src/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue
index 7ff9f1aab..98d6a14ca 100644
--- a/src/client/app/mobile/views/components/post-detail.sub.vue
+++ b/src/client/app/mobile/views/components/post-detail.sub.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ getUserName(post.user) }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.createdAt"/>
@@ -21,12 +21,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
 		acct() {
 			return getAcct(this.post.user);
+		},
+		name() {
+			return getUserName(this.post.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
index ed394acd3..0226ce081 100644
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -22,7 +22,7 @@
 			</router-link>
 			%fa:retweet%
 			<router-link class="name" :to="`/@${acct}`">
-				{{ post.user.name }}
+				{{ name }}
 			</router-link>
 			がRepost
 		</p>
@@ -33,7 +33,7 @@
 				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			<div>
-				<router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link>
+				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
 				<span class="username">@{{ pAcct }}</span>
 			</div>
 		</header>
@@ -81,6 +81,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
@@ -114,9 +115,15 @@ export default Vue.extend({
 		acct(): string {
 			return getAcct(this.post.user);
 		},
+		name(): string {
+			return getUserName(this.post.user);
+		},
 		pAcct(): string {
 			return getAcct(this.p.user);
 		},
+		pName(): string {
+			return getUserName(this.p.user);
+		},
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue
index 81b0c22bf..96bd4d5c1 100644
--- a/src/client/app/mobile/views/components/post-preview.vue
+++ b/src/client/app/mobile/views/components/post-preview.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.createdAt"/>
@@ -21,12 +21,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
 		acct() {
 			return getAcct(this.post.user);
+		},
+		name() {
+			return getUserName(this.post.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/post.sub.vue b/src/client/app/mobile/views/components/post.sub.vue
index 85ddb9188..909d5cb59 100644
--- a/src/client/app/mobile/views/components/post.sub.vue
+++ b/src/client/app/mobile/views/components/post.sub.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ getUserName(post.user) }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.createdAt"/>
@@ -21,12 +21,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
 		acct() {
 			return getAcct(this.post.user);
+		},
+		name() {
+			return getUserName(this.post.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
index 1454bc939..eee1e80fd 100644
--- a/src/client/app/mobile/views/components/post.vue
+++ b/src/client/app/mobile/views/components/post.vue
@@ -10,7 +10,7 @@
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="post.createdAt"/>
@@ -21,7 +21,7 @@
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link>
+				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
 				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
 				<span class="username">@{{ pAcct }}</span>
 				<div class="info">
@@ -78,6 +78,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
@@ -102,9 +103,15 @@ export default Vue.extend({
 		acct(): string {
 			return getAcct(this.post.user);
 		},
+		name(): string {
+			return getUserName(this.post.user);
+		},
 		pAcct(): string {
 			return getAcct(this.p.user);
 		},
+		pName(): string {
+			return getUserName(this.p.user);
+		},
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index 2bf47a90a..fd4f31fd9 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -3,7 +3,7 @@
 	<mk-special-message/>
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
-		<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p>
+		<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p>
 		<div class="content" ref="mainContainer">
 			<button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button>
 			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages || hasGameInvitations">%fa:circle%</template>
@@ -19,9 +19,15 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as anime from 'animejs';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['func'],
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	},
 	data() {
 		return {
 			hasUnreadNotifications: false,
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index a923774a7..61dd8ca9d 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -11,7 +11,7 @@
 		<div class="body" v-if="isOpen">
 			<router-link class="me" v-if="os.isSignedIn" :to="`/@${os.i.username}`">
 				<img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/>
-				<p class="name">{{ os.i.name }}</p>
+				<p class="name">{{ name }}</p>
 			</router-link>
 			<div class="links">
 				<ul>
@@ -40,9 +40,15 @@
 <script lang="ts">
 import Vue from 'vue';
 import { docsUrl, chUrl, lang } from '../../../config';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['isOpen'],
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	},
 	data() {
 		return {
 			hasUnreadNotifications: false,
diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
index 46fa3b473..e8698a62f 100644
--- a/src/client/app/mobile/views/components/user-card.vue
+++ b/src/client/app/mobile/views/components/user-card.vue
@@ -5,7 +5,7 @@
 			<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
 		</a>
 	</header>
-	<a class="name" :href="`/@${acct}`" target="_blank">{{ user.name }}</a>
+	<a class="name" :href="`/@${acct}`" target="_blank">{{ name }}</a>
 	<p class="username">@{{ acct }}</p>
 	<mk-follow-button :user="user"/>
 </div>
@@ -14,12 +14,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
 		acct() {
 			return getAcct(this.user);
+		},
+		name() {
+			return getUserName(this.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index 00f87e554..72a6bcf8a 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 		</header>
 		<div class="body">
@@ -18,12 +18,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
 		acct() {
 			return getAcct(this.user);
+		},
+		name() {
+			return getUserName(this.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index d2e6a9aea..f4225d556 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -2,7 +2,7 @@
 <mk-ui>
 	<template slot="header" v-if="!fetching">
 		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
-		{{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) }}
+		{{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', name) }}
 	</template>
 	<mk-users-list
 		v-if="!fetching"
@@ -20,6 +20,7 @@
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -28,6 +29,11 @@ export default Vue.extend({
 			user: null
 		};
 	},
+	computed: {
+		name() {
+			return getUserName(this.user);
+		}
+	},
 	watch: {
 		$route: 'fetch'
 	},
@@ -46,7 +52,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
+				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', this.name) + ' | Misskey';
 			});
 		},
 		onLoaded() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index 3690536cf..cc2442e47 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -2,7 +2,7 @@
 <mk-ui>
 	<template slot="header" v-if="!fetching">
 		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
-		{{ '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name) }}
+		{{ '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', name) }}
 	</template>
 	<mk-users-list
 		v-if="!fetching"
@@ -20,6 +20,7 @@
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -28,6 +29,11 @@ export default Vue.extend({
 			user: null
 		};
 	},
+	computed: {
+		name() {
+			return getUserName(this.user);
+		}
+	},
 	watch: {
 		$route: 'fetch'
 	},
@@ -46,7 +52,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
+				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', this.name) + ' | Misskey';
 			});
 		},
 		onLoaded() {
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index 172666eea..ae6662dc0 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-ui>
 	<span slot="header">
-		<template v-if="user">%fa:R comments%{{ user.name }}</template>
+		<template v-if="user">%fa:R comments%{{ name }}</template>
 		<template v-else><mk-ellipsis/></template>
 	</span>
 	<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
@@ -11,6 +11,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import parseAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -19,6 +20,11 @@ export default Vue.extend({
 			user: null
 		};
 	},
+	computed: {
+		name() {
+			return getUserName(this.user);
+		}
+	},
 	watch: {
 		$route: 'fetch'
 	},
@@ -33,7 +39,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${user.name} | Misskey`;
+				document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${this.name} | Misskey`;
 			});
 		}
 	}
diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue
index 15f9bc9b6..4a560c027 100644
--- a/src/client/app/mobile/views/pages/profile-setting.vue
+++ b/src/client/app/mobile/views/pages/profile-setting.vue
@@ -52,7 +52,7 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		this.name = (this as any).os.i.name;
+		this.name = (this as any).os.i.name || '';
 		this.location = (this as any).os.i.account.profile.location;
 		this.description = (this as any).os.i.description;
 		this.birthday = (this as any).os.i.account.profile.birthday;
@@ -94,7 +94,7 @@ export default Vue.extend({
 			this.saving = true;
 
 			(this as any).api('i/update', {
-				name: this.name,
+				name: this.name || null,
 				location: this.location || null,
 				description: this.description || null,
 				birthday: this.birthday || null
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index a945a21c5..58a9d4e37 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -2,7 +2,7 @@
 <mk-ui>
 	<span slot="header">%fa:cog%%i18n:mobile.tags.mk-settings-page.settings%</span>
 	<div :class="$style.content">
-		<p v-html="'%i18n:mobile.tags.mk-settings.signed-in-as%'.replace('{}', '<b>' + os.i.name + '</b>')"></p>
+		<p v-html="'%i18n:mobile.tags.mk-settings.signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p>
 		<ul>
 			<li><router-link to="./settings/profile">%fa:user%%i18n:mobile.tags.mk-settings-page.profile%%fa:angle-right%</router-link></li>
 			<li><router-link to="./settings/authorized-apps">%fa:puzzle-piece%%i18n:mobile.tags.mk-settings-page.applications%%fa:angle-right%</router-link></li>
@@ -20,6 +20,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { version, codename } from '../../../config';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -28,6 +29,11 @@ export default Vue.extend({
 			codename
 		};
 	},
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	},
 	mounted() {
 		document.title = 'Misskey | %i18n:mobile.tags.mk-settings-page.settings%';
 		document.documentElement.style.background = '#313a42';
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index be696484b..3b7b4d6c2 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span>
+	<span slot="header" v-if="!fetching">%fa:user% {{ user }}</span>
 	<main v-if="!fetching">
 		<header>
 			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div>
@@ -12,7 +12,7 @@
 					<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
 				</div>
 				<div class="title">
-					<h1>{{ user.name }}</h1>
+					<h1>{{ user }}</h1>
 					<span class="username">@{{ acct }}</span>
 					<span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span>
 				</div>
@@ -61,7 +61,7 @@
 import Vue from 'vue';
 import * as age from 's-age';
 import getAcct from '../../../../../acct/render';
-import getAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
 import Progress from '../../../common/scripts/loading';
 import XHome from './user/home.vue';
 
@@ -82,6 +82,9 @@ export default Vue.extend({
 		},
 		age(): number {
 			return age(this.user.account.profile.birthday);
+		},
+		name() {
+			return getUserName(this.user);
 		}
 	},
 	watch: {
@@ -102,7 +105,7 @@ export default Vue.extend({
 				this.fetching = false;
 
 				Progress.done();
-				document.title = user.name + ' | Misskey';
+				document.title = this.name + ' | Misskey';
 			});
 		}
 	}
diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
index ffdd9f178..1b128e2f2 100644
--- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -3,7 +3,7 @@
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
 		<a v-for="user in users" :key="user.id" :href="`/@${getAcct(user)}`">
-			<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name"/>
+			<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)"/>
 		</a>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
@@ -13,6 +13,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../../acct/render';
+import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
@@ -22,6 +23,11 @@ export default Vue.extend({
 			users: []
 		};
 	},
+	computed: {
+		name() {
+			return getUserName(this.user);
+		}
+	},
 	methods: {
 		getAcct
 	},
diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue
index f1d283e45..bd257a3ff 100644
--- a/src/client/app/mobile/views/widgets/profile.vue
+++ b/src/client/app/mobile/views/widgets/profile.vue
@@ -8,15 +8,23 @@
 			:src="`${os.i.avatarUrl}?thumbnail&size=96`"
 			alt="avatar"
 		/>
-		<router-link :class="$style.name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link>
+		<router-link :class="$style.name" :to="`/@${os.i.username}`">{{ name }}</router-link>
 	</mk-widget-container>
 </div>
 </template>
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import getUserName from '../../../../../renderers/get-user-name';
+
 export default define({
 	name: 'profile'
+}).extend({
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	}
 });
 </script>
 
diff --git a/src/models/user.ts b/src/models/user.ts
index f817c33aa..92091c687 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -21,7 +21,7 @@ type IUserBase = {
 	deletedAt: Date;
 	followersCount: number;
 	followingCount: number;
-	name: string;
+	name?: string;
 	postsCount: number;
 	driveCapacity: number;
 	username: string;
@@ -99,8 +99,8 @@ export function validatePassword(password: string): boolean {
 	return typeof password == 'string' && password != '';
 }
 
-export function isValidName(name: string): boolean {
-	return typeof name == 'string' && name.length < 30 && name.trim() != '';
+export function isValidName(name?: string): boolean {
+	return name === null || (typeof name == 'string' && name.length < 30 && name.trim() != '');
 }
 
 export function isValidDescription(description: string): boolean {
diff --git a/src/othello/ai/back.ts b/src/othello/ai/back.ts
index d6704b175..4d06ed956 100644
--- a/src/othello/ai/back.ts
+++ b/src/othello/ai/back.ts
@@ -9,6 +9,7 @@
 import * as request from 'request-promise-native';
 import Othello, { Color } from '../core';
 import conf from '../../config';
+import getUserName from '../../renderers/get-user-name';
 
 let game;
 let form;
@@ -47,8 +48,8 @@ process.on('message', async msg => {
 		const user = game.user1Id == id ? game.user2 : game.user1;
 		const isSettai = form[0].value === 0;
 		const text = isSettai
-			? `?[${user.name}](${conf.url}/@${user.username})さんの接待を始めました!`
-			: `対局を?[${user.name}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`;
+			? `?[${getUserName(user)}](${conf.url}/@${user.username})さんの接待を始めました!`
+			: `対局を?[${getUserName(user)}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`;
 
 		const res = await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
@@ -72,15 +73,15 @@ process.on('message', async msg => {
 		const isSettai = form[0].value === 0;
 		const text = isSettai
 			? msg.body.winnerId === null
-				? `?[${user.name}](${conf.url}/@${user.username})さんに接待で引き分けました...`
+				? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で引き分けました...`
 				: msg.body.winnerId == id
-					? `?[${user.name}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...`
-					: `?[${user.name}](${conf.url}/@${user.username})さんに接待で負けてあげました♪`
+					? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...`
+					: `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で負けてあげました♪`
 			: msg.body.winnerId === null
-				? `?[${user.name}](${conf.url}/@${user.username})さんと引き分けました~`
+				? `?[${getUserName(user)}](${conf.url}/@${user.username})さんと引き分けました~`
 				: msg.body.winnerId == id
-					? `?[${user.name}](${conf.url}/@${user.username})さんに勝ちました♪`
-					: `?[${user.name}](${conf.url}/@${user.username})さんに負けました...`;
+					? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに勝ちました♪`
+					: `?[${getUserName(user)}](${conf.url}/@${user.username})さんに負けました...`;
 
 		await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
diff --git a/src/renderers/get-notification-summary.ts b/src/renderers/get-notification-summary.ts
index 03db722c8..f5e38faf9 100644
--- a/src/renderers/get-notification-summary.ts
+++ b/src/renderers/get-notification-summary.ts
@@ -1,3 +1,4 @@
+import getUserName from '../renderers/get-user-name';
 import getPostSummary from './get-post-summary';
 import getReactionEmoji from './get-reaction-emoji';
 
@@ -8,19 +9,19 @@ import getReactionEmoji from './get-reaction-emoji';
 export default function(notification: any): string {
 	switch (notification.type) {
 		case 'follow':
-			return `${notification.user.name}にフォローされました`;
+			return `${getUserName(notification.user)}にフォローされました`;
 		case 'mention':
-			return `言及されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
+			return `言及されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
 		case 'reply':
-			return `返信されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
+			return `返信されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
 		case 'repost':
-			return `Repostされました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
+			return `Repostされました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
 		case 'quote':
-			return `引用されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
+			return `引用されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
 		case 'reaction':
-			return `リアクションされました:\n${notification.user.name} <${getReactionEmoji(notification.reaction)}>「${getPostSummary(notification.post)}」`;
+			return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getPostSummary(notification.post)}」`;
 		case 'poll_vote':
-			return `投票されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
+			return `投票されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
 		default:
 			return `<不明な通知タイプ: ${notification.type}>`;
 	}
diff --git a/src/renderers/get-user-summary.ts b/src/renderers/get-user-summary.ts
index 208987113..d002933b6 100644
--- a/src/renderers/get-user-summary.ts
+++ b/src/renderers/get-user-summary.ts
@@ -1,12 +1,13 @@
 import { IUser, isLocalUser } from '../models/user';
 import getAcct from '../acct/render';
+import getUserName from './get-user-name';
 
 /**
  * ユーザーを表す文字列を取得します。
  * @param user ユーザー
  */
 export default function(user: IUser): string {
-	let string = `${user.name} (@${getAcct(user)})\n` +
+	let string = `${getUserName(user)} (@${getAcct(user)})\n` +
 		`${user.postsCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
 
 	if (isLocalUser(user)) {
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index 7e80f31e5..a44aa9d7b 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -4,6 +4,7 @@ import * as bcrypt from 'bcryptjs';
 import User, { IUser, init as initUser, ILocalUser } from '../../../models/user';
 
 import getPostSummary from '../../../renderers/get-post-summary';
+import getUserName from '../../../renderers/get-user-name';
 import getUserSummary from '../../../renderers/get-user-summary';
 import parseAcct from '../../../acct/parse';
 import getNotificationSummary from '../../../renderers/get-notification-summary';
@@ -90,7 +91,7 @@ export default class BotCore extends EventEmitter {
 					'タイムラインや通知を見た後、「次」というとさらに遡ることができます。';
 
 			case 'me':
-				return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
+				return this.user ? `${getUserName(this.user)}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
 
 			case 'login':
 			case 'signin':
@@ -230,7 +231,7 @@ class SigninContext extends Context {
 			if (same) {
 				this.bot.signin(this.temporaryUser);
 				this.bot.clearContext();
-				return `${this.temporaryUser.name}さん、おかえりなさい!`;
+				return `${getUserName(this.temporaryUser)}さん、おかえりなさい!`;
 			} else {
 				return `パスワードが違います... もう一度教えてください:`;
 			}
@@ -305,7 +306,7 @@ class TlContext extends Context {
 			this.emit('updated');
 
 			const text = tl
-				.map(post => `${post.user.name}\n「${getPostSummary(post)}」`)
+				.map(post => `${getUserName(post.user)}\n「${getPostSummary(post)}」`)
 				.join('\n-----\n');
 
 			return text;
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index 7847cbdea..1191aaf39 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -10,6 +10,7 @@ import prominence = require('prominence');
 import getAcct from '../../../../acct/render';
 import parseAcct from '../../../../acct/parse';
 import getPostSummary from '../../../../renderers/get-post-summary';
+import getUserName from '../../../../renderers/get-user-name';
 
 const redis = prominence(_redis);
 
@@ -131,7 +132,7 @@ class LineBot extends BotCore {
 			template: {
 				type: 'buttons',
 				thumbnailImageUrl: `${user.avatarUrl}?thumbnail&size=1024`,
-				title: `${user.name} (@${acct})`,
+				title: `${getUserName(user)} (@${acct})`,
 				text: user.description || '(no description)',
 				actions: actions
 			}
@@ -146,7 +147,7 @@ class LineBot extends BotCore {
 			limit: 5
 		}, this.user);
 
-		const text = `${tl[0].user.name}さんのタイムラインはこちらです:\n\n` + tl
+		const text = `${getUserName(tl[0].user)}さんのタイムラインはこちらです:\n\n` + tl
 			.map(post => getPostSummary(post))
 			.join('\n-----\n');
 

From d99d628f50eb62ca9d5ac56470e8e6f0ae8c3d2c Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 6 Apr 2018 01:37:24 +0900
Subject: [PATCH 12/15] =?UTF-8?q?Do=20not=20save=20=E5=90=8D=E7=84=A1?=
 =?UTF-8?q?=E3=81=97=20as=20the=20name=20of=20a=20new=20user?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/server/api/private/signup.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index 4203ce526..c54d6f1a1 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -47,7 +47,6 @@ export default async (req: express.Request, res: express.Response) => {
 
 	const username = req.body['username'];
 	const password = req.body['password'];
-	const name = '名無し';
 
 	// Validate username
 	if (!validateUsername(username)) {
@@ -113,7 +112,7 @@ export default async (req: express.Request, res: express.Response) => {
 		description: null,
 		followersCount: 0,
 		followingCount: 0,
-		name: name,
+		name: null,
 		postsCount: 0,
 		driveCapacity: 1024 * 1024 * 128, // 128MiB
 		username: username,

From 39b896a900f4c0c3bf48cab9e01bc2f7b1f05e47 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 6 Apr 2018 12:19:44 +0900
Subject: [PATCH 13/15] Add missing src/renderers/get-user-name.ts

---
 src/renderers/get-user-name.ts | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 src/renderers/get-user-name.ts

diff --git a/src/renderers/get-user-name.ts b/src/renderers/get-user-name.ts
new file mode 100644
index 000000000..acd5e6626
--- /dev/null
+++ b/src/renderers/get-user-name.ts
@@ -0,0 +1,5 @@
+import { IUser } from '../models/user';
+
+export default function(user: IUser): string {
+	return user.name || '名無し';
+}

From 6752594578e64a26c0cd1c5a9fd2d83313134466 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 6 Apr 2018 12:20:11 +0900
Subject: [PATCH 14/15] Resolve local Person ID

---
 src/remote/activitypub/resolve-person.ts | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index a7c0020dd..84746169f 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -1,5 +1,7 @@
 import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
+import parseAcct from '../../acct/parse';
+import config from '../../config';
 import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
 import { createHttp } from '../../queue';
 import webFinger from '../webfinger';
@@ -10,10 +12,18 @@ async function isCollection(collection) {
 }
 
 export default async (parentResolver, value, verifier?: string) => {
+	const id = value.id || value;
+	const localPrefix = config.url + '/@';
+
+	if (id.startsWith(localPrefix)) {
+		return User.findOne(parseAcct(id.slice(localPrefix)));
+	}
+
 	const { resolver, object } = await parentResolver.resolveOne(value);
 
 	if (
 		object === null ||
+		object.id !== id ||
 		object.type !== 'Person' ||
 		typeof object.preferredUsername !== 'string' ||
 		!validateUsername(object.preferredUsername) ||
@@ -36,7 +46,7 @@ export default async (parentResolver, value, verifier?: string) => {
 			resolved => isCollection(resolved.object) ? resolved.object : null,
 			() => null
 		),
-		webFinger(object.id, verifier),
+		webFinger(id, verifier),
 	]);
 
 	const host = toUnicode(finger.subject.replace(/^.*?@/, ''));
@@ -64,7 +74,7 @@ export default async (parentResolver, value, verifier?: string) => {
 				publicKeyPem: object.publicKey.publicKeyPem
 			},
 			inbox: object.inbox,
-			uri: object.id,
+			uri: id,
 		},
 	});
 

From 41acde0d8a5fd59e4b0250d650eade3019947f2e Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 6 Apr 2018 12:53:39 +0900
Subject: [PATCH 15/15] Resolve local Object ID

---
 src/remote/activitypub/create.ts | 37 ++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 3bc0c66f3..bbe595a45 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -1,8 +1,10 @@
 import { JSDOM } from 'jsdom';
 import { ObjectID } from 'mongodb';
+import parseAcct from '../../acct/parse';
 import config from '../../config';
 import DriveFile from '../../models/drive-file';
 import Post from '../../models/post';
+import User from '../../models/user';
 import { IRemoteUser } from '../../models/user';
 import uploadFromUrl from '../../drive/upload-from-url';
 import createPost from '../../post/create';
@@ -133,6 +135,41 @@ class Creator {
 
 		return collection.object.map(async element => {
 			const uri = element.id || element;
+			const localPrefix = config.url + '/@';
+
+			if (uri.startsWith(localPrefix)) {
+				const [acct, id] = uri.slice(localPrefix).split('/', 2);
+				const user = await User.aggregate([
+					{
+						$match: parseAcct(acct)
+					},
+					{
+						$lookup: {
+							from: 'posts',
+							localField: '_id',
+							foreignField: 'userId',
+							as: 'post'
+						}
+					},
+					{
+						$match: {
+							post: { _id: id }
+						}
+					}
+				]);
+
+				if (user === null || user.posts.length <= 0) {
+					throw new Error();
+				}
+
+				return {
+					resolver: collection.resolver,
+					object: {
+						$ref: 'posts',
+						id
+					}
+				};
+			}
 
 			try {
 				await Promise.all([