From eb443709bfcda81c329b51f95bf02cef85932456 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 13:05:30 +0900
Subject: [PATCH] #1294

---
 locales/ja.yml                                |  2 +-
 src/api/bot/core.ts                           |  2 +-
 .../app/common/views/components/signin.vue    |  2 +-
 .../app/common/views/components/signup.vue    |  2 +-
 .../common/views/directives/autocomplete.ts   |  2 +-
 src/web/app/dev/views/new-app.vue             |  4 +-
 src/web/app/mobile/views/pages/welcome.vue    |  2 +-
 tools/migration/node.2018-03-28.appname.js    | 40 +++++++++++++++++++
 tools/migration/node.2018-03-28.username.js   | 40 +++++++++++++++++++
 9 files changed, 88 insertions(+), 8 deletions(-)
 create mode 100644 tools/migration/node.2018-03-28.appname.js
 create mode 100644 tools/migration/node.2018-03-28.username.js

diff --git a/locales/ja.yml b/locales/ja.yml
index f826b1b6ca..fd140ecc30 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -147,7 +147,7 @@ common:
       available: "利用できます"
       unavailable: "既に利用されています"
       error: "通信エラー"
-      invalid-format: "a~z、A~Z、0~9、-(ハイフン)が使えます"
+      invalid-format: "a~z、A~Z、0~9、_が使えます"
       too-short: "3文字以上でお願いします!"
       too-long: "20文字以内でお願いします"
       password: "パスワード"
diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 77a68aaee6..d6706e9a1c 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -67,7 +67,7 @@ export default class BotCore extends EventEmitter {
 			return await this.context.q(query);
 		}
 
-		if (/^@[a-zA-Z0-9-]+$/.test(query)) {
+		if (/^@[a-zA-Z0-9_]+$/.test(query)) {
 			return await this.showUserCommand(query);
 		}
 
diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index 2434684085..273143262b 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -1,7 +1,7 @@
 <template>
 <form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
 	<label class="user-name">
-		<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus required @change="onUsernameChange"/>%fa:at%
+		<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus required @change="onUsernameChange"/>%fa:at%
 	</label>
 	<label class="password">
 		<input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required/>%fa:lock%
diff --git a/src/web/app/common/views/components/signup.vue b/src/web/app/common/views/components/signup.vue
index c2e78aa8a3..e77d849ade 100644
--- a/src/web/app/common/views/components/signup.vue
+++ b/src/web/app/common/views/components/signup.vue
@@ -2,7 +2,7 @@
 <form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off">
 	<label class="username">
 		<p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p>
-		<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/>
+		<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/>
 		<p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/@${username}` }}</p>
 		<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p>
 		<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p>
diff --git a/src/web/app/common/views/directives/autocomplete.ts b/src/web/app/common/views/directives/autocomplete.ts
index 3440c4212a..94635d301a 100644
--- a/src/web/app/common/views/directives/autocomplete.ts
+++ b/src/web/app/common/views/directives/autocomplete.ts
@@ -77,7 +77,7 @@ class Autocomplete {
 
 		if (mentionIndex != -1 && mentionIndex > emojiIndex) {
 			const username = text.substr(mentionIndex + 1);
-			if (username != '' && username.match(/^[a-zA-Z0-9-]+$/)) {
+			if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) {
 				this.open('user', username);
 				opened = true;
 			}
diff --git a/src/web/app/dev/views/new-app.vue b/src/web/app/dev/views/new-app.vue
index 344e8468f9..1a796299cb 100644
--- a/src/web/app/dev/views/new-app.vue
+++ b/src/web/app/dev/views/new-app.vue
@@ -6,12 +6,12 @@
 				<b-form-input v-model="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required/>
 			</b-form-group>
 			<b-form-group label="ID" description="あなたのアプリのID。">
-				<b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9-]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
+				<b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9_]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
 				<p class="info" v-if="nidState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%確認しています...</p>
 				<p class="info" v-if="nidState == 'ok'" style="color:#3CB7B5">%fa:fw check%利用できます</p>
 				<p class="info" v-if="nidState == 'unavailable'" style="color:#FF1161">%fa:fw exclamation-triangle%既に利用されています</p>
 				<p class="info" v-if="nidState == 'error'" style="color:#FF1161">%fa:fw exclamation-triangle%通信エラー</p>
-				<p class="info" v-if="nidState == 'invalid-format'" style="color:#FF1161">%fa:fw exclamation-triangle%a~z、A~Z、0~9、-(ハイフン)が使えます</p>
+				<p class="info" v-if="nidState == 'invalid-format'" style="color:#FF1161">%fa:fw exclamation-triangle%a~z、A~Z、0~9、_が使えます</p>
 				<p class="info" v-if="nidState == 'min-range'" style="color:#FF1161">%fa:fw exclamation-triangle%3文字以上でお願いします!</p>
 				<p class="info" v-if="nidState == 'max-range'" style="color:#FF1161">%fa:fw exclamation-triangle%30文字以内でお願いします</p>
 			</b-form-group>
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
index 3384ee6997..8557448346 100644
--- a/src/web/app/mobile/views/pages/welcome.vue
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -6,7 +6,7 @@
 		<p>%fa:lock% ログイン</p>
 		<div>
 			<form @submit.prevent="onSubmit">
-				<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/>
+				<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/>
 				<input v-model="password" type="password" placeholder="パスワード" required/>
 				<input v-if="user && user.account.two_factor_enabled" v-model="token" type="number" placeholder="トークン" required/>
 				<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
diff --git a/tools/migration/node.2018-03-28.appname.js b/tools/migration/node.2018-03-28.appname.js
new file mode 100644
index 0000000000..9f16e47202
--- /dev/null
+++ b/tools/migration/node.2018-03-28.appname.js
@@ -0,0 +1,40 @@
+// for Node.js interpret
+
+const { default: App } = require('../../built/api/models/app');
+const { generate } = require('../../built/crypto_key');
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (app) => {
+	const result = await User.update(app._id, {
+		$set: {
+			'name_id': app.name_id.replace(/\-/g, '_'),
+			'name_id_lower': app.name_id_lower.replace(/\-/g, '_')
+		}
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await App.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await App.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)
diff --git a/tools/migration/node.2018-03-28.username.js b/tools/migration/node.2018-03-28.username.js
new file mode 100644
index 0000000000..2222152101
--- /dev/null
+++ b/tools/migration/node.2018-03-28.username.js
@@ -0,0 +1,40 @@
+// for Node.js interpret
+
+const { default: User } = require('../../built/api/models/user');
+const { generate } = require('../../built/crypto_key');
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (user) => {
+	const result = await User.update(user._id, {
+		$set: {
+			'username': user.username.replace(/\-/g, '_'),
+			'username_lower': user.username_lower.replace(/\-/g, '_')
+		}
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await User.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await User.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)