From 046976dffc1aa8bc02259ab4a65e74b1216a0ec3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 12 Sep 2018 02:48:19 +0900
Subject: [PATCH] Resolve #2691

---
 .../app/desktop/views/components/timeline.vue |  11 +-
 .../views/pages/admin/admin.dashboard.vue     |  35 +++--
 src/client/app/mobile/views/pages/home.vue    |  11 +-
 src/models/meta.ts                            |   1 +
 src/server/api/endpoints/admin/update-meta.ts |  10 ++
 src/server/api/endpoints/meta.ts              |   1 +
 src/stream.ts                                 | 135 +++++++++++-------
 7 files changed, 142 insertions(+), 62 deletions(-)

diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index 52a7753438..8d72016f22 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -2,8 +2,8 @@
 <div class="mk-timeline">
 	<header>
 		<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
-		<span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span>
-		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span>
+		<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
+		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
 		<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
 		<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
 		<button @click="chooseList" title="%i18n:@list%">%fa:list%</button>
@@ -29,7 +29,8 @@ export default Vue.extend({
 	data() {
 		return {
 			src: 'home',
-			list: null
+			list: null,
+			enableLocalTimeline: false
 		};
 	},
 
@@ -44,6 +45,10 @@ export default Vue.extend({
 	},
 
 	created() {
+		(this as any).os.getMeta().then(meta => {
+			this.enableLocalTimeline = !meta.disableLocalTimeline;
+		});
+
 		if (this.$store.state.device.tl) {
 			this.src = this.$store.state.device.tl.src;
 			if (this.src == 'list') {
diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
index ebb54d782e..c86c30db17 100644
--- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
@@ -1,22 +1,34 @@
 <template>
 <div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card">
 	<header>%i18n:@dashboard%</header>
+
 	<div v-if="stats" class="stats">
 		<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
 		<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
 		<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
 		<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
 	</div>
+
 	<div class="cpu-memory">
 		<x-cpu-memory :connection="connection"/>
 	</div>
-	<div>
-		<label>
-			<input type="checkbox" v-model="disableRegistration" @change="updateMeta">
-			<span>disableRegistration</span>
-		</label>
-		<button class="ui" @click="invite">%i18n:@invite%</button>
-		<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
+
+	<div class="form">
+		<div>
+			<label>
+				<input type="checkbox" v-model="disableRegistration" @change="updateMeta">
+				<span>%i18n:@disableRegistration%</span>
+			</label>
+			<button class="ui" @click="invite">%i18n:@invite%</button>
+			<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
+		</div>
+
+		<div>
+			<label>
+				<input type="checkbox" v-model="disableLocalTimeline" @change="updateMeta">
+				<span>%i18n:@disableLocalTimeline%</span>
+			</label>
+		</div>
 	</div>
 </div>
 </template>
@@ -33,6 +45,7 @@ export default Vue.extend({
 		return {
 			stats: null,
 			disableRegistration: false,
+			disableLocalTimeline: false,
 			inviteCode: null,
 			connection: null,
 			connectionId: null
@@ -44,6 +57,7 @@ export default Vue.extend({
 
 		(this as any).os.getMeta().then(meta => {
 			this.disableRegistration = meta.disableRegistration;
+			this.disableLocalTimeline = meta.disableLocalTimeline;
 		});
 
 		(this as any).api('stats').then(stats => {
@@ -61,7 +75,8 @@ export default Vue.extend({
 		},
 		updateMeta() {
 			(this as any).api('admin/update-meta', {
-				disableRegistration: this.disableRegistration
+				disableRegistration: this.disableRegistration,
+				disableLocalTimeline: this.disableLocalTimeline
 			});
 		}
 	}
@@ -97,4 +112,8 @@ export default Vue.extend({
 		border solid 1px #eee
 		border-radius: 8px
 
+	> .form
+		> div
+			border-bottom solid 1px #eee
+
 </style>
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index 706c9cd28b..333ca1a7a1 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -24,8 +24,8 @@
 			<div class="body">
 				<div>
 					<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
-					<span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span>
-					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span>
+					<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
+					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
 					<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
 					<template v-if="lists">
 						<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
@@ -60,7 +60,8 @@ export default Vue.extend({
 			src: 'home',
 			list: null,
 			lists: null,
-			showNav: false
+			showNav: false,
+			enableLocalTimeline: false
 		};
 	},
 
@@ -85,6 +86,10 @@ export default Vue.extend({
 	},
 
 	created() {
+		(this as any).os.getMeta().then(meta => {
+			this.enableLocalTimeline = !meta.disableLocalTimeline;
+		});
+
 		if (this.$store.state.device.tl) {
 			this.src = this.$store.state.device.tl.src;
 			if (this.src == 'list') {
diff --git a/src/models/meta.ts b/src/models/meta.ts
index 4f1977f3b5..8ca68416f8 100644
--- a/src/models/meta.ts
+++ b/src/models/meta.ts
@@ -12,5 +12,6 @@ export type IMeta = {
 		originalUsersCount: number;
 	};
 	disableRegistration?: boolean;
+	disableLocalTimeline?: boolean;
 	hidedTags?: string[];
 };
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index f903628774..3f5cd56b2f 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -23,6 +23,12 @@ export const meta = {
 			}
 		}),
 
+		disableLocalTimeline: $.bool.optional.nullable.note({
+			desc: {
+				'ja-JP': 'ローカルタイムライン(とソーシャルタイムライン)を無効にするか否か'
+			}
+		}),
+
 		hidedTags: $.arr($.str).optional.nullable.note({
 			desc: {
 				'ja-JP': '統計などで無視するハッシュタグ'
@@ -45,6 +51,10 @@ export default (params: any) => new Promise(async (res, rej) => {
 		set.disableRegistration = ps.disableRegistration;
 	}
 
+	if (typeof ps.disableLocalTimeline === 'boolean') {
+		set.disableLocalTimeline = ps.disableLocalTimeline;
+	}
+
 	if (Array.isArray(ps.hidedTags)) {
 		set.hidedTags = ps.hidedTags;
 	}
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 4472d8d779..18b0882f76 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -34,6 +34,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 		},
 		broadcasts: meta.broadcasts,
 		disableRegistration: meta.disableRegistration,
+		disableLocalTimeline: meta.disableLocalTimeline,
 		driveCapacityPerLocalUserMb: config.localDriveCapacityMb,
 		recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null,
 		swPublickey: config.sw ? config.sw.public_key : null,
diff --git a/src/stream.ts b/src/stream.ts
index 38a640c5da..ebc75c875c 100644
--- a/src/stream.ts
+++ b/src/stream.ts
@@ -1,58 +1,97 @@
 import * as mongo from 'mongodb';
 import Xev from 'xev';
-
-const ev = new Xev();
+import Meta, { IMeta } from './models/meta';
 
 type ID = string | mongo.ObjectID;
 
-function publish(channel: string, type: string, value?: any): void {
-	const message = type == null ? value : value == null ?
-		{ type: type } :
-		{ type: type, body: value };
+class Publisher {
+	private ev: Xev;
+	private meta: IMeta;
 
-	ev.emit(channel, message);
+	constructor() {
+		this.ev = new Xev();
+
+		setInterval(async () => {
+			this.meta = await Meta.findOne({});
+		}, 5000);
+	}
+
+	public getMeta = async () => {
+		if (this.meta != null) return this.meta;
+
+		this.meta = await Meta.findOne({});
+		return this.meta;
+	}
+
+	private publish = (channel: string, type: string, value?: any): void => {
+		const message = type == null ? value : value == null ?
+			{ type: type } :
+			{ type: type, body: value };
+
+		this.ev.emit(channel, message);
+	}
+
+	public publishUserStream = (userId: ID, type: string, value?: any): void => {
+		this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishDriveStream = (userId: ID, type: string, value?: any): void => {
+		this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishNoteStream = (noteId: ID, type: string): void => {
+		this.publish(`note-stream:${noteId}`, null, noteId);
+	}
+
+	public publishUserListStream = (listId: ID, type: string, value?: any): void => {
+		this.publish(`user-list-stream:${listId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishMessagingStream = (userId: ID, otherpartyId: ID, type: string, value?: any): void => {
+		this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishMessagingIndexStream = (userId: ID, type: string, value?: any): void => {
+		this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishReversiStream = (userId: ID, type: string, value?: any): void => {
+		this.publish(`reversi-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishReversiGameStream = (gameId: ID, type: string, value?: any): void => {
+		this.publish(`reversi-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishLocalTimelineStream = async (note: any): Promise<void> => {
+		const meta = await this.getMeta();
+		if (meta.disableLocalTimeline) return;
+		this.publish('local-timeline', null, note);
+	}
+
+	public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => {
+		const meta = await this.getMeta();
+		if (meta.disableLocalTimeline) return;
+		this.publish(userId ? `hybrid-timeline:${userId}` : 'hybrid-timeline', null, note);
+	}
+
+	public publishGlobalTimelineStream = (note: any): void => {
+		this.publish('global-timeline', null, note);
+	}
 }
 
-export function publishUserStream(userId: ID, type: string, value?: any): void {
-	publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
-}
+const publisher = new Publisher();
 
-export function publishDriveStream(userId: ID, type: string, value?: any): void {
-	publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
-}
+export default publisher;
 
-export function publishNoteStream(noteId: ID, type: string): void {
-	publish(`note-stream:${noteId}`, null, noteId);
-}
-
-export function publishUserListStream(listId: ID, type: string, value?: any): void {
-	publish(`user-list-stream:${listId}`, type, typeof value === 'undefined' ? null : value);
-}
-
-export function publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: any): void {
-	publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
-}
-
-export function publishMessagingIndexStream(userId: ID, type: string, value?: any): void {
-	publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
-}
-
-export function publishReversiStream(userId: ID, type: string, value?: any): void {
-	publish(`reversi-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
-}
-
-export function publishReversiGameStream(gameId: ID, type: string, value?: any): void {
-	publish(`reversi-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value);
-}
-
-export function publishLocalTimelineStream(note: any): void {
-	publish('local-timeline', null, note);
-}
-
-export function publishHybridTimelineStream(userId: ID, note: any): void {
-	publish(userId ? `hybrid-timeline:${userId}` : 'hybrid-timeline', null, note);
-}
-
-export function publishGlobalTimelineStream(note: any): void {
-	publish('global-timeline', null, note);
-}
+export const publishUserStream = publisher.publishUserStream;
+export const publishDriveStream = publisher.publishDriveStream;
+export const publishNoteStream = publisher.publishNoteStream;
+export const publishUserListStream = publisher.publishUserListStream;
+export const publishMessagingStream = publisher.publishMessagingStream;
+export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
+export const publishReversiStream = publisher.publishReversiStream;
+export const publishReversiGameStream = publisher.publishReversiGameStream;
+export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
+export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
+export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;