From 8b91502ab5734d9b632e5329e50061f08a6312bd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 16 Nov 2017 03:06:52 +0900
Subject: [PATCH] #919

---
 package.json                                  |   2 +
 src/web/app/auth/script.ts                    |   2 +-
 src/web/app/ch/router.ts                      |   2 +-
 src/web/app/ch/script.ts                      |   4 +-
 src/web/app/common/mios.ts                    | 181 ++++++++++++++++++
 src/web/app/common/mixins.ts                  |  39 ++++
 src/web/app/common/mixins/api.ts              |   8 -
 src/web/app/common/mixins/i.ts                |  20 --
 src/web/app/common/mixins/index.ts            |  18 --
 src/web/app/common/scripts/api.ts             |   2 +-
 .../app/common/scripts/check-for-update.ts    |  20 +-
 src/web/app/desktop/router.ts                 |   5 +-
 src/web/app/desktop/script.ts                 |   7 +-
 .../app/desktop/tags/home-widgets/server.tag  |   4 +-
 src/web/app/dev/router.ts                     |   2 +-
 src/web/app/dev/script.ts                     |   4 +-
 src/web/app/init.ts                           |  98 ++--------
 src/web/app/mobile/router.ts                  |   5 +-
 src/web/app/mobile/script.ts                  |   5 +-
 src/web/app/stats/script.ts                   |   2 +-
 src/web/app/status/script.ts                  |   2 +-
 21 files changed, 267 insertions(+), 165 deletions(-)
 create mode 100644 src/web/app/common/mios.ts
 create mode 100644 src/web/app/common/mixins.ts
 delete mode 100644 src/web/app/common/mixins/api.ts
 delete mode 100644 src/web/app/common/mixins/i.ts
 delete mode 100644 src/web/app/common/mixins/index.ts

diff --git a/package.json b/package.json
index 7a8d57aed..8cb457e8a 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
 		"@types/deep-equal": "1.0.1",
 		"@types/elasticsearch": "5.0.17",
 		"@types/event-stream": "3.3.32",
+		"@types/eventemitter3": "^2.0.2",
 		"@types/express": "4.0.37",
 		"@types/gm": "1.17.33",
 		"@types/gulp": "4.0.3",
@@ -114,6 +115,7 @@
 		"diskusage": "0.2.2",
 		"elasticsearch": "13.3.1",
 		"escape-regexp": "0.0.1",
+		"eventemitter3": "^2.0.3",
 		"express": "4.15.4",
 		"file-type": "7.2.0",
 		"fuckadblock": "3.2.1",
diff --git a/src/web/app/auth/script.ts b/src/web/app/auth/script.ts
index fe7f9befe..dd598d1ed 100644
--- a/src/web/app/auth/script.ts
+++ b/src/web/app/auth/script.ts
@@ -14,7 +14,7 @@ document.title = 'Misskey | アプリの連携';
 /**
  * init
  */
