From e3ec0ad97e988467301488624f3c74aeec26b477 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Nov 2018 10:40:01 +0900
Subject: [PATCH 1/7] [Client] Improve admin panel usability

---
 locales/ja-JP.yml                            |  3 ++
 src/client/app/admin/views/announcements.vue | 29 ++++++++++++++------
 2 files changed, 24 insertions(+), 8 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 4d87859d88..2650d3c21c 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1151,6 +1151,9 @@ admin/views/announcements.vue:
   title: "タイトル"
   text: "内容"
   saved: "保存しました"
+  _remove:
+    are-you-sure: "「$1」を削除しますか?"
+    removed: "削除しました"
 
 admin/views/hashtags.vue:
   hided-tags: "Hidden Tags"
diff --git a/src/client/app/admin/views/announcements.vue b/src/client/app/admin/views/announcements.vue
index 926426844d..bd99e1bc0d 100644
--- a/src/client/app/admin/views/announcements.vue
+++ b/src/client/app/admin/views/announcements.vue
@@ -10,7 +10,7 @@
 				<span>%i18n:@text%</span>
 			</ui-textarea>
 			<ui-horizon-group>
-				<ui-button @click="save">%fa:save R% %i18n:@save%</ui-button>
+				<ui-button @click="save()">%fa:save R% %i18n:@save%</ui-button>
 				<ui-button @click="remove(i)">%fa:trash-alt R% %i18n:@remove%</ui-button>
 			</ui-horizon-group>
 		</section>
