From 5f208a7d99cf985e9c613e2d65656ae4d46aa68a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 6 Aug 2018 18:28:27 +0900
Subject: [PATCH] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E6=A4=9C?=
 =?UTF-8?q?=E7=B4=A2API=E3=82=92=E7=B5=B1=E5=90=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../common/views/components/autocomplete.vue  |   2 +-
 src/server/api/endpoints/users/search.ts      | 161 +++++++++++++++---
 .../api/endpoints/users/search_by_username.ts |  70 --------
 3 files changed, 143 insertions(+), 90 deletions(-)
 delete mode 100644 src/server/api/endpoints/users/search_by_username.ts

diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index cd6066877c..b274eaa0a0 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -132,7 +132,7 @@ export default Vue.extend({
 					this.users = users;
 					this.fetching = false;
 				} else {
-					(this as any).api('users/search_by_username', {
+					(this as any).api('users/search', {
 						query: this.q,
 						limit: 30
 					}).then(users => {
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
index d443d35b47..eda3f95728 100644
--- a/src/server/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -1,33 +1,156 @@
 import $ from 'cafy';
-import User, { pack, ILocalUser } from '../../../../models/user';
 const escapeRegexp = require('escape-regexp');
+import User, { pack, ILocalUser, validateUsername, IUser } from '../../../../models/user';
+import getParams from '../../get-params';
+
+export const meta = {
+	desc: {
+		ja: 'ユーザーを検索します。'
+	},
+
+	requireCredential: false,
+
+	params: {
+		query: $.str.note({
+			desc: {
+				ja: 'クエリ'
+			}
+		}),
+
+		offset: $.num.optional.min(0).note({
+			default: 0,
+			desc: {
+				ja: 'オフセット'
+			}
+		}),
+
+		limit: $.num.optional.range(1, 100).note({
+			default: 10,
+			desc: {
+				ja: '取得する数'
+			}
+		}),
+
+		localOnly: $.bool.optional.note({
+			default: false,
+			desc: {
+				ja: 'ローカルユーザーのみ検索対象にするか否か'
+			}
+		}),
+	},
+};
 
 /**
  * Search a user
  */
 export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'query' parameter
-	const [query, queryError] = $.str.pipe(x => x != '').get(params.query);
-	if (queryError) return rej('invalid query param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
-	// Get 'max' parameter
-	const [max = 10, maxErr] = $.num.optional.range(1, 30).get(params.max);
-	if (maxErr) return rej('invalid max param');
+	const isUsername = validateUsername(ps.query.replace('@', ''));
 
-	const escapedQuery = escapeRegexp(query);
+	let users: IUser[] = [];
 
-	// Search users
-	const users = await User
-		.find({
-			host: null,
-			$or: [{
-				usernameLower: new RegExp(escapedQuery.replace('@', '').toLowerCase())
+	if (isUsername) {
+		users = await User
+			.find({
+				host: null,
+				usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase()))
 			}, {
-				name: new RegExp(escapedQuery)
-			}]
-		}, {
-			limit: max
-		});
+				limit: ps.limit,
+				skip: ps.offset
+			});
+
+		if (users.length < ps.limit && !ps.localOnly) {
+			const otherUsers = await User
+				.find({
+					host: { $ne: null },
+					usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase()))
+				}, {
+					limit: ps.limit - users.length
+				});
+
+			users = users.concat(otherUsers);
+		}
+
+		if (users.length < ps.limit) {
+			const otherUsers = await User
+				.find({
+					_id: { $nin: users.map(u => u._id) },
+					host: null,
+					usernameLower: new RegExp(escapeRegexp(ps.query.replace('@', '').toLowerCase()))
+				}, {
+					limit: ps.limit - users.length
+				});
+
+			users = users.concat(otherUsers);
+		}
+
+		if (users.length < ps.limit && !ps.localOnly) {
+			const otherUsers = await User
+				.find({
+					_id: { $nin: users.map(u => u._id) },
+					host: { $ne: null },
+					usernameLower: new RegExp(escapeRegexp(ps.query.replace('@', '').toLowerCase()))
+				}, {
+					limit: ps.limit - users.length
+				});
+
+			users = users.concat(otherUsers);
+		}
+	}
+
+	if (users.length < ps.limit) {
+		const otherUsers = await User
+			.find({
+				_id: { $nin: users.map(u => u._id) },
+				host: null,
+				name: new RegExp('^' + escapeRegexp(ps.query.toLowerCase()))
+			}, {
+				limit: ps.limit - users.length
+			});
+
+		users = users.concat(otherUsers);
+	}
+
+	if (users.length < ps.limit && !ps.localOnly) {
+		const otherUsers = await User
+			.find({
+				_id: { $nin: users.map(u => u._id) },
+				host: { $ne: null },
+				name: new RegExp('^' + escapeRegexp(ps.query.toLowerCase()))
+			}, {
+				limit: ps.limit - users.length
+			});
+
+		users = users.concat(otherUsers);
+	}
+
+	if (users.length < ps.limit) {
+		const otherUsers = await User
+			.find({
+				_id: { $nin: users.map(u => u._id) },
+				host: null,
+				name: new RegExp(escapeRegexp(ps.query.toLowerCase()))
+			}, {
+				limit: ps.limit - users.length
+			});
+
+		users = users.concat(otherUsers);
+	}
+
+	if (users.length < ps.limit && !ps.localOnly) {
+		const otherUsers = await User
+			.find({
+				_id: { $nin: users.map(u => u._id) },
+				host: { $ne: null },
+				name: new RegExp(escapeRegexp(ps.query.toLowerCase()))
+			}, {
+				limit: ps.limit - users.length
+			});
+
+		users = users.concat(otherUsers);
+	}
 
 	// Serialize
 	res(await Promise.all(users.map(user => pack(user, me, { detail: true }))));
diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts
deleted file mode 100644
index bfab378389..0000000000
--- a/src/server/api/endpoints/users/search_by_username.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import $ from 'cafy';
-import User, { pack, ILocalUser } from '../../../../models/user';
-const escapeRegexp = require('escape-regexp');
-
-/**
- * Search a user by username
- */
-export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'query' parameter
-	const [query, queryError] = $.str.get(params.query);
-	if (queryError) return rej('invalid query param');
-
-	// Get 'offset' parameter
-	const [offset = 0, offsetErr] = $.num.optional.min(0).get(params.offset);
-	if (offsetErr) return rej('invalid offset param');
-
-	// Get 'limit' parameter
-	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
-
-	let users = await User
-		.find({
-			host: null,
-			usernameLower: new RegExp('^' + escapeRegexp(query.toLowerCase()))
-		}, {
-			limit: limit,
-			skip: offset
-		});
-
-	if (users.length < limit) {
-		const otherUsers = await User
-			.find({
-				host: { $ne: null },
-				usernameLower: new RegExp('^' + escapeRegexp(query.toLowerCase()))
-			}, {
-				limit: limit - users.length
-			});
-
-		users = users.concat(otherUsers);
-	}
-
-	if (users.length < limit) {
-		const otherUsers = await User
-			.find({
-				_id: { $nin: users.map(u => u._id) },
-				host: null,
-				usernameLower: new RegExp(escapeRegexp(query.toLowerCase()))
-			}, {
-				limit: limit - users.length
-			});
-
-		users = users.concat(otherUsers);
-	}
-
-	if (users.length < limit) {
-		const otherUsers = await User
-			.find({
-				_id: { $nin: users.map(u => u._id) },
-				host: { $ne: null },
-				usernameLower: new RegExp(escapeRegexp(query.toLowerCase()))
-			}, {
-				limit: limit - users.length
-			});
-
-		users = users.concat(otherUsers);
-	}
-
-	// Serialize
-	res(await Promise.all(users.map(user => pack(user, me, { detail: true }))));
-});