-init(me => {
+init(() => {
 	mount(document.createElement('mk-index'));
 });
 
diff --git a/src/web/app/ch/router.ts b/src/web/app/ch/router.ts
index fe014d4e3..f10c4acdf 100644
--- a/src/web/app/ch/router.ts
+++ b/src/web/app/ch/router.ts
@@ -2,7 +2,7 @@ import * as riot from 'riot';
 import * as route from 'page';
 let page = null;
 
-export default me => {
+export default () => {
 	route('/',         index);
 	route('/:channel', channel);
 	route('*',         notFound);
diff --git a/src/web/app/ch/script.ts b/src/web/app/ch/script.ts
index 760d405c5..e23558037 100644
--- a/src/web/app/ch/script.ts
+++ b/src/web/app/ch/script.ts
@@ -12,7 +12,7 @@ import route from './router';
 /**
  * init
  */
-init(me => {
+init(() => {
 	// Start routing
-	route(me);
+	route();
 });
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
new file mode 100644
index 000000000..36c851ac6
--- /dev/null
+++ b/src/web/app/common/mios.ts
@@ -0,0 +1,181 @@
+import { EventEmitter } from 'eventemitter3';
+import * as riot from 'riot';
+import signout from './scripts/signout';
+import Progress from './scripts/loading';
+import Connection from './scripts/home-stream';
+import CONFIG from './scripts/config';
+import api from './scripts/api';
+
+/**
+ * Misskey Operating System
+ */
+export default class MiOS extends EventEmitter {
+	/**
+	 * Misskeyの /meta で取得できるメタ情報
+	 */
+	private meta: {
+		data: { [x: string]: any };
+		chachedAt: Date;
+	};
+
+	private isMetaFetching = false;
+
+	/**
+	 * A signing user
+	 */
+	public i: any;
+
+	/**
+	 * Whether signed in
+	 */
+	public get isSignedin() {
+		return this.i != null;
+	}
+
+	/**
+	 * A connection of home stream
+	 */
+	public stream: Connection;
+
+	constructor() {
+		super();
+
+		//#region BIND
+		this.init = this.init.bind(this);
+		this.api = this.api.bind(this);
+		this.getMeta = this.getMeta.bind(this);
+		//#endregion
+	}
+
+	/**
+	 * Initialize MiOS (boot)
+	 * @param callback A function that call when initialized
+	 */
+	public async init(callback) {
+		// ユーザーをフェッチしてコールバックする
+		const fetchme = (token, cb) => {
+			let me = null;
+
+			// Return when not signed in
+			if (token == null) {
+				return done();
+			}
+
+			// Fetch user
+			fetch(`${CONFIG.apiUrl}/i`, {
+				method: 'POST',
+				body: JSON.stringify({
+					i: token
+				})
+			}).then(res => { // When success
+				// When failed to authenticate user
+				if (res.status !== 200) {
+					return signout();
+				}
+
+				res.json().then(i => {
+					me = i;
+					me.token = token;
+					done();
+				});
+			}, () => { // When failure
+				// Render the error screen
+				document.body.innerHTML = '<mk-error />';
+				riot.mount('*');
+				Progress.done();
+			});
+
+			function done() {
+				if (cb) cb(me);
+			}
+		};
+
+		// フェッチが完了したとき
+		const fetched = me => {
+			if (me) {
+				riot.observable(me);
+
+				// この me オブジェクトを更新するメソッド
+				me.update = data => {
+					if (data) Object.assign(me, data);
+					me.trigger('updated');
+				};
+
+				// ローカルストレージにキャッシュ
+				localStorage.setItem('me', JSON.stringify(me));
+
+				me.on('updated', () => {
+					// キャッシュ更新
+					localStorage.setItem('me', JSON.stringify(me));
+				});
+			}
+
+			this.i = me;
+
+			// Init home stream connection
+			this.stream = this.i ? new Connection(this.i) : null;
+
+			// Finish init
+			callback();
+		};
+
+		// Get cached account data
+		const cachedMe = JSON.parse(localStorage.getItem('me'));
+
+		if (cachedMe) {
+			fetched(cachedMe);
+
+			// 後から新鮮なデータをフェッチ
+			fetchme(cachedMe.token, freshData => {
+				Object.assign(cachedMe, freshData);
+				cachedMe.trigger('updated');
+			});
+		} else {
+			// Get token from cookie
+			const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1];
+
+			fetchme(i, fetched);
+		}
+	}
+
+	/**
+	 * Misskey APIにリクエストします
+	 * @param endpoint エンドポイント名
+	 * @param data パラメータ
+	 */
+	public api(endpoint: string, data?: { [x: string]: any }) {
+		return api(this.i, endpoint, data);
+	}
+
+	/**
+	 * Misskeyのメタ情報を取得します
+	 * @param force キャッシュを無視するか否か
+	 */
+	public getMeta(force = false) {
+		return new Promise<{ [x: string]: any }>(async (res, rej) => {
+			if (this.isMetaFetching) {
+				this.once('_meta_fetched_', () => {
+					res(this.meta.data);
+				});
+				return;
+			}
+
+			const expire = 1000 * 60; // 1min
+
+			// forceが有効, meta情報を保持していない or 期限切れ
+			if (force || this.meta == null || Date.now() - this.meta.chachedAt.getTime() > expire) {
+				this.isMetaFetching = true;
+				const meta = await this.api('meta');
+				this.meta = {
+					data: meta,
+					chachedAt: new Date()
+				};
+				this.isMetaFetching = false;
+				this.emit('_meta_fetched_');
+				res(meta);
+			} else {
+				res(this.meta.data);
+			}
+		});
+	}
+}
diff --git a/src/web/app/common/mixins.ts b/src/web/app/common/mixins.ts
new file mode 100644
index 000000000..b5eb1acc7
--- /dev/null
+++ b/src/web/app/common/mixins.ts
@@ -0,0 +1,39 @@
+import * as riot from 'riot';
+
+import MiOS from './mios';
+import ServerStreamManager from './scripts/server-stream-manager';
+import RequestsStreamManager from './scripts/requests-stream-manager';
+import MessagingIndexStream from './scripts/messaging-index-stream-manager';
+
+export default (mios: MiOS) => {
+	(riot as any).mixin('os', {
+		mios: mios
+	});
+
+	(riot as any).mixin('i', {
+		init: function() {
+			this.I = mios.i;
+			this.SIGNIN = mios.isSignedin;
+
+			if (this.SIGNIN) {
+				this.on('mount', () => {
+					mios.i.on('updated', this.update);
+				});
+				this.on('unmount', () => {
+					mios.i.off('updated', this.update);
+				});
+			}
+		},
+		me: mios.i
+	});
+
+	(riot as any).mixin('api', {
+		api: mios.api
+	});
+
+	(riot as any).mixin('stream', { stream: mios.stream });
+
+	(riot as any).mixin('server-stream', { serverStream: new ServerStreamManager() });
+	(riot as any).mixin('requests-stream', { requestsStream: new RequestsStreamManager() });
+	(riot as any).mixin('messaging-index-stream', { messagingIndexStream: new MessagingIndexStream(mios.i) });
+};
diff --git a/src/web/app/common/mixins/api.ts b/src/web/app/common/mixins/api.ts
deleted file mode 100644
index 9726caf51..000000000
--- a/src/web/app/common/mixins/api.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import * as riot from 'riot';
-import api from '../scripts/api';
-
-export default me => {
-	(riot as any).mixin('api', {
-		api: api.bind(null, me ? me.token : null)
-	});
-};
diff --git a/src/web/app/common/mixins/i.ts b/src/web/app/common/mixins/i.ts
deleted file mode 100644
index 0879d02d3..000000000
--- a/src/web/app/common/mixins/i.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as riot from 'riot';
-
-export default me => {
-	(riot as any).mixin('i', {
-		init: function() {
-			this.I = me;
-			this.SIGNIN = me != null;
-
-			if (this.SIGNIN) {
-				this.on('mount', () => {
-					me.on('updated', this.update);
-				});
-				this.on('unmount', () => {
-					me.off('updated', this.update);
-				});
-			}
-		},
-		me: me
-	});
-};
diff --git a/src/web/app/common/mixins/index.ts b/src/web/app/common/mixins/index.ts
deleted file mode 100644
index c0c1c0555..000000000
--- a/src/web/app/common/mixins/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as riot from 'riot';
-
-import activateMe from './i';
-import activateApi from './api';
-import ServerStreamManager from '../scripts/server-stream-manager';
-import RequestsStreamManager from '../scripts/requests-stream-manager';
-import MessagingIndexStream from '../scripts/messaging-index-stream-manager';
-
-export default (me, stream) => {
-	activateMe(me);
-	activateApi(me);
-
-	(riot as any).mixin('stream', { stream });
-
-	(riot as any).mixin('server-stream', { serverStream: new ServerStreamManager() });
-	(riot as any).mixin('requests-stream', { requestsStream: new RequestsStreamManager() });
-	(riot as any).mixin('messaging-index-stream', { messagingIndexStream: new MessagingIndexStream(me) });
-};
diff --git a/src/web/app/common/scripts/api.ts b/src/web/app/common/scripts/api.ts
index 2a9d78e87..5dcdb5971 100644
--- a/src/web/app/common/scripts/api.ts
+++ b/src/web/app/common/scripts/api.ts
@@ -14,7 +14,7 @@ let pending = 0;
  * @param  {any} [data={}] Data
  * @return {Promise<any>} Response
  */
-export default (i, endpoint, data = {}): Promise<any> => {
+export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => {
 	if (++pending === 1) {
 		spinner = document.createElement('div');
 		spinner.setAttribute('id', 'wait');
diff --git a/src/web/app/common/scripts/check-for-update.ts b/src/web/app/common/scripts/check-for-update.ts
index 99d8b5d05..c1398ba54 100644
--- a/src/web/app/common/scripts/check-for-update.ts
+++ b/src/web/app/common/scripts/check-for-update.ts
@@ -1,16 +1,12 @@
-import CONFIG from './config';
+import MiOS from '../mios';
 
 declare var VERSION: string;
 
-export default function() {
-	fetch(CONFIG.apiUrl + '/meta', {
-		method: 'POST'
-	}).then(res => {
-		res.json().then(meta => {
-			if (meta.version != VERSION) {
-				localStorage.setItem('should-refresh', 'true');
-				alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', VERSION));
-			}
-		});
-	});
+export default async function(mios: MiOS) {
+	const meta = await mios.getMeta();
+
+	if (meta.version != VERSION) {
+		localStorage.setItem('should-refresh', 'true');
+		alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', VERSION));
+	}
 }
diff --git a/src/web/app/desktop/router.ts b/src/web/app/desktop/router.ts
index a74299b28..27b63ab2e 100644
--- a/src/web/app/desktop/router.ts
+++ b/src/web/app/desktop/router.ts
@@ -4,9 +4,10 @@
 
 import * as riot from 'riot';
 import * as route from 'page';
+import MiOS from '../common/mios';
 let page = null;
 
-export default me => {
+export default (mios: MiOS) => {
 	route('/',                       index);
 	route('/selectdrive',            selectDrive);
 	route('/i/customize-home',       customizeHome);
@@ -22,7 +23,7 @@ export default me => {
 	route('*',                       notFound);
 
 	function index() {
-		me ? home() : entrance();
+		mios.isSignedin ? home() : entrance();
 	}
 
 	function home() {
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index a0453865e..b4a8d829d 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -12,11 +12,12 @@ import init from '../init';
 import route from './router';
 import fuckAdBlock from './scripts/fuck-ad-block';
 import getPostSummary from '../../../common/get-post-summary';
+import MiOS from '../common/mios';
 
 /**
  * init
  */
-init(async (me, stream) => {
+init(async (mios: MiOS) => {
 	/**
 	 * Fuck AD Block
 	 */
@@ -32,12 +33,12 @@ init(async (me, stream) => {
 		}
 
 		if ((Notification as any).permission == 'granted') {
-			registerNotifications(stream);
+			registerNotifications(mios.stream);
 		}
 	}
 
 	// Start routing
-	route(me);
+	route(mios);
 });
 
 function registerNotifications(stream) {
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag
index ce8c63c97..b37d34736 100644
--- a/src/web/app/desktop/tags/home-widgets/server.tag
+++ b/src/web/app/desktop/tags/home-widgets/server.tag
@@ -62,6 +62,8 @@
 
 	</style>
 	<script>
+		this.mixin('os');
+
 		this.data = {
 			view: 0,
 			design: 0
@@ -76,7 +78,7 @@
 		this.initializing = true;
 
 		this.on('mount', () => {
-			this.api('meta').then(meta => {
+			this.mios.getMeta().then(meta => {
 				this.update({
 					initializing: false,
 					meta
diff --git a/src/web/app/dev/router.ts b/src/web/app/dev/router.ts
index 532ec23c7..fcd2b1f76 100644
--- a/src/web/app/dev/router.ts
+++ b/src/web/app/dev/router.ts
@@ -2,7 +2,7 @@ import * as riot from 'riot';
 import * as route from 'page';
 let page = null;
 
-export default me => {
+export default () => {
 	route('/',         index);
 	route('/apps',     apps);
 	route('/app/new',  newApp);
diff --git a/src/web/app/dev/script.ts b/src/web/app/dev/script.ts
index 39d7fc891..b115c5be4 100644
--- a/src/web/app/dev/script.ts
+++ b/src/web/app/dev/script.ts
@@ -12,7 +12,7 @@ import route from './router';
 /**
  * init
  */
-init(me => {
+init(() => {
 	// Start routing
-	route(me);
+	route();
 });
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index e68a7c915..4deeab704 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -6,12 +6,10 @@ declare var VERSION: string;
 declare var LANG: string;
 
 import * as riot from 'riot';
-import signout from './common/scripts/signout';
 import checkForUpdate from './common/scripts/check-for-update';
-import Connection from './common/scripts/home-stream';
-import Progress from './common/scripts/loading';
 import mixin from './common/mixins';
 import CONFIG from './common/scripts/config';
+import MiOS from './common/mios';
 require('./common/tags');
 
 /**
@@ -51,54 +49,13 @@ if (localStorage.getItem('should-refresh') == 'true') {
 	location.reload(true);
 }
 
-// 更新チェック
-setTimeout(checkForUpdate, 3000);
-
-// ユーザーをフェッチしてコールバックする
+// MiOSを初期化してコールバックする
 export default callback => {
-	// Get cached account data
-	const cachedMe = JSON.parse(localStorage.getItem('me'));
-
-	if (cachedMe) {
-		fetched(cachedMe);
-
-		// 後から新鮮なデータをフェッチ
-		fetchme(cachedMe.token, freshData => {
-			Object.assign(cachedMe, freshData);
-			cachedMe.trigger('updated');
-		});
-	} else {
-		// Get token from cookie
-		const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1];
-
-		fetchme(i, fetched);
-	}
-
-	// フェッチが完了したとき
-	function fetched(me) {
-		if (me) {
-			riot.observable(me);
-
-			// この me オブジェクトを更新するメソッド
-			me.update = data => {
-				if (data) Object.assign(me, data);
-				me.trigger('updated');
-			};
-
-			// ローカルストレージにキャッシュ
-			localStorage.setItem('me', JSON.stringify(me));
-
-			me.on('updated', () => {
-				// キャッシュ更新
-				localStorage.setItem('me', JSON.stringify(me));
-			});
-		}
-
-		// Init home stream connection
-		const stream = me ? new Connection(me) : null;
+	const mios = new MiOS();
 
+	mios.init(() => {
 		// ミックスイン初期化
-		mixin(me, stream);
+		mixin(mios);
 
 		// ローディング画面クリア
 		const ini = document.getElementById('ini');
@@ -110,50 +67,17 @@ export default callback => {
 		document.body.appendChild(app);
 
 		try {
-			callback(me, stream);
+			callback(mios);
 		} catch (e) {
 			panic(e);
 		}
-	}
-};
 
-// ユーザーをフェッチしてコールバックする
-function fetchme(token, cb) {
-	let me = null;
-
-	// Return when not signed in
-	if (token == null) {
-		return done();
-	}
-
-	// Fetch user
-	fetch(`${CONFIG.apiUrl}/i`, {
-		method: 'POST',
-		body: JSON.stringify({
-			i: token
-		})
-	}).then(res => { // When success
-		// When failed to authenticate user
-		if (res.status !== 200) {
-			return signout();
-		}
-
-		res.json().then(i => {
-			me = i;
-			me.token = token;
-			done();
-		});
-	}, () => { // When failure
-		// Render the error screen
-		document.body.innerHTML = '<mk-error />';
-		riot.mount('*');
-		Progress.done();
+		// 更新チェック
+		setTimeout(() => {
+			checkForUpdate(mios);
+		}, 3000);
 	});
-
-	function done() {
-		if (cb) cb(me);
-	}
-}
+};
 
 // BSoD
 function panic(e) {
diff --git a/src/web/app/mobile/router.ts b/src/web/app/mobile/router.ts
index 7fae9db54..0358d10e9 100644
--- a/src/web/app/mobile/router.ts
+++ b/src/web/app/mobile/router.ts
@@ -4,9 +4,10 @@
 
 import * as riot from 'riot';
 import * as route from 'page';
+import MiOS from '../common/mios';
 let page = null;
 
-export default me => {
+export default (mios: MiOS) => {
 	route('/',                           index);
 	route('/selectdrive',                selectDrive);
 	route('/i/notifications',            notifications);
@@ -32,7 +33,7 @@ export default me => {
 	route('*',                           notFound);
 
 	function index() {
-		me ? home() : entrance();
+		mios.isSignedin ? home() : entrance();
 	}
 
 	function home() {
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 503e0fd67..74dfe3dfe 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -8,14 +8,15 @@ import './style.styl';
 require('./tags');
 import init from '../init';
 import route from './router';
+import MiOS from '../common/mios';
 
 /**
  * init
  */
-init(me => {
+init((mios: MiOS) => {
 	// http://qiita.com/junya/items/3ff380878f26ca447f85
 	document.body.setAttribute('ontouchstart', '');
 
 	// Start routing
-	route(me);
+	route(mios);
 });
diff --git a/src/web/app/stats/script.ts b/src/web/app/stats/script.ts
index 75063501b..3bbd80c33 100644
--- a/src/web/app/stats/script.ts
+++ b/src/web/app/stats/script.ts
@@ -14,7 +14,7 @@ document.title = 'Misskey Statistics';
 /**
  * init
  */
-init(me => {
+init(() => {
 	mount(document.createElement('mk-index'));
 });
 
diff --git a/src/web/app/status/script.ts b/src/web/app/status/script.ts
index 06d4d9a7a..84483acab 100644
--- a/src/web/app/status/script.ts
+++ b/src/web/app/status/script.ts
@@ -14,7 +14,7 @@ document.title = 'Misskey System Status';
 /**
  * init
  */
-init(me => {
+init(() => {
 	mount(document.createElement('mk-index'));
 });