From 3c4235067f2ff1f8057080482559a8015f0492c6 Mon Sep 17 00:00:00 2001
From: unarist <m.unarist@gmail.com>
Date: Mon, 9 Apr 2018 01:10:04 +0900
Subject: [PATCH] Fix username/mention regexes

* Allow underscore instead of hypen
* Fix domain part handling
* Add tests for remote mention
---
 src/client/app/common/views/components/signup.vue |  2 +-
 src/client/app/dev/views/new-app.vue              |  2 +-
 src/models/app.ts                                 |  2 +-
 src/models/user.ts                                |  2 +-
 src/text/parse/elements/mention.ts                |  2 +-
 test/text.ts                                      | 14 ++++++++++++--
 6 files changed, 17 insertions(+), 7 deletions(-)

diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index e77d849ade..8d0b16cabd 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -76,7 +76,7 @@ export default Vue.extend({
 			}
 
 			const err =
-				!this.username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
+				!this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
 				this.username.length < 3 ? 'min-range' :
 				this.username.length > 20 ? 'max-range' :
 				null;
diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue
index c9d5971395..2317f24d48 100644
--- a/src/client/app/dev/views/new-app.vue
+++ b/src/client/app/dev/views/new-app.vue
@@ -65,7 +65,7 @@ export default Vue.extend({
 			}
 
 			const err =
-				!this.nid.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
+				!this.nid.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
 				this.nid.length < 3                 ? 'min-range' :
 				this.nid.length > 30                ? 'max-range' :
 				null;
diff --git a/src/models/app.ts b/src/models/app.ts
index 83162a6b99..703f4ef8f5 100644
--- a/src/models/app.ts
+++ b/src/models/app.ts
@@ -24,7 +24,7 @@ export type IApp = {
 };
 
 export function isValidNameId(nameId: string): boolean {
-	return typeof nameId == 'string' && /^[a-zA-Z0-9\-]{3,30}$/.test(nameId);
+	return typeof nameId == 'string' && /^[a-zA-Z0-9_]{3,30}$/.test(nameId);
 }
 
 /**
diff --git a/src/models/user.ts b/src/models/user.ts
index ea59730e4d..36c63a56da 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -89,7 +89,7 @@ export const isRemoteUser = (user: any): user is IRemoteUser =>
 
 //#region Validators
 export function validateUsername(username: string): boolean {
-	return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username);
+	return typeof username == 'string' && /^[a-zA-Z0-9_]{3,20}$/.test(username);
 }
 
 export function validatePassword(password: string): boolean {
diff --git a/src/text/parse/elements/mention.ts b/src/text/parse/elements/mention.ts
index 4f2997b39f..2ad2788300 100644
--- a/src/text/parse/elements/mention.ts
+++ b/src/text/parse/elements/mention.ts
@@ -4,7 +4,7 @@
 import parseAcct from '../../../acct/parse';
 
 module.exports = text => {
-	const match = text.match(/^(?:@[a-zA-Z0-9\-]+){1,2}/);
+	const match = text.match(/^@[a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9])?/i);
 	if (!match) return null;
 	const mention = match[0];
 	const { username, host } = parseAcct(mention.substr(1));
diff --git a/test/text.ts b/test/text.ts
index 711aad8167..8ce55cd1bc 100644
--- a/test/text.ts
+++ b/test/text.ts
@@ -9,9 +9,11 @@ const syntaxhighlighter = require('../built/text/parse/core/syntax-highlighter')
 
 describe('Text', () => {
 	it('can be analyzed', () => {
-		const tokens = analyze('@himawari お腹ペコい :cat: #yryr');
+		const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
 		assert.deepEqual([
 			{ type: 'mention', content: '@himawari', username: 'himawari', host: null },
+			{ type: 'text', content: ' '},
+			{ type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
 			{ type: 'text', content: ' お腹ペコい ' },
 			{ type: 'emoji', content: ':cat:', emoji: 'cat'},
 			{ type: 'text', content: ' '},
@@ -20,7 +22,7 @@ describe('Text', () => {
 	});
 
 	it('can be inverted', () => {
-		const text = '@himawari お腹ペコい :cat: #yryr';
+		const text = '@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr';
 		assert.equal(analyze(text).map(x => x.content).join(''), text);
 	});
 
@@ -41,6 +43,14 @@ describe('Text', () => {
 			], tokens);
 		});
 
+		it('remote mention', () => {
+			const tokens = analyze('@hima_sub@namori.net お腹ペコい');
+			assert.deepEqual([
+				{ type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
+				{ type: 'text', content: ' お腹ペコい' }
+			], tokens);
+		});
+
 		it('hashtag', () => {
 			const tokens = analyze('Strawberry Pasta #alice');
 			assert.deepEqual([