@@ -46,18 +46,31 @@ export default Vue.extend({
 		},
 
 		remove(i) {
-			this.announcements = this.announcements.filter((_, j) => j !== i);
-			this.save();
+			this.$swal({
+				type: 'warning',
+				text: '%i18n:@_remove.are-you-sure%'.replace('$1', this.announcements.find((_, j) => j == i).title),
+				showCancelButton: true
+			}).then(res => {
+				if (!res.value) return;
+				this.announcements = this.announcements.filter((_, j) => j !== i);
+				this.save(true);
+				this.$swal({
+					type: 'success',
+					text: '%i18n:@_remove.removed%'
+				});
+			});
 		},
 
-		save() {
+		save(silent) {
 			(this as any).api('admin/update-meta', {
 				broadcasts: this.announcements
 			}).then(() => {
-				this.$swal({
-					type: 'success',
-					text: '%i18n:@saved%'
-				});
+				if (!silent) {
+					this.$swal({
+						type: 'success',
+						text: '%i18n:@saved%'
+					});
+				}
 			}).catch(e => {
 				this.$swal({
 					type: 'error',

From a1a3ee44b5c9a3254a33a1948aa79df6dd4dbb0d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Mon, 5 Nov 2018 10:45:57 +0900
Subject: [PATCH 2/7] Implement /api/v1/custom_emojis (#3116)

---
 src/server/api/mastodon.ts | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/server/api/mastodon.ts b/src/server/api/mastodon.ts
index a5b59e2e0f..33a0b4b5f1 100644
--- a/src/server/api/mastodon.ts
+++ b/src/server/api/mastodon.ts
@@ -4,12 +4,18 @@ import { toASCII } from 'punycode';
 import config from '../../config';
 import Meta from '../../models/meta';
 import { ObjectID } from 'bson';
+import Emoji from '../../models/emoji';
 const pkg = require('../../../package.json');
 
 // Init router
 const router = new Router();
 
-router.get('/v1/custom_emojis', async ctx => ctx.body = {});
+router.get('/v1/custom_emojis', async ctx => ctx.body =
+	await Emoji.find({ host: null }, {
+		fields: {
+			_id: false
+		}
+	}));
 
 router.get('/v1/instance', async ctx => { // TODO: This is a temporary implementation. Consider creating helper methods!
 	const meta = await Meta.findOne() || {};

From cdd123dfd38477b806102830787352f5c7cb7dc9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Nov 2018 10:48:40 +0900
Subject: [PATCH 3/7] [doc] specify node version

---
 docs/setup.en.md | 2 +-
 docs/setup.ja.md | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/setup.en.md b/docs/setup.en.md
index bb26beb4bc..72da57a9aa 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -22,7 +22,7 @@ adduser --disabled-password --disabled-login misskey
 Please install and setup these softwares:
 
 #### Dependencies :package:
-* **[Node.js](https://nodejs.org/en/)**
+* **[Node.js](https://nodejs.org/en/)** >= 10.0.0
 * **[MongoDB](https://www.mongodb.com/)** >= 3.6
 
 ##### Optional
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index c3a24032ff..606857219a 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -22,7 +22,7 @@ adduser --disabled-password --disabled-login misskey
 これらのソフトウェアをインストール・設定してください:
 
 #### 依存関係 :package:
-* **[Node.js](https://nodejs.org/en/)**
+* **[Node.js](https://nodejs.org/en/)** (10.0.0以上)
 * **[MongoDB](https://www.mongodb.com/)** (3.6以上)
 
 ##### オプション

From 10c434f24a669cf5ae987ac409446c7ade343f78 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Nov 2018 10:52:07 +0900
Subject: [PATCH 4/7] Remove Travis

Closes #3109
---
 .travis.yml     | 41 -----------------------------------------
 CONTRIBUTING.md |  4 ++--
 README.md       |  3 ---
 3 files changed, 2 insertions(+), 46 deletions(-)
 delete mode 100644 .travis.yml

diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 80ecb236c0..0000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-# travis file
-# https://docs.travis-ci.com/user/customizing-the-build
-
-notifications:
-  email: false
-
-branches:
-  except:
-    - l10n_master
-
-language: node_js
-
-node_js:
-  - 11.0.0
-
-env:
-  - CXX=g++-4.8 NODE_ENV=production
-
-addons:
-  apt:
-    sources:
-      - ubuntu-toolchain-r-test
-    packages:
-      - g++-4.8
-
-cache:
-  directories:
-    - node_modules
-
-services:
-  - mongodb
-  - redis-server
-
-before_script:
-  - npm install
-
-  # 設定ファイルを配置
-  - cp ./.ci/default.yml ./.config
-  - cp ./.ci/test.yml ./.config
-
-  - travis_wait npm run build
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2fa78d1934..80fe89c11b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,5 +23,5 @@ Please use [Crowdin](https://crowdin.com/project/misskey) for localization.
 * Test codes are located in `/test`.
 
 ## Continuous integration
-Misskey uses Travis for automated test.
-Configuration files are located in `/.travis`.
+Misskey uses CircleCI for automated test.
+Configuration files are located in `/.circleci`.
diff --git a/README.md b/README.md
index 913cc6153a..29c9cb54b4 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@
 ================================================================
 
 [![CircleCI](https://circleci.com/gh/syuilo/misskey.svg?style=svg)](https://circleci.com/gh/syuilo/misskey)
-[![][travis-badge]][travis-link]
 [![][dependencies-badge]][dependencies-link]
 [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
 
@@ -124,8 +123,6 @@ Misskey is an open-source software licensed under the [GNU AGPLv3](LICENSE).
 
 [agpl-3.0]:           https://www.gnu.org/licenses/agpl-3.0.en.html
 [agpl-3.0-badge]:     https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square
-[travis-link]:        https://travis-ci.org/syuilo/misskey
-[travis-badge]:       http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
 [dependencies-link]:  https://david-dm.org/syuilo/misskey
 [dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square
 

From d7a3b710281f19746aaccaf23c07feb700503f39 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Nov 2018 11:09:05 +0900
Subject: [PATCH 5/7] =?UTF-8?q?=E6=8A=95=E7=A8=BF=E3=81=AE=E6=9C=80?=
 =?UTF-8?q?=E5=A4=A7=E6=96=87=E5=AD=97=E6=95=B0=E6=83=85=E5=A0=B1=E3=82=92?=
 =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=A7?=
 =?UTF-8?q?=E3=81=AF=E3=81=AA=E3=81=8FDB=E3=81=AB=E4=BF=9D=E5=AD=98?=
 =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .config/example.yml                           |  3 ---
 locales/ja-JP.yml                             |  1 +
 src/client/app/admin/views/instance.vue       |  6 +++++-
 src/config/load.ts                            |  2 --
 src/config/types.ts                           |  2 --
 src/models/meta.ts                            |  5 +++++
 src/models/note.ts                            |  5 -----
 src/server/api/endpoints/admin/update-meta.ts | 11 +++++++++++
 src/server/api/endpoints/meta.ts              |  2 +-
 src/server/api/endpoints/notes/create.ts      | 16 ++++++++++++++--
 10 files changed, 37 insertions(+), 16 deletions(-)

diff --git a/.config/example.yml b/.config/example.yml
index c0afcc4a6e..0265c8cc2a 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -167,6 +167,3 @@ drive:
 #  external: true
 #  engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
 #  timeout: 300000
-
-# Max allowed note text length in charactors
-maxNoteTextLength: 1000
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 2650d3c21c..577b68365d 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1077,6 +1077,7 @@ admin/views/instance.vue:
   instance-name: "インスタンス名"
   instance-description: "インスタンスの紹介"
   banner-url: "バナー画像URL"
+  max-note-text-length: "投稿の最大文字数"
   disableRegistration: "ユーザー登録の受付を停止する"
   disableLocalTimeline: "ローカルタイムラインを無効にする"
   invite: "招待"
diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue
index 85ef0a60c4..703d0f622b 100644
--- a/src/client/app/admin/views/instance.vue
+++ b/src/client/app/admin/views/instance.vue
@@ -6,6 +6,7 @@
 			<ui-input v-model="name">%i18n:@instance-name%</ui-input>
 			<ui-textarea v-model="description">%i18n:@instance-description%</ui-textarea>
 			<ui-input v-model="bannerUrl">%i18n:@banner-url%</ui-input>
+			<ui-input v-model="maxNoteTextLength">%i18n:@max-note-text-length%</ui-input>
 			<ui-button @click="updateMeta">%i18n:@save%</ui-button>
 		</section>
 	</ui-card>
@@ -39,6 +40,7 @@ export default Vue.extend({
 			bannerUrl: null,
 			name: null,
 			description: null,
+			maxNoteTextLength: null,
 			inviteCode: null,
 		};
 	},
@@ -48,6 +50,7 @@ export default Vue.extend({
 			this.bannerUrl = meta.bannerUrl;
 			this.name = meta.name;
 			this.description = meta.description;
+			this.maxNoteTextLength = meta.maxNoteTextLength;
 		});
 	},
 
@@ -69,7 +72,8 @@ export default Vue.extend({
 				disableLocalTimeline: this.disableLocalTimeline,
 				bannerUrl: this.bannerUrl,
 				name: this.name,
-				description: this.description
+				description: this.description,
+				maxNoteTextLength: parseInt(this.maxNoteTextLength, 10)
 			}).then(() => {
 				this.$swal({
 					type: 'success',
diff --git a/src/config/load.ts b/src/config/load.ts
index 5be52c0ee7..738d6427f3 100644
--- a/src/config/load.ts
+++ b/src/config/load.ts
@@ -49,8 +49,6 @@ export default function load() {
 	if (config.localDriveCapacityMb == null) config.localDriveCapacityMb = 256;
 	if (config.remoteDriveCapacityMb == null) config.remoteDriveCapacityMb = 8;
 
-	if (config.maxNoteTextLength == null) config.maxNoteTextLength = 1000;
-
 	return Object.assign(config, mixin);
 }
 
diff --git a/src/config/types.ts b/src/config/types.ts
index ce4ab3ec38..a3c761cc64 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -107,8 +107,6 @@ export type Source = {
 		engine: string;
 		timeout: number;
 	};
-
-	maxNoteTextLength?: number;
 };
 
 /**
diff --git a/src/models/meta.ts b/src/models/meta.ts
index 6d75258df8..d8a9b46037 100644
--- a/src/models/meta.ts
+++ b/src/models/meta.ts
@@ -43,4 +43,9 @@ export type IMeta = {
 	disableLocalTimeline?: boolean;
 	hidedTags?: string[];
 	bannerUrl?: string;
+
+	/**
+	 * Max allowed note text length in charactors
+	 */
+	maxNoteTextLength?: number;
 };
diff --git a/src/models/note.ts b/src/models/note.ts
index 6856d6d07d..516045225c 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -11,7 +11,6 @@ import Reaction from './note-reaction';
 import { packMany as packFileMany, IDriveFile } from './drive-file';
 import Favorite from './favorite';
 import Following from './following';
-import config from '../config';
 import Emoji from './emoji';
 
 const Note = db.get<INote>('notes');
@@ -27,10 +26,6 @@ Note.createIndex({ createdAt: -1 });
 Note.createIndex({ score: -1 }, { sparse: true });
 export default Note;
 
-export function isValidText(text: string): boolean {
-	return length(text.trim()) <= config.maxNoteTextLength && text.trim() != '';
-}
-
 export function isValidCw(text: string): boolean {
 	return length(text.trim()) <= 100;
 }
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index 26ade439ab..a0f2b329aa 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -59,6 +59,13 @@ export const meta = {
 				'ja-JP': 'インスタンスの紹介文'
 			}
 		},
+
+		maxNoteTextLength: {
+			validator: $.num.optional.min(1),
+			desc: {
+				'ja-JP': '投稿の最大文字数'
+			}
+		}
 	}
 };
 
@@ -93,6 +100,10 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
 		set.description = ps.description;
 	}
 
+	if (ps.maxNoteTextLength) {
+		set.maxNoteTextLength = ps.maxNoteTextLength;
+	}
+
 	await Meta.update({}, {
 		$set: set
 	}, { upsert: true });
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 34a62d6452..90a5952e9f 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -62,7 +62,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 		swPublickey: config.sw ? config.sw.public_key : null,
 		hidedTags: (me && me.isAdmin) ? met.hidedTags : undefined,
 		bannerUrl: met.bannerUrl,
-		maxNoteTextLength: config.maxNoteTextLength,
+		maxNoteTextLength: met.maxNoteTextLength || 1000,
 
 		emojis: emojis,
 
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index 8a8813daba..f4d7e96265 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -1,10 +1,20 @@
 import $ from 'cafy'; import ID, { transform, transformMany } from '../../../../misc/cafy-id';
 const ms = require('ms');
-import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
+import { length } from 'stringz';
+import Note, { INote, isValidCw, pack } from '../../../../models/note';
 import User, { IUser } from '../../../../models/user';
 import DriveFile, { IDriveFile } from '../../../../models/drive-file';
 import create from '../../../../services/note/create';
 import define from '../../define';
+import Meta from '../../../../models/meta';
+
+let maxNoteTextLength = 1000;
+
+setInterval(() => {
+	Meta.findOne({}).then(m => {
+		if (m.maxNoteTextLength) maxNoteTextLength = m.maxNoteTextLength;
+	});
+}, 3000);
 
 export const meta = {
 	stability: 'stable',
@@ -40,7 +50,9 @@ export const meta = {
 		},
 
 		text: {
-			validator: $.str.optional.nullable.pipe(isValidText),
+			validator: $.str.optional.nullable.pipe(text =>
+				length(text.trim()) <= maxNoteTextLength && text.trim() != ''
+			),
 			default: null as any,
 			desc: {
 				'ja-JP': '投稿内容'

From abe99c3c7334859ea966412fd6c38ff2069965f0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Nov 2018 11:10:02 +0900
Subject: [PATCH 6/7] Update locales/ja-JP.yml

---
 locales/ja-JP.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 577b68365d..10d566fc34 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1078,8 +1078,8 @@ admin/views/instance.vue:
   instance-description: "インスタンスの紹介"
   banner-url: "バナー画像URL"
   max-note-text-length: "投稿の最大文字数"
-  disableRegistration: "ユーザー登録の受付を停止する"
-  disableLocalTimeline: "ローカルタイムラインを無効にする"
+  disable-registration: "ユーザー登録の受付を停止する"
+  disable-local-timeline: "ローカルタイムラインを無効にする"
   invite: "招待"
   save: "保存"
   saved: "保存しました"

From 712802e6820bed9ab17fadbf6e1ed65d0f31216b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Nov 2018 11:11:23 +0900
Subject: [PATCH 7/7] 10.38.7

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 695fa8d607..9a97091384 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "10.38.6",
-	"clientVersion": "1.0.11516",
+	"version": "10.38.7",
+	"clientVersion": "1.0.11530",
 	"codename": "nighthike",
 	"main": "./built/index.js",
 	"private": true,