V10 (#2826)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update CHANGELOG.md * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update CHANGELOG.md * Update CHANGELOG.md * wip * Update CHANGELOG.md * wip * wip * wip * wip
This commit is contained in:
		
							parent
							
								
									0b98a2364b
								
							
						
					
					
						commit
						d0570d7fe3
					
				
					 126 changed files with 1812 additions and 2273 deletions
				
			
		
							
								
								
									
										82
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										82
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -5,6 +5,88 @@ ChangeLog | |||
| 
 | ||||
| This document describes breaking changes only. | ||||
| 
 | ||||
| 10.0.0 | ||||
| ------ | ||||
| 
 | ||||
| ストリーミングAPIに破壊的変更があります。運営者がすべきことはありません。 | ||||
| 
 | ||||
| 変更は以下の通りです | ||||
| 
 | ||||
| * ストリーミングでやり取りする際の snake_case が全て camelCase に | ||||
| * リバーシのストリームエンドポイント名が reversi → gamesReversi、reversiGame → gamesReversiGame に | ||||
| * ストリーミングの個々のエンドポイントが廃止され、一旦元となるストリームに接続してから、個々のチャンネル(今までのエンドポイント)に接続します。詳細は後述します。 | ||||
| * ストリームから流れてくる、キャプチャした投稿の更新イベントに投稿自体のデータは含まれず、代わりにアクションが設定されるようになります。詳細は後述します。 | ||||
| * ストリームに接続する際に追加で指定していたパラメータ(トークン除く)が、URLにクエリとして含むのではなくチャンネル接続時にパラメータ指定するように | ||||
| 
 | ||||
| ### 個々のエンドポイントが廃止されることによる新しいストリーミングAPIの利用方法 | ||||
| 具体的には、まず https://example.misskey/streaming にwebsocket接続します。 | ||||
| 次に、例えば「messaging」ストリーム(チャンネルと呼びます)に接続したいときは、ストリームに次のようなデータを送信します: | ||||
| ``` javascript | ||||
| { | ||||
|   type: 'connect', | ||||
|   body: { | ||||
|     channel: 'messaging', | ||||
|     id: 'foobar', | ||||
|     params: { | ||||
|       otherparty: 'xxxxxxxxxxxx' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| ここで、`id`にはそのチャンネルとやり取りするための任意のIDを設定します。 | ||||
| IDはチャンネルごとではなく「チャンネルの接続ごと」です。なぜなら、同じチャンネルに異なるパラメータで複数接続するケースもあるからです。 | ||||
| `params`はチャンネルに接続する際のパラメータです。チャンネルによって接続時に必要とされるパラメータは異なります。パラメータ不要のチャンネルに接続する際は、このプロパティは省略可能です。 | ||||
| 
 | ||||
| チャンネルにメッセージを送信するには、次のようなデータを送信します: | ||||
| ``` javascript | ||||
| { | ||||
|   type: 'channel', | ||||
|   body: { | ||||
|     id: 'foobar', | ||||
|     type: 'something', | ||||
|     body: { | ||||
|       some: 'thing' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| ここで、`id`にはチャンネルに接続するときに指定したIDを設定します。 | ||||
| 
 | ||||
| 逆に、チャンネルからメッセージが流れてくると、次のようなデータが受信されます: | ||||
| ``` javascript | ||||
| { | ||||
|   type: 'channel', | ||||
|   body: { | ||||
|     id: 'foobar', | ||||
|     type: 'something', | ||||
|     body: { | ||||
|       some: 'thing' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| ここで、`id`にはチャンネルに接続するときに指定したIDが設定されています。 | ||||
| 
 | ||||
| ### 投稿のキャプチャに関する変更 | ||||
| 投稿の更新イベントに投稿情報は含まれなくなりました。代わりに、その投稿が「リアクションされた」「アンケートに投票された」「削除された」といったアクション情報が設定されます。 | ||||
| 
 | ||||
| 具体的には次のようなデータが受信されます: | ||||
| ``` javascript | ||||
| { | ||||
|   type: 'noteUpdated', | ||||
|   body: { | ||||
|     id: 'xxxxxxxxxxx', | ||||
|     type: 'reacted', | ||||
|     body: { | ||||
|       reaction: 'hmm' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| * reacted ... 投稿にリアクションされた。`reaction`プロパティにリアクションコードが含まれます。 | ||||
| * pollVoted ... アンケートに投票された。`choice`プロパティに選択肢ID、`userId`に投票者IDが含まれます。 | ||||
| 
 | ||||
| 9.0.0 | ||||
| ----- | ||||
| 
 | ||||
|  |  | |||
|  | @ -83,6 +83,7 @@ | |||
| 		"@types/websocket": "0.0.40", | ||||
| 		"@types/ws": "6.0.1", | ||||
| 		"animejs": "2.2.0", | ||||
| 		"autobind-decorator": "2.1.0", | ||||
| 		"autosize": "4.0.2", | ||||
| 		"autwh": "0.1.0", | ||||
| 		"bcryptjs": "2.4.3", | ||||
|  | @ -225,8 +226,8 @@ | |||
| 		"vuex-persistedstate": "2.5.4", | ||||
| 		"web-push": "3.3.3", | ||||
| 		"webfinger.js": "2.6.6", | ||||
| 		"webpack-cli": "3.1.2", | ||||
| 		"webpack": "4.20.2", | ||||
| 		"webpack-cli": "3.1.2", | ||||
| 		"websocket": "1.0.28", | ||||
| 		"ws": "6.0.0", | ||||
| 		"xev": "2.0.1" | ||||
|  |  | |||
|  | @ -13,21 +13,21 @@ type Notification = { | |||
| 
 | ||||
| export default function(type, data): Notification { | ||||
| 	switch (type) { | ||||
| 		case 'drive_file_created': | ||||
| 		case 'driveFileCreated': | ||||
| 			return { | ||||
| 				title: '%i18n:common.notification.file-uploaded%', | ||||
| 				body: data.name, | ||||
| 				icon: data.url | ||||
| 			}; | ||||
| 
 | ||||
| 		case 'unread_messaging_message': | ||||
| 		case 'unreadMessagingMessage': | ||||
| 			return { | ||||
| 				title: '%i18n:common.notification.message-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split("{}")[1] , | ||||
| 				body: data.text, // TODO: getMessagingMessageSummary(data),
 | ||||
| 				icon: data.user.avatarUrl | ||||
| 			}; | ||||
| 
 | ||||
| 		case 'reversi_invited': | ||||
| 		case 'reversiInvited': | ||||
| 			return { | ||||
| 				title: '%i18n:common.notification.reversi-invited%', | ||||
| 				body: '%i18n:common.notification.reversi-invited-by%'.split("{}")[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split("{}")[1], | ||||
|  |  | |||
							
								
								
									
										105
									
								
								src/client/app/common/scripts/note-subscriber.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/client/app/common/scripts/note-subscriber.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | |||
| import Vue from 'vue'; | ||||
| 
 | ||||
| export default prop => ({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		$_ns_note_(): any { | ||||
| 			return this[prop]; | ||||
| 		}, | ||||
| 
 | ||||
| 		$_ns_isRenote(): boolean { | ||||
| 			return (this.$_ns_note_.renote && | ||||
| 				this.$_ns_note_.text == null && | ||||
| 				this.$_ns_note_.fileIds.length == 0 && | ||||
| 				this.$_ns_note_.poll == null); | ||||
| 		}, | ||||
| 
 | ||||
| 		$_ns_target(): any { | ||||
| 			return this._ns_isRenote ? this.$_ns_note_.renote : this.$_ns_note_; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.capture(true); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.on('_connected_', this.onStreamConnected); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.decapture(true); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('_connected_', this.onStreamConnected); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		capture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				const data = { | ||||
| 					id: this.$_ns_target.id | ||||
| 				} as any; | ||||
| 
 | ||||
| 				if ( | ||||
| 					(this.$_ns_target.visibleUserIds || []).includes(this.$store.state.i.id) || | ||||
| 					(this.$_ns_target.mentions || []).includes(this.$store.state.i.id) | ||||
| 				) { | ||||
| 					data.read = true; | ||||
| 				} | ||||
| 
 | ||||
| 				this.connection.send('sn', data); | ||||
| 				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		decapture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				this.connection.send('un', { | ||||
| 					id: this.$_ns_target.id | ||||
| 				}); | ||||
| 				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onStreamConnected() { | ||||
| 			this.capture(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onStreamNoteUpdated(data) { | ||||
| 			const { type, id, body } = data; | ||||
| 
 | ||||
| 			if (id !== this.$_ns_target.id) return; | ||||
| 
 | ||||
| 			switch (type) { | ||||
| 				case 'reacted': { | ||||
| 					const reaction = body.reaction; | ||||
| 					if (this.$_ns_target.reactionCounts == null) Vue.set(this.$_ns_target, 'reactionCounts', {}); | ||||
| 					this.$_ns_target.reactionCounts[reaction] = (this.$_ns_target.reactionCounts[reaction] || 0) + 1; | ||||
| 					break; | ||||
| 				} | ||||
| 
 | ||||
| 				case 'pollVoted': { | ||||
| 					if (body.userId == this.$store.state.i.id) return; | ||||
| 					const choice = body.choice; | ||||
| 					this.$_ns_target.poll.choices.find(c => c.id === choice).votes++; | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			this.$emit(`update:${prop}`, this.$_ns_note_); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
							
								
								
									
										318
									
								
								src/client/app/common/scripts/stream.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										318
									
								
								src/client/app/common/scripts/stream.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,318 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import { EventEmitter } from 'eventemitter3'; | ||||
| import * as ReconnectingWebsocket from 'reconnecting-websocket'; | ||||
| import { wsUrl } from '../../config'; | ||||
| import MiOS from '../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Misskey stream connection | ||||
|  */ | ||||
| export default class Stream extends EventEmitter { | ||||
| 	private stream: ReconnectingWebsocket; | ||||
| 	private state: string; | ||||
| 	private buffer: any[]; | ||||
| 	private sharedConnections: SharedConnection[] = []; | ||||
| 	private nonSharedConnections: NonSharedConnection[] = []; | ||||
| 
 | ||||
| 	constructor(os: MiOS) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.state = 'initializing'; | ||||
| 		this.buffer = []; | ||||
| 
 | ||||
| 		const user = os.store.state.i; | ||||
| 
 | ||||
| 		this.stream = new ReconnectingWebsocket(wsUrl + (user ? `?i=${user.token}` : '')); | ||||
| 		this.stream.addEventListener('open', this.onOpen); | ||||
| 		this.stream.addEventListener('close', this.onClose); | ||||
| 		this.stream.addEventListener('message', this.onMessage); | ||||
| 
 | ||||
| 		if (user) { | ||||
| 			const main = this.useSharedConnection('main'); | ||||
| 
 | ||||
| 			// 自分の情報が更新されたとき
 | ||||
| 			main.on('meUpdated', i => { | ||||
| 				os.store.dispatch('mergeMe', i); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('readAllNotifications', () => { | ||||
| 				os.store.dispatch('mergeMe', { | ||||
| 					hasUnreadNotification: false | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('unreadNotification', () => { | ||||
| 				os.store.dispatch('mergeMe', { | ||||
| 					hasUnreadNotification: true | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('readAllMessagingMessages', () => { | ||||
| 				os.store.dispatch('mergeMe', { | ||||
| 					hasUnreadMessagingMessage: false | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('unreadMessagingMessage', () => { | ||||
| 				os.store.dispatch('mergeMe', { | ||||
| 					hasUnreadMessagingMessage: true | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('unreadMention', () => { | ||||
| 				os.store.dispatch('mergeMe', { | ||||
| 					hasUnreadMentions: true | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('readAllUnreadMentions', () => { | ||||
| 				os.store.dispatch('mergeMe', { | ||||
| 					hasUnreadMentions: false | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('unreadSpecifiedNote', () => { | ||||
| 				os.store.dispatch('mergeMe', { | ||||
| 					hasUnreadSpecifiedNotes: true | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('readAllUnreadSpecifiedNotes', () => { | ||||
| 				os.store.dispatch('mergeMe', { | ||||
| 					hasUnreadSpecifiedNotes: false | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('clientSettingUpdated', x => { | ||||
| 				os.store.commit('settings/set', { | ||||
| 					key: x.key, | ||||
| 					value: x.value | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('homeUpdated', x => { | ||||
| 				os.store.commit('settings/setHome', x); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('mobileHomeUpdated', x => { | ||||
| 				os.store.commit('settings/setMobileHome', x); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('widgetUpdated', x => { | ||||
| 				os.store.commit('settings/setWidget', { | ||||
| 					id: x.id, | ||||
| 					data: x.data | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			// トークンが再生成されたとき
 | ||||
| 			// このままではMisskeyが利用できないので強制的にサインアウトさせる
 | ||||
| 			main.on('myTokenRegenerated', () => { | ||||
| 				alert('%i18n:common.my-token-regenerated%'); | ||||
| 				os.signout(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public useSharedConnection = (channel: string): SharedConnection => { | ||||
| 		const existConnection = this.sharedConnections.find(c => c.channel === channel); | ||||
| 
 | ||||
| 		if (existConnection) { | ||||
| 			existConnection.use(); | ||||
| 			return existConnection; | ||||
| 		} else { | ||||
| 			const connection = new SharedConnection(this, channel); | ||||
| 			connection.use(); | ||||
| 			this.sharedConnections.push(connection); | ||||
| 			return connection; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public removeSharedConnection(connection: SharedConnection) { | ||||
| 		this.sharedConnections = this.sharedConnections.filter(c => c.id !== connection.id); | ||||
| 	} | ||||
| 
 | ||||
| 	public connectToChannel = (channel: string, params?: any): NonSharedConnection => { | ||||
| 		const connection = new NonSharedConnection(this, channel, params); | ||||
| 		this.nonSharedConnections.push(connection); | ||||
| 		return connection; | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public disconnectToChannel(connection: NonSharedConnection) { | ||||
| 		this.nonSharedConnections = this.nonSharedConnections.filter(c => c.id !== connection.id); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Callback of when open connection | ||||
| 	 */ | ||||
| 	@autobind | ||||
| 	private onOpen() { | ||||
| 		const isReconnect = this.state == 'reconnecting'; | ||||
| 
 | ||||
| 		this.state = 'connected'; | ||||
| 		this.emit('_connected_'); | ||||
| 
 | ||||
| 		// バッファーを処理
 | ||||
| 		const _buffer = [].concat(this.buffer); // Shallow copy
 | ||||
| 		this.buffer = []; // Clear buffer
 | ||||
| 		_buffer.forEach(data => { | ||||
| 			this.send(data); // Resend each buffered messages
 | ||||
| 		}); | ||||
| 
 | ||||
| 		// チャンネル再接続
 | ||||
| 		if (isReconnect) { | ||||
| 			this.sharedConnections.forEach(c => { | ||||
| 				c.connect(); | ||||
| 			}); | ||||
| 			this.nonSharedConnections.forEach(c => { | ||||
| 				c.connect(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Callback of when close connection | ||||
| 	 */ | ||||
| 	@autobind | ||||
| 	private onClose() { | ||||
| 		this.state = 'reconnecting'; | ||||
| 		this.emit('_disconnected_'); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Callback of when received a message from connection | ||||
| 	 */ | ||||
| 	@autobind | ||||
| 	private onMessage(message) { | ||||
| 		const { type, body } = JSON.parse(message.data); | ||||
| 
 | ||||
| 		if (type == 'channel') { | ||||
| 			const id = body.id; | ||||
| 			const connection = this.sharedConnections.find(c => c.id === id) || this.nonSharedConnections.find(c => c.id === id); | ||||
| 			connection.emit(body.type, body.body); | ||||
| 		} else { | ||||
| 			this.emit(type, body); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Send a message to connection | ||||
| 	 */ | ||||
| 	@autobind | ||||
| 	public send(typeOrPayload, payload?) { | ||||
| 		const data = payload === undefined ? typeOrPayload : { | ||||
| 			type: typeOrPayload, | ||||
| 			body: payload | ||||
| 		}; | ||||
| 
 | ||||
| 		// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
 | ||||
| 		if (this.state != 'connected') { | ||||
| 			this.buffer.push(data); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		this.stream.send(JSON.stringify(data)); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Close this connection | ||||
| 	 */ | ||||
| 	@autobind | ||||
| 	public close() { | ||||
| 		this.stream.removeEventListener('open', this.onOpen); | ||||
| 		this.stream.removeEventListener('message', this.onMessage); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| abstract class Connection extends EventEmitter { | ||||
| 	public channel: string; | ||||
| 	public id: string; | ||||
| 	protected params: any; | ||||
| 	protected stream: Stream; | ||||
| 
 | ||||
| 	constructor(stream: Stream, channel: string, params?: any) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.stream = stream; | ||||
| 		this.channel = channel; | ||||
| 		this.params = params; | ||||
| 		this.id = Math.random().toString(); | ||||
| 		this.connect(); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public connect() { | ||||
| 		this.stream.send('connect', { | ||||
| 			channel: this.channel, | ||||
| 			id: this.id, | ||||
| 			params: this.params | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public send(typeOrPayload, payload?) { | ||||
| 		const data = payload === undefined ? typeOrPayload : { | ||||
| 			type: typeOrPayload, | ||||
| 			body: payload | ||||
| 		}; | ||||
| 
 | ||||
| 		this.stream.send('channel', { | ||||
| 			id: this.id, | ||||
| 			body: data | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	public abstract dispose: () => void; | ||||
| } | ||||
| 
 | ||||
| class SharedConnection extends Connection { | ||||
| 	private users = 0; | ||||
| 	private disposeTimerId: any; | ||||
| 
 | ||||
| 	constructor(stream: Stream, channel: string) { | ||||
| 		super(stream, channel); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public use() { | ||||
| 		this.users++; | ||||
| 
 | ||||
| 		// タイマー解除
 | ||||
| 		if (this.disposeTimerId) { | ||||
| 			clearTimeout(this.disposeTimerId); | ||||
| 			this.disposeTimerId = null; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public dispose() { | ||||
| 		this.users--; | ||||
| 
 | ||||
| 		// そのコネクションの利用者が誰もいなくなったら
 | ||||
| 		if (this.users === 0) { | ||||
| 			// また直ぐに再利用される可能性があるので、一定時間待ち、
 | ||||
| 			// 新たな利用者が現れなければコネクションを切断する
 | ||||
| 			this.disposeTimerId = setTimeout(() => { | ||||
| 				this.disposeTimerId = null; | ||||
| 				this.removeAllListeners(); | ||||
| 				this.stream.send('disconnect', { id: this.id }); | ||||
| 				this.stream.removeSharedConnection(this); | ||||
| 			}, 3000); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| class NonSharedConnection extends Connection { | ||||
| 	constructor(stream: Stream, channel: string, params?: any) { | ||||
| 		super(stream, channel, params); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public dispose() { | ||||
| 		this.removeAllListeners(); | ||||
| 		this.stream.send('disconnect', { id: this.id }); | ||||
| 		this.stream.disconnectToChannel(this); | ||||
| 	} | ||||
| } | ||||
|  | @ -1,34 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import StreamManager from './stream-manager'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Drive stream connection | ||||
|  */ | ||||
| export class DriveStream extends Stream { | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(os, 'drive', { | ||||
| 			i: me.token | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export class DriveStreamManager extends StreamManager<DriveStream> { | ||||
| 	private me; | ||||
| 	private os: MiOS; | ||||
| 
 | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.me = me; | ||||
| 		this.os = os; | ||||
| 	} | ||||
| 
 | ||||
| 	public getConnection() { | ||||
| 		if (this.connection == null) { | ||||
| 			this.connection = new DriveStream(this.os, this.me); | ||||
| 		} | ||||
| 
 | ||||
| 		return this.connection; | ||||
| 	} | ||||
| } | ||||
|  | @ -1,13 +0,0 @@ | |||
| import Stream from '../../stream'; | ||||
| import MiOS from '../../../../../mios'; | ||||
| 
 | ||||
| export class ReversiGameStream extends Stream { | ||||
| 	constructor(os: MiOS, me, game) { | ||||
| 		super(os, 'games/reversi-game', me ? { | ||||
| 			i: me.token, | ||||
| 			game: game.id | ||||
| 		} : { | ||||
| 			game: game.id | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | @ -1,31 +0,0 @@ | |||
| import StreamManager from '../../stream-manager'; | ||||
| import Stream from '../../stream'; | ||||
| import MiOS from '../../../../../mios'; | ||||
| 
 | ||||
| export class ReversiStream extends Stream { | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(os, 'games/reversi', { | ||||
| 			i: me.token | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export class ReversiStreamManager extends StreamManager<ReversiStream> { | ||||
| 	private me; | ||||
| 	private os: MiOS; | ||||
| 
 | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.me = me; | ||||
| 		this.os = os; | ||||
| 	} | ||||
| 
 | ||||
| 	public getConnection() { | ||||
| 		if (this.connection == null) { | ||||
| 			this.connection = new ReversiStream(this.os, this.me); | ||||
| 		} | ||||
| 
 | ||||
| 		return this.connection; | ||||
| 	} | ||||
| } | ||||
|  | @ -1,34 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import StreamManager from './stream-manager'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Global timeline stream connection | ||||
|  */ | ||||
| export class GlobalTimelineStream extends Stream { | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(os, 'global-timeline', { | ||||
| 			i: me.token | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export class GlobalTimelineStreamManager extends StreamManager<GlobalTimelineStream> { | ||||
| 	private me; | ||||
| 	private os: MiOS; | ||||
| 
 | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.me = me; | ||||
| 		this.os = os; | ||||
| 	} | ||||
| 
 | ||||
| 	public getConnection() { | ||||
| 		if (this.connection == null) { | ||||
| 			this.connection = new GlobalTimelineStream(this.os, this.me); | ||||
| 		} | ||||
| 
 | ||||
| 		return this.connection; | ||||
| 	} | ||||
| } | ||||
|  | @ -1,13 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| export class HashtagStream extends Stream { | ||||
| 	constructor(os: MiOS, me, q) { | ||||
| 		super(os, 'hashtag', me ? { | ||||
| 			i: me.token, | ||||
| 			q: JSON.stringify(q) | ||||
| 		} : { | ||||
| 			q: JSON.stringify(q) | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | @ -1,126 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import StreamManager from './stream-manager'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Home stream connection | ||||
|  */ | ||||
| export class HomeStream extends Stream { | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(os, '', { | ||||
| 			i: me.token | ||||
| 		}); | ||||
| 
 | ||||
| 		// 最終利用日時を更新するため定期的にaliveメッセージを送信
 | ||||
| 		setInterval(() => { | ||||
| 			this.send({ type: 'alive' }); | ||||
| 			me.lastUsedAt = new Date(); | ||||
| 		}, 1000 * 60); | ||||
| 
 | ||||
| 		// 自分の情報が更新されたとき
 | ||||
| 		this.on('meUpdated', i => { | ||||
| 			if (os.debug) { | ||||
| 				console.log('I updated:', i); | ||||
| 			} | ||||
| 
 | ||||
| 			os.store.dispatch('mergeMe', i); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('read_all_notifications', () => { | ||||
| 			os.store.dispatch('mergeMe', { | ||||
| 				hasUnreadNotification: false | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('unread_notification', () => { | ||||
| 			os.store.dispatch('mergeMe', { | ||||
| 				hasUnreadNotification: true | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('read_all_messaging_messages', () => { | ||||
| 			os.store.dispatch('mergeMe', { | ||||
| 				hasUnreadMessagingMessage: false | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('unread_messaging_message', () => { | ||||
| 			os.store.dispatch('mergeMe', { | ||||
| 				hasUnreadMessagingMessage: true | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('unreadMention', () => { | ||||
| 			os.store.dispatch('mergeMe', { | ||||
| 				hasUnreadMentions: true | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('readAllUnreadMentions', () => { | ||||
| 			os.store.dispatch('mergeMe', { | ||||
| 				hasUnreadMentions: false | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('unreadSpecifiedNote', () => { | ||||
| 			os.store.dispatch('mergeMe', { | ||||
| 				hasUnreadSpecifiedNotes: true | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('readAllUnreadSpecifiedNotes', () => { | ||||
| 			os.store.dispatch('mergeMe', { | ||||
| 				hasUnreadSpecifiedNotes: false | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('clientSettingUpdated', x => { | ||||
| 			os.store.commit('settings/set', { | ||||
| 				key: x.key, | ||||
| 				value: x.value | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('home_updated', x => { | ||||
| 			os.store.commit('settings/setHome', x); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('mobile_home_updated', x => { | ||||
| 			os.store.commit('settings/setMobileHome', x); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.on('widgetUpdated', x => { | ||||
| 			os.store.commit('settings/setWidget', { | ||||
| 				id: x.id, | ||||
| 				data: x.data | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		// トークンが再生成されたとき
 | ||||
| 		// このままではMisskeyが利用できないので強制的にサインアウトさせる
 | ||||
| 		this.on('my_token_regenerated', () => { | ||||
| 			alert('%i18n:common.my-token-regenerated%'); | ||||
| 			os.signout(); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export class HomeStreamManager extends StreamManager<HomeStream> { | ||||
| 	private me; | ||||
| 	private os: MiOS; | ||||
| 
 | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.me = me; | ||||
| 		this.os = os; | ||||
| 	} | ||||
| 
 | ||||
| 	public getConnection() { | ||||
| 		if (this.connection == null) { | ||||
| 			this.connection = new HomeStream(this.os, this.me); | ||||
| 		} | ||||
| 
 | ||||
| 		return this.connection; | ||||
| 	} | ||||
| } | ||||
|  | @ -1,34 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import StreamManager from './stream-manager'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Hybrid timeline stream connection | ||||
|  */ | ||||
| export class HybridTimelineStream extends Stream { | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(os, 'hybrid-timeline', { | ||||
| 			i: me.token | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export class HybridTimelineStreamManager extends StreamManager<HybridTimelineStream> { | ||||
| 	private me; | ||||
| 	private os: MiOS; | ||||
| 
 | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.me = me; | ||||
| 		this.os = os; | ||||
| 	} | ||||
| 
 | ||||
| 	public getConnection() { | ||||
| 		if (this.connection == null) { | ||||
| 			this.connection = new HybridTimelineStream(this.os, this.me); | ||||
| 		} | ||||
| 
 | ||||
| 		return this.connection; | ||||
| 	} | ||||
| } | ||||
|  | @ -1,34 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import StreamManager from './stream-manager'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Local timeline stream connection | ||||
|  */ | ||||
| export class LocalTimelineStream extends Stream { | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(os, 'local-timeline', me ? { | ||||
| 			i: me.token | ||||
| 		} : {}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export class LocalTimelineStreamManager extends StreamManager<LocalTimelineStream> { | ||||
| 	private me; | ||||
| 	private os: MiOS; | ||||
| 
 | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.me = me; | ||||
| 		this.os = os; | ||||
| 	} | ||||
| 
 | ||||
| 	public getConnection() { | ||||
| 		if (this.connection == null) { | ||||
| 			this.connection = new LocalTimelineStream(this.os, this.me); | ||||
| 		} | ||||
| 
 | ||||
| 		return this.connection; | ||||
| 	} | ||||
| } | ||||
|  | @ -1,34 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import StreamManager from './stream-manager'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Messaging index stream connection | ||||
|  */ | ||||
| export class MessagingIndexStream extends Stream { | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(os, 'messaging-index', { | ||||
| 			i: me.token | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export class MessagingIndexStreamManager extends StreamManager<MessagingIndexStream> { | ||||
| 	private me; | ||||
| 	private os: MiOS; | ||||
| 
 | ||||
| 	constructor(os: MiOS, me) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.me = me; | ||||
| 		this.os = os; | ||||
| 	} | ||||
| 
 | ||||
| 	public getConnection() { | ||||
| 		if (this.connection == null) { | ||||
| 			this.connection = new MessagingIndexStream(this.os, this.me); | ||||
| 		} | ||||
| 
 | ||||
| 		return this.connection; | ||||
| 	} | ||||
| } | ||||
|  | @ -1,20 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Messaging stream connection | ||||
|  */ | ||||
| export class MessagingStream extends Stream { | ||||
| 	constructor(os: MiOS, me, otherparty) { | ||||
| 		super(os, 'messaging', { | ||||
| 			i: me.token, | ||||
| 			otherparty | ||||
| 		}); | ||||
| 
 | ||||
| 		(this as any).on('_connected_', () => { | ||||
| 			this.send({ | ||||
| 				i: me.token | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | @ -1,30 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import StreamManager from './stream-manager'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Notes stats stream connection | ||||
|  */ | ||||
| export class NotesStatsStream extends Stream { | ||||
| 	constructor(os: MiOS) { | ||||
| 		super(os, 'notes-stats'); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export class NotesStatsStreamManager extends StreamManager<NotesStatsStream> { | ||||
| 	private os: MiOS; | ||||
| 
 | ||||
| 	constructor(os: MiOS) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.os = os; | ||||
| 	} | ||||
| 
 | ||||
| 	public getConnection() { | ||||
| 		if (this.connection == null) { | ||||
| 			this.connection = new NotesStatsStream(this.os); | ||||
| 		} | ||||
| 
 | ||||
| 		return this.connection; | ||||
| 	} | ||||
| } | ||||
|  | @ -1,30 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import StreamManager from './stream-manager'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Server stats stream connection | ||||
|  */ | ||||
| export class ServerStatsStream extends Stream { | ||||
| 	constructor(os: MiOS) { | ||||
| 		super(os, 'server-stats'); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export class ServerStatsStreamManager extends StreamManager<ServerStatsStream> { | ||||
| 	private os: MiOS; | ||||
| 
 | ||||
| 	constructor(os: MiOS) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.os = os; | ||||
| 	} | ||||
| 
 | ||||
| 	public getConnection() { | ||||
| 		if (this.connection == null) { | ||||
| 			this.connection = new ServerStatsStream(this.os); | ||||
| 		} | ||||
| 
 | ||||
| 		return this.connection; | ||||
| 	} | ||||
| } | ||||
|  | @ -1,109 +0,0 @@ | |||
| import { EventEmitter } from 'eventemitter3'; | ||||
| import * as uuid from 'uuid'; | ||||
| import Connection from './stream'; | ||||
| import { erase } from '../../../../../prelude/array'; | ||||
| 
 | ||||
| /** | ||||
|  * ストリーム接続を管理するクラス | ||||
|  * 複数の場所から同じストリームを利用する際、接続をまとめたりする | ||||
|  */ | ||||
| export default abstract class StreamManager<T extends Connection> extends EventEmitter { | ||||
| 	private _connection: T = null; | ||||
| 
 | ||||
| 	private disposeTimerId: any; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * コネクションを必要としているユーザー | ||||
| 	 */ | ||||
| 	private users = []; | ||||
| 
 | ||||
| 	protected set connection(connection: T) { | ||||
| 		this._connection = connection; | ||||
| 
 | ||||
| 		if (this._connection == null) { | ||||
| 			this.emit('disconnected'); | ||||
| 		} else { | ||||
| 			this.emit('connected', this._connection); | ||||
| 
 | ||||
| 			this._connection.on('_connected_', () => { | ||||
| 				this.emit('_connected_'); | ||||
| 			}); | ||||
| 
 | ||||
| 			this._connection.on('_disconnected_', () => { | ||||
| 				this.emit('_disconnected_'); | ||||
| 			}); | ||||
| 
 | ||||
| 			this._connection.user = 'Managed'; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	protected get connection() { | ||||
| 		return this._connection; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * コネクションを持っているか否か | ||||
| 	 */ | ||||
| 	public get hasConnection() { | ||||
| 		return this._connection != null; | ||||
| 	} | ||||
| 
 | ||||
| 	public get state(): string { | ||||
| 		if (!this.hasConnection) return 'no-connection'; | ||||
| 		return this._connection.state; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * コネクションを要求します | ||||
| 	 */ | ||||
| 	public abstract getConnection(): T; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 現在接続しているコネクションを取得します | ||||
| 	 */ | ||||
| 	public borrow() { | ||||
| 		return this._connection; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * コネクションを要求するためのユーザーIDを発行します | ||||
| 	 */ | ||||
| 	public use() { | ||||
| 		// タイマー解除
 | ||||
| 		if (this.disposeTimerId) { | ||||
| 			clearTimeout(this.disposeTimerId); | ||||
| 			this.disposeTimerId = null; | ||||
| 		} | ||||
| 
 | ||||
| 		// ユーザーID生成
 | ||||
| 		const userId = uuid(); | ||||
| 
 | ||||
| 		this.users.push(userId); | ||||
| 
 | ||||
| 		this._connection.user = `Managed (${ this.users.length })`; | ||||
| 
 | ||||
| 		return userId; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * コネクションを利用し終わってもう必要ないことを通知します | ||||
| 	 * @param userId use で発行したユーザーID | ||||
| 	 */ | ||||
| 	public dispose(userId) { | ||||
| 		this.users = erase(userId, this.users); | ||||
| 
 | ||||
| 		this._connection.user = `Managed (${ this.users.length })`; | ||||
| 
 | ||||
| 		// 誰もコネクションの利用者がいなくなったら
 | ||||
| 		if (this.users.length == 0) { | ||||
| 			// また直ぐに再利用される可能性があるので、一定時間待ち、
 | ||||
| 			// 新たな利用者が現れなければコネクションを切断する
 | ||||
| 			this.disposeTimerId = setTimeout(() => { | ||||
| 				this.disposeTimerId = null; | ||||
| 
 | ||||
| 				this.connection.close(); | ||||
| 				this.connection = null; | ||||
| 			}, 3000); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,137 +0,0 @@ | |||
| import { EventEmitter } from 'eventemitter3'; | ||||
| import * as uuid from 'uuid'; | ||||
| import * as ReconnectingWebsocket from 'reconnecting-websocket'; | ||||
| import { wsUrl } from '../../../config'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * Misskey stream connection | ||||
|  */ | ||||
| export default class Connection extends EventEmitter { | ||||
| 	public state: string; | ||||
| 	private buffer: any[]; | ||||
| 	public socket: ReconnectingWebsocket; | ||||
| 	public name: string; | ||||
| 	public connectedAt: Date; | ||||
| 	public user: string = null; | ||||
| 	public in: number = 0; | ||||
| 	public out: number = 0; | ||||
| 	public inout: Array<{ | ||||
| 		type: 'in' | 'out', | ||||
| 		at: Date, | ||||
| 		data: string | ||||
| 	}> = []; | ||||
| 	public id: string; | ||||
| 	public isSuspended = false; | ||||
| 	private os: MiOS; | ||||
| 
 | ||||
| 	constructor(os: MiOS, endpoint, params?) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		//#region BIND
 | ||||
| 		this.onOpen =    this.onOpen.bind(this); | ||||
| 		this.onClose =   this.onClose.bind(this); | ||||
| 		this.onMessage = this.onMessage.bind(this); | ||||
| 		this.send =      this.send.bind(this); | ||||
| 		this.close =     this.close.bind(this); | ||||
| 		//#endregion
 | ||||
| 
 | ||||
| 		this.id = uuid(); | ||||
| 		this.os = os; | ||||
| 		this.name = endpoint; | ||||
| 		this.state = 'initializing'; | ||||
| 		this.buffer = []; | ||||
| 
 | ||||
| 		const query = params | ||||
| 			? Object.keys(params) | ||||
| 				.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`) | ||||
| 				.join('&') | ||||
| 			: null; | ||||
| 
 | ||||
| 		this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? `?${query}` : ''}`); | ||||
| 		this.socket.addEventListener('open', this.onOpen); | ||||
| 		this.socket.addEventListener('close', this.onClose); | ||||
| 		this.socket.addEventListener('message', this.onMessage); | ||||
| 
 | ||||
| 		// Register this connection for debugging
 | ||||
| 		this.os.registerStreamConnection(this); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Callback of when open connection | ||||
| 	 */ | ||||
| 	private onOpen() { | ||||
| 		this.state = 'connected'; | ||||
| 		this.emit('_connected_'); | ||||
| 
 | ||||
| 		this.connectedAt = new Date(); | ||||
| 
 | ||||
| 		// バッファーを処理
 | ||||
| 		const _buffer = [].concat(this.buffer); // Shallow copy
 | ||||
| 		this.buffer = []; // Clear buffer
 | ||||
| 		_buffer.forEach(data => { | ||||
| 			this.send(data); // Resend each buffered messages
 | ||||
| 
 | ||||
| 			if (this.os.debug) { | ||||
| 				this.out++; | ||||
| 				this.inout.push({ type: 'out', at: new Date(), data }); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Callback of when close connection | ||||
| 	 */ | ||||
| 	private onClose() { | ||||
| 		this.state = 'reconnecting'; | ||||
| 		this.emit('_disconnected_'); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Callback of when received a message from connection | ||||
| 	 */ | ||||
| 	private onMessage(message) { | ||||
| 		if (this.isSuspended) return; | ||||
| 
 | ||||
| 		if (this.os.debug) { | ||||
| 			this.in++; | ||||
| 			this.inout.push({ type: 'in', at: new Date(), data: message.data }); | ||||
| 		} | ||||
| 
 | ||||
| 		try { | ||||
| 			const msg = JSON.parse(message.data); | ||||
| 			if (msg.type) this.emit(msg.type, msg.body); | ||||
| 		} catch (e) { | ||||
| 			// noop
 | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Send a message to connection | ||||
| 	 */ | ||||
| 	public send(data) { | ||||
| 		if (this.isSuspended) return; | ||||
| 
 | ||||
| 		// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
 | ||||
| 		if (this.state != 'connected') { | ||||
| 			this.buffer.push(data); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		if (this.os.debug) { | ||||
| 			this.out++; | ||||
| 			this.inout.push({ type: 'out', at: new Date(), data }); | ||||
| 		} | ||||
| 
 | ||||
| 		this.socket.send(JSON.stringify(data)); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Close this connection | ||||
| 	 */ | ||||
| 	public close() { | ||||
| 		this.os.unregisterStreamConnection(this); | ||||
| 		this.socket.removeEventListener('open', this.onOpen); | ||||
| 		this.socket.removeEventListener('message', this.onMessage); | ||||
| 	} | ||||
| } | ||||
|  | @ -1,17 +0,0 @@ | |||
| import Stream from './stream'; | ||||
| import MiOS from '../../mios'; | ||||
| 
 | ||||
| export class UserListStream extends Stream { | ||||
| 	constructor(os: MiOS, me, listId) { | ||||
| 		super(os, 'user-list', { | ||||
| 			i: me.token, | ||||
| 			listId | ||||
| 		}); | ||||
| 
 | ||||
| 		(this as any).on('_connected_', () => { | ||||
| 			this.send({ | ||||
| 				i: me.token | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | @ -9,7 +9,6 @@ | |||
| import Vue from 'vue'; | ||||
| import XGame from './reversi.game.vue'; | ||||
| import XRoom from './reversi.room.vue'; | ||||
| import { ReversiGameStream } from '../../../../scripts/streaming/games/reversi/reversi-game'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
|  | @ -34,12 +33,13 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 	created() { | ||||
| 		this.g = this.game; | ||||
| 		this.connection = new ReversiGameStream((this as any).os, this.$store.state.i, this.game); | ||||
| 		this.connection = (this as any).os.stream.connectToChannel('gamesReversiGame', { | ||||
| 			gameId: this.game.id | ||||
| 		}); | ||||
| 		this.connection.on('started', this.onStarted); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('started', this.onStarted); | ||||
| 		this.connection.close(); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onStarted(game) { | ||||
|  |  | |||
|  | @ -59,15 +59,13 @@ export default Vue.extend({ | |||
| 			myGames: [], | ||||
| 			matching: null, | ||||
| 			invitations: [], | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.streams.reversiStream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.streams.reversiStream.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('gamesReversi'); | ||||
| 
 | ||||
| 			this.connection.on('invited', this.onInvited); | ||||
| 
 | ||||
|  | @ -90,8 +88,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		if (this.connection) { | ||||
| 			this.connection.off('invited', this.onInvited); | ||||
| 			(this as any).os.streams.reversiStream.dispose(this.connectionId); | ||||
| 			this.connection.dispose(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -47,7 +47,6 @@ export default Vue.extend({ | |||
| 			game: null, | ||||
| 			matching: null, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			pingClock: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | @ -66,8 +65,7 @@ export default Vue.extend({ | |||
| 		this.fetch(); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.streams.reversiStream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.streams.reversiStream.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('gamesReversi'); | ||||
| 
 | ||||
| 			this.connection.on('matched', this.onMatched); | ||||
| 
 | ||||
|  | @ -84,9 +82,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		if (this.connection) { | ||||
| 			this.connection.off('matched', this.onMatched); | ||||
| 			(this as any).os.streams.reversiStream.dispose(this.connectionId); | ||||
| 
 | ||||
| 			this.connection.dispose(); | ||||
| 			clearInterval(this.pingClock); | ||||
| 		} | ||||
| 	}, | ||||
|  |  | |||
|  | @ -30,7 +30,6 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { MessagingStream } from '../../scripts/streaming/messaging'; | ||||
| import XMessage from './messaging-room.message.vue'; | ||||
| import XForm from './messaging-room.form.vue'; | ||||
| import { url } from '../../../config'; | ||||
|  | @ -72,7 +71,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = new MessagingStream((this as any).os, this.$store.state.i, this.user.id); | ||||
| 		this.connection =((this as any).os.stream.connectToChannel('messaging', { otherparty: this.user.id }); | ||||
| 
 | ||||
| 		this.connection.on('message', this.onMessage); | ||||
| 		this.connection.on('read', this.onRead); | ||||
|  | @ -92,9 +91,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('message', this.onMessage); | ||||
| 		this.connection.off('read', this.onRead); | ||||
| 		this.connection.close(); | ||||
| 		this.connection.dispose(); | ||||
| 
 | ||||
| 		if (this.isNaked) { | ||||
| 			window.removeEventListener('scroll', this.onScroll); | ||||
|  | @ -166,6 +163,7 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 
 | ||||
| 		onMessage(message) { | ||||
| 			console.log(message); | ||||
| 			// サウンドを再生する | ||||
| 			if (this.$store.state.device.enableSounds) { | ||||
| 				const sound = new Audio(`${url}/assets/message.mp3`); | ||||
|  |  | |||
|  | @ -71,13 +71,11 @@ export default Vue.extend({ | |||
| 			messages: [], | ||||
| 			q: null, | ||||
| 			result: [], | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.streams.messagingIndexStream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.streams.messagingIndexStream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('messagingIndex'); | ||||
| 
 | ||||
| 		this.connection.on('message', this.onMessage); | ||||
| 		this.connection.on('read', this.onRead); | ||||
|  | @ -88,9 +86,7 @@ export default Vue.extend({ | |||
| 		}); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('message', this.onMessage); | ||||
| 		this.connection.off('read', this.onRead); | ||||
| 		(this as any).os.streams.messagingIndexStream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		getAcct, | ||||
|  |  | |||
|  | @ -56,7 +56,7 @@ export default Vue.extend({ | |||
| 				username: this.username, | ||||
| 				password: this.password, | ||||
| 				token: this.user && this.user.twoFactorEnabled ? this.token : undefined | ||||
| 			}).then(() => { | ||||
| 			}, true).then(() => { | ||||
| 				location.reload(); | ||||
| 			}).catch(() => { | ||||
| 				alert('%i18n:@login-failed%'); | ||||
|  |  | |||
|  | @ -131,11 +131,11 @@ export default Vue.extend({ | |||
| 				password: this.password, | ||||
| 				invitationCode: this.invitationCode, | ||||
| 				'g-recaptcha-response': this.meta.recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null | ||||
| 			}).then(() => { | ||||
| 			}, true).then(() => { | ||||
| 				(this as any).api('signin', { | ||||
| 					username: this.username, | ||||
| 					password: this.password | ||||
| 				}).then(() => { | ||||
| 				}, true).then(() => { | ||||
| 					location.href = '/'; | ||||
| 				}); | ||||
| 			}).catch(() => { | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ import * as anime from 'animejs'; | |||
| export default Vue.extend({ | ||||
| 	computed: { | ||||
| 		stream() { | ||||
| 			return (this as any).os.stream; | ||||
| 			return (this as any).os.stream.useSharedConnection('main'); | ||||
| 		} | ||||
| 	}, | ||||
| 	created() { | ||||
|  |  | |||
|  | @ -38,23 +38,20 @@ export default Vue.extend({ | |||
| 		return { | ||||
| 			fetching: true, | ||||
| 			notes: [], | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 
 | ||||
| 		this.connection = (this as any).os.streams.localTimelineStream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.streams.localTimelineStream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('localTimeline'); | ||||
| 
 | ||||
| 		this.connection.on('note', this.onNote); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('note', this.onNote); | ||||
| 		(this as any).os.streams.localTimelineStream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -24,15 +24,13 @@ export default define({ | |||
| 		return { | ||||
| 			images: [], | ||||
| 			fetching: true, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 		this.connection.on('drive_file_created', this.onDriveFileCreated); | ||||
| 		this.connection.on('driveFileCreated', this.onDriveFileCreated); | ||||
| 
 | ||||
| 		(this as any).api('drive/stream', { | ||||
| 			type: 'image/*', | ||||
|  | @ -43,8 +41,7 @@ export default define({ | |||
| 		}); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('drive_file_created', this.onDriveFileCreated); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onDriveFileCreated(file) { | ||||
|  |  | |||
|  | @ -82,7 +82,6 @@ export default define({ | |||
| 	data() { | ||||
| 		return { | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			viewBoxY: 30, | ||||
| 			stats: [], | ||||
| 			fediGradientId: uuid(), | ||||
|  | @ -110,8 +109,7 @@ export default define({ | |||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.streams.notesStatsStream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.streams.notesStatsStream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('notesStats'); | ||||
| 
 | ||||
| 		this.connection.on('stats', this.onStats); | ||||
| 		this.connection.on('statsLog', this.onStatsLog); | ||||
|  | @ -121,9 +119,7 @@ export default define({ | |||
| 		}); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('stats', this.onStats); | ||||
| 		this.connection.off('statsLog', this.onStatsLog); | ||||
| 		(this as any).os.streams.notesStatsStream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		toggle() { | ||||
|  |  | |||
|  | @ -45,8 +45,7 @@ export default define({ | |||
| 		return { | ||||
| 			fetching: true, | ||||
| 			meta: null, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
|  | @ -55,11 +54,10 @@ export default define({ | |||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 
 | ||||
| 		this.connection = (this as any).os.streams.serverStatsStream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.streams.serverStatsStream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('serverStats'); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		(this as any).os.streams.serverStatsStream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		toggle() { | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ export const host = address.host; | |||
| export const hostname = address.hostname; | ||||
| export const url = address.origin; | ||||
| export const apiUrl = url + '/api'; | ||||
| export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://'); | ||||
| export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming'; | ||||
| export const lang = _LANG_; | ||||
| export const langs = _LANGS_; | ||||
| export const themeColor = _THEME_COLOR_; | ||||
|  |  | |||
|  | @ -9,7 +9,6 @@ import './style.styl'; | |||
| 
 | ||||
| import init from '../init'; | ||||
| import fuckAdBlock from '../common/scripts/fuck-ad-block'; | ||||
| import { HomeStreamManager } from '../common/scripts/streaming/home'; | ||||
| import composeNotification from '../common/scripts/compose-notification'; | ||||
| 
 | ||||
| import chooseDriveFolder from './api/choose-drive-folder'; | ||||
|  | @ -37,6 +36,7 @@ import MkTag from './views/pages/tag.vue'; | |||
| import MkReversi from './views/pages/games/reversi.vue'; | ||||
| import MkShare from './views/pages/share.vue'; | ||||
| import MkFollow from '../common/views/pages/follow.vue'; | ||||
| import MiOS from '../mios'; | ||||
| 
 | ||||
| /** | ||||
|  * init | ||||
|  | @ -102,23 +102,18 @@ init(async (launch) => { | |||
| 		} | ||||
| 
 | ||||
| 		if ((Notification as any).permission == 'granted') { | ||||
| 			registerNotifications(os.stream); | ||||
| 			registerNotifications(os); | ||||
| 		} | ||||
| 	} | ||||
| }, true); | ||||
| 
 | ||||
| function registerNotifications(stream: HomeStreamManager) { | ||||
| function registerNotifications(os: MiOS) { | ||||
| 	const stream = os.stream; | ||||
| 
 | ||||
| 	if (stream == null) return; | ||||
| 
 | ||||
| 	if (stream.hasConnection) { | ||||
| 		attach(stream.borrow()); | ||||
| 	} | ||||
| 	const connection = stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 	stream.on('connected', connection => { | ||||
| 		attach(connection); | ||||
| 	}); | ||||
| 
 | ||||
| 	function attach(connection) { | ||||
| 	connection.on('notification', notification => { | ||||
| 		const _n = composeNotification('notification', notification); | ||||
| 		const n = new Notification(_n.title, { | ||||
|  | @ -128,8 +123,8 @@ function registerNotifications(stream: HomeStreamManager) { | |||
| 		setTimeout(n.close.bind(n), 6000); | ||||
| 	}); | ||||
| 
 | ||||
| 		connection.on('drive_file_created', file => { | ||||
| 			const _n = composeNotification('drive_file_created', file); | ||||
| 	connection.on('driveFileCreated', file => { | ||||
| 		const _n = composeNotification('driveFileCreated', file); | ||||
| 		const n = new Notification(_n.title, { | ||||
| 			body: _n.body, | ||||
| 			icon: _n.icon | ||||
|  | @ -137,8 +132,8 @@ function registerNotifications(stream: HomeStreamManager) { | |||
| 		setTimeout(n.close.bind(n), 5000); | ||||
| 	}); | ||||
| 
 | ||||
| 		connection.on('unread_messaging_message', message => { | ||||
| 			const _n = composeNotification('unread_messaging_message', message); | ||||
| 	connection.on('unreadMessagingMessage', message => { | ||||
| 		const _n = composeNotification('unreadMessagingMessage', message); | ||||
| 		const n = new Notification(_n.title, { | ||||
| 			body: _n.body, | ||||
| 			icon: _n.icon | ||||
|  | @ -152,12 +147,11 @@ function registerNotifications(stream: HomeStreamManager) { | |||
| 		setTimeout(n.close.bind(n), 7000); | ||||
| 	}); | ||||
| 
 | ||||
| 		connection.on('reversi_invited', matching => { | ||||
| 			const _n = composeNotification('reversi_invited', matching); | ||||
| 	connection.on('reversiInvited', matching => { | ||||
| 		const _n = composeNotification('reversiInvited', matching); | ||||
| 		const n = new Notification(_n.title, { | ||||
| 			body: _n.body, | ||||
| 			icon: _n.icon | ||||
| 		}); | ||||
| 	}); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -98,8 +98,7 @@ export default Vue.extend({ | |||
| 			hierarchyFolders: [], | ||||
| 			selectedFiles: [], | ||||
| 			uploadings: [], | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			connection: null | ||||
| 
 | ||||
| 			/** | ||||
| 			 * ドロップされようとしているか | ||||
|  | @ -116,8 +115,7 @@ export default Vue.extend({ | |||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.streams.driveStream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.streams.driveStream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('drive'); | ||||
| 
 | ||||
| 		this.connection.on('file_created', this.onStreamDriveFileCreated); | ||||
| 		this.connection.on('file_updated', this.onStreamDriveFileUpdated); | ||||
|  | @ -132,12 +130,7 @@ export default Vue.extend({ | |||
| 		} | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('file_created', this.onStreamDriveFileCreated); | ||||
| 		this.connection.off('file_updated', this.onStreamDriveFileUpdated); | ||||
| 		this.connection.off('file_deleted', this.onStreamDriveFileDeleted); | ||||
| 		this.connection.off('folder_created', this.onStreamDriveFolderCreated); | ||||
| 		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); | ||||
| 		(this as any).os.streams.driveStream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onContextmenu(e) { | ||||
|  |  | |||
|  | @ -34,23 +34,18 @@ export default Vue.extend({ | |||
| 		return { | ||||
| 			u: this.user, | ||||
| 			wait: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 
 | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 		this.connection.on('follow', this.onFollow); | ||||
| 		this.connection.on('unfollow', this.onUnfollow); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('follow', this.onFollow); | ||||
| 		this.connection.off('unfollow', this.onUnfollow); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -141,7 +141,6 @@ export default Vue.extend({ | |||
| 	data() { | ||||
| 		return { | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			widgetAdderSelected: null, | ||||
| 			trash: [] | ||||
| 		}; | ||||
|  | @ -176,12 +175,11 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -93,12 +93,15 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue'; | |||
| import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './notes.note.sub.vue'; | ||||
| import { sum } from '../../../../../prelude/array'; | ||||
| import noteSubscriber from '../../../common/scripts/note-subscriber'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XSub | ||||
| 	}, | ||||
| 
 | ||||
| 	mixins: [noteSubscriber('note')], | ||||
| 
 | ||||
| 	props: { | ||||
| 		note: { | ||||
| 			type: Object, | ||||
|  |  | |||
|  | @ -77,6 +77,7 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue'; | |||
| import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './notes.note.sub.vue'; | ||||
| import { sum } from '../../../../../prelude/array'; | ||||
| import noteSubscriber from '../../../common/scripts/note-subscriber'; | ||||
| 
 | ||||
| function focus(el, fn) { | ||||
| 	const target = fn(el); | ||||
|  | @ -94,6 +95,8 @@ export default Vue.extend({ | |||
| 		XSub | ||||
| 	}, | ||||
| 
 | ||||
| 	mixins: [noteSubscriber('note')], | ||||
| 
 | ||||
| 	props: { | ||||
| 		note: { | ||||
| 			type: Object, | ||||
|  | @ -104,9 +107,7 @@ export default Vue.extend({ | |||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false, | ||||
| 			isDetailOpened: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			isDetailOpened: false | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -168,86 +169,7 @@ export default Vue.extend({ | |||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.stream.use(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.capture(true); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.on('_connected_', this.onStreamConnected); | ||||
| 		} | ||||
| 
 | ||||
| 		// Draw map | ||||
| 		if (this.p.geo) { | ||||
| 			const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true; | ||||
| 			if (shouldShowMap) { | ||||
| 				(this as any).os.getGoogleMaps().then(maps => { | ||||
| 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); | ||||
| 					const map = new maps.Map(this.$refs.map, { | ||||
| 						center: uluru, | ||||
| 						zoom: 15 | ||||
| 					}); | ||||
| 					new maps.Marker({ | ||||
| 						position: uluru, | ||||
| 						map: map | ||||
| 					}); | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.decapture(true); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('_connected_', this.onStreamConnected); | ||||
| 			(this as any).os.stream.dispose(this.connectionId); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		capture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				const data = { | ||||
| 					type: 'capture', | ||||
| 					id: this.p.id | ||||
| 				} as any; | ||||
| 				if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) { | ||||
| 					data.read = true; | ||||
| 				} | ||||
| 				this.connection.send(data); | ||||
| 				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		decapture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				this.connection.send({ | ||||
| 					type: 'decapture', | ||||
| 					id: this.p.id | ||||
| 				}); | ||||
| 				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onStreamConnected() { | ||||
| 			this.capture(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onStreamNoteUpdated(data) { | ||||
| 			const note = data.note; | ||||
| 			if (note.id == this.note.id) { | ||||
| 				this.$emit('update:note', note); | ||||
| 			} else if (note.id == this.note.renoteId) { | ||||
| 				this.note.renote = note; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		reply(viaKeyboard = false) { | ||||
| 			(this as any).os.new(MkPostFormWindow, { | ||||
| 				reply: this.p, | ||||
|  |  | |||
|  | @ -118,10 +118,10 @@ export default Vue.extend({ | |||
| 			notifications: [], | ||||
| 			moreNotifications: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			getNoteSummary | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		_notifications(): any[] { | ||||
| 			return (this.notifications as any).map(notification => { | ||||
|  | @ -133,9 +133,9 @@ export default Vue.extend({ | |||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 		this.connection.on('notification', this.onNotification); | ||||
| 
 | ||||
|  | @ -153,10 +153,11 @@ export default Vue.extend({ | |||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('notification', this.onNotification); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetchMoreNotifications() { | ||||
| 			this.fetchingMoreNotifications = true; | ||||
|  | @ -177,10 +178,11 @@ export default Vue.extend({ | |||
| 				this.fetchingMoreNotifications = false; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		onNotification(notification) { | ||||
| 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない | ||||
| 			this.connection.send({ | ||||
| 				type: 'read_notification', | ||||
| 				type: 'readNotification', | ||||
| 				id: notification.id | ||||
| 			}); | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,25 +23,25 @@ export default Vue.extend({ | |||
| 		return { | ||||
| 			fetching: true, | ||||
| 			signins: [], | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		(this as any).api('i/signin_history').then(signins => { | ||||
| 			this.signins = signins; | ||||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 
 | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 		this.connection.on('signin', this.onSignin); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('signin', this.onSignin); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		onSignin(signin) { | ||||
| 			this.signins.unshift(signin); | ||||
|  |  | |||
|  | @ -15,7 +15,6 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { HashtagStream } from '../../../common/scripts/streaming/hashtag'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
|  | @ -35,9 +34,7 @@ export default Vue.extend({ | |||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			streamManager: null, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			date: null, | ||||
| 			baseQuery: { | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
|  | @ -69,69 +66,33 @@ export default Vue.extend({ | |||
| 			this.query = { | ||||
| 				query: this.tagTl.query | ||||
| 			}; | ||||
| 			this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||
| 			this.connection = (this as any).os.stream.connectToChannel('hashtag', { q: this.tagTl.query }); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.connection.close(); | ||||
| 			}); | ||||
| 		} else if (this.src == 'home') { | ||||
| 			this.endpoint = 'notes/timeline'; | ||||
| 			const onChangeFollowing = () => { | ||||
| 				this.fetch(); | ||||
| 			}; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('homeTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.connection.on('follow', onChangeFollowing); | ||||
| 			this.connection.on('unfollow', onChangeFollowing); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.connection.off('follow', onChangeFollowing); | ||||
| 				this.connection.off('unfollow', onChangeFollowing); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'local') { | ||||
| 			this.endpoint = 'notes/local-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.localTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('localTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.endpoint = 'notes/hybrid-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.hybridTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('hybridTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.endpoint = 'notes/global-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.globalTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('globalTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'mentions') { | ||||
| 			this.endpoint = 'notes/mentions'; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 			this.connection.on('mention', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('mention', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'messages') { | ||||
| 			this.endpoint = 'notes/mentions'; | ||||
| 			this.query = { | ||||
|  | @ -142,21 +103,15 @@ export default Vue.extend({ | |||
| 					prepend(note); | ||||
| 				} | ||||
| 			}; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 			this.connection.on('mention', onNote); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('mention', onNote); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.$emit('beforeDestroy'); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -42,8 +42,7 @@ export default Vue.extend({ | |||
| 	data() { | ||||
| 		return { | ||||
| 			hasGameInvitations: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
|  | @ -53,18 +52,15 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 	mounted() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.stream.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 			this.connection.on('reversi_invited', this.onReversiInvited); | ||||
| 			this.connection.on('reversiInvited', this.onReversiInvited); | ||||
| 			this.connection.on('reversi_no_invites', this.onReversiNoInvites); | ||||
| 		} | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('reversi_invited', this.onReversiInvited); | ||||
| 			this.connection.off('reversi_no_invites', this.onReversiNoInvites); | ||||
| 			(this as any).os.stream.dispose(this.connectionId); | ||||
| 			this.connection.dispose(); | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { UserListStream } from '../../../common/scripts/streaming/user-list'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
|  |  | |||
|  | @ -56,13 +56,11 @@ export default Vue.extend({ | |||
| 			disableLocalTimeline: false, | ||||
| 			bannerUrl: null, | ||||
| 			inviteCode: null, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.connection = (this as any).os.streams.serverStatsStream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.streams.serverStatsStream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('serverStats'); | ||||
| 
 | ||||
| 		(this as any).os.getMeta().then(meta => { | ||||
| 			this.disableRegistration = meta.disableRegistration; | ||||
|  | @ -75,7 +73,7 @@ export default Vue.extend({ | |||
| 		}); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		(this as any).os.streams.serverStatsStream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		invite() { | ||||
|  |  | |||
|  | @ -21,23 +21,19 @@ export default Vue.extend({ | |||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 
 | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 		this.connection.on('mention', this.onNote); | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('mention', this.onNote); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ | |||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
| import { HashtagStream } from '../../../../common/scripts/streaming/hashtag'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
|  | @ -48,7 +47,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 	mounted() { | ||||
| 		if (this.connection) this.connection.close(); | ||||
| 		this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||
| 		this.connection = (this as any).os.stream.connectToChannel('hashtag', this.tagTl.query); | ||||
| 		this.connection.on('note', this.onNote); | ||||
| 
 | ||||
| 		this.fetch(); | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ | |||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
| import { UserListStream } from '../../../../common/scripts/streaming/user-list'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
|  |  | |||
|  | @ -21,23 +21,19 @@ export default Vue.extend({ | |||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 
 | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 		this.connection.on('mention', this.onNote); | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('mention', this.onNote); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -70,12 +70,15 @@ import parse from '../../../../../../mfm/parse'; | |||
| import MkNoteMenu from '../../../../common/views/components/note-menu.vue'; | ||||
| import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './deck.note.sub.vue'; | ||||
| import noteSubscriber from '../../../../common/scripts/note-subscriber'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XSub | ||||
| 	}, | ||||
| 
 | ||||
| 	mixins: [noteSubscriber('note')], | ||||
| 
 | ||||
| 	props: { | ||||
| 		note: { | ||||
| 			type: Object, | ||||
|  | @ -90,9 +93,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			showContent: false | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -120,68 +121,7 @@ export default Vue.extend({ | |||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.stream.use(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.capture(true); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.on('_connected_', this.onStreamConnected); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.decapture(true); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('_connected_', this.onStreamConnected); | ||||
| 			(this as any).os.stream.dispose(this.connectionId); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		capture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				const data = { | ||||
| 					type: 'capture', | ||||
| 					id: this.p.id | ||||
| 				} as any; | ||||
| 				if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) { | ||||
| 					data.read = true; | ||||
| 				} | ||||
| 				this.connection.send(data); | ||||
| 				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		decapture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				this.connection.send({ | ||||
| 					type: 'decapture', | ||||
| 					id: this.p.id | ||||
| 				}); | ||||
| 				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onStreamConnected() { | ||||
| 			this.capture(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onStreamNoteUpdated(data) { | ||||
| 			const note = data.note; | ||||
| 			if (note.id == this.note.id) { | ||||
| 				this.$emit('update:note', note); | ||||
| 			} else if (note.id == this.note.renoteId) { | ||||
| 				this.note.renote = note; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		reply() { | ||||
| 			(this as any).apis.post({ | ||||
| 				reply: this.p | ||||
|  |  | |||
|  | @ -38,8 +38,7 @@ export default Vue.extend({ | |||
| 			notifications: [], | ||||
| 			queue: [], | ||||
| 			moreNotifications: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -62,8 +61,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 		this.connection.on('notification', this.onNotification); | ||||
| 
 | ||||
|  | @ -86,8 +84,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('notification', this.onNotification); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 
 | ||||
| 		this.column.$off('top', this.onTop); | ||||
| 		this.column.$off('bottom', this.onBottom); | ||||
|  | @ -117,7 +114,7 @@ export default Vue.extend({ | |||
| 		onNotification(notification) { | ||||
| 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない | ||||
| 			this.connection.send({ | ||||
| 				type: 'read_notification', | ||||
| 				type: 'readNotification', | ||||
| 				id: notification.id | ||||
| 			}); | ||||
| 
 | ||||
|  |  | |||
|  | @ -36,18 +36,17 @@ export default Vue.extend({ | |||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		stream(): any { | ||||
| 			switch (this.src) { | ||||
| 				case 'home': return (this as any).os.stream; | ||||
| 				case 'local': return (this as any).os.streams.localTimelineStream; | ||||
| 				case 'hybrid': return (this as any).os.streams.hybridTimelineStream; | ||||
| 				case 'global': return (this as any).os.streams.globalTimelineStream; | ||||
| 				case 'home': return (this as any).os.stream.useSharedConnection('homeTimeline'); | ||||
| 				case 'local': return (this as any).os.stream.useSharedConnection('localTimeline'); | ||||
| 				case 'hybrid': return (this as any).os.stream.useSharedConnection('hybridTimeline'); | ||||
| 				case 'global': return (this as any).os.stream.useSharedConnection('globalTimeline'); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
|  | @ -68,8 +67,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = this.stream.getConnection(); | ||||
| 		this.connectionId = this.stream.use(); | ||||
| 		this.connection = this.stream; | ||||
| 
 | ||||
| 		this.connection.on('note', this.onNote); | ||||
| 		if (this.src == 'home') { | ||||
|  | @ -81,12 +79,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('note', this.onNote); | ||||
| 		if (this.src == 'home') { | ||||
| 			this.connection.off('follow', this.onChangeFollowing); | ||||
| 			this.connection.off('unfollow', this.onChangeFollowing); | ||||
| 		} | ||||
| 		this.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Vue from 'vue'; | ||||
| import { EventEmitter } from 'eventemitter3'; | ||||
| import * as uuid from 'uuid'; | ||||
|  | @ -5,19 +6,9 @@ import * as uuid from 'uuid'; | |||
| import initStore from './store'; | ||||
| import { apiUrl, version, lang } from './config'; | ||||
| import Progress from './common/scripts/loading'; | ||||
| import Connection from './common/scripts/streaming/stream'; | ||||
| import { HomeStreamManager } from './common/scripts/streaming/home'; | ||||
| import { DriveStreamManager } from './common/scripts/streaming/drive'; | ||||
| import { ServerStatsStreamManager } from './common/scripts/streaming/server-stats'; | ||||
| import { NotesStatsStreamManager } from './common/scripts/streaming/notes-stats'; | ||||
| import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index'; | ||||
| import { ReversiStreamManager } from './common/scripts/streaming/games/reversi/reversi'; | ||||
| 
 | ||||
| import Err from './common/views/components/connect-failed.vue'; | ||||
| import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline'; | ||||
| import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline'; | ||||
| import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline'; | ||||
| import { erase } from '../../prelude/array'; | ||||
| import Stream from './common/scripts/stream'; | ||||
| 
 | ||||
| //#region api requests
 | ||||
| let spinner = null; | ||||
|  | @ -102,30 +93,7 @@ export default class MiOS extends EventEmitter { | |||
| 	/** | ||||
| 	 * A connection manager of home stream | ||||
| 	 */ | ||||
| 	public stream: HomeStreamManager; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Connection managers | ||||
| 	 */ | ||||
| 	public streams: { | ||||
| 		localTimelineStream: LocalTimelineStreamManager; | ||||
| 		hybridTimelineStream: HybridTimelineStreamManager; | ||||
| 		globalTimelineStream: GlobalTimelineStreamManager; | ||||
| 		driveStream: DriveStreamManager; | ||||
| 		serverStatsStream: ServerStatsStreamManager; | ||||
| 		notesStatsStream: NotesStatsStreamManager; | ||||
| 		messagingIndexStream: MessagingIndexStreamManager; | ||||
| 		reversiStream: ReversiStreamManager; | ||||
| 	} = { | ||||
| 		localTimelineStream: null, | ||||
| 		hybridTimelineStream: null, | ||||
| 		globalTimelineStream: null, | ||||
| 		driveStream: null, | ||||
| 		serverStatsStream: null, | ||||
| 		notesStatsStream: null, | ||||
| 		messagingIndexStream: null, | ||||
| 		reversiStream: null | ||||
| 	}; | ||||
| 	public stream: Stream; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * A registration of service worker | ||||
|  | @ -151,71 +119,36 @@ export default class MiOS extends EventEmitter { | |||
| 
 | ||||
| 		this.shouldRegisterSw = shouldRegisterSw; | ||||
| 
 | ||||
| 		//#region BIND
 | ||||
| 		this.log = this.log.bind(this); | ||||
| 		this.logInfo = this.logInfo.bind(this); | ||||
| 		this.logWarn = this.logWarn.bind(this); | ||||
| 		this.logError = this.logError.bind(this); | ||||
| 		this.init = this.init.bind(this); | ||||
| 		this.api = this.api.bind(this); | ||||
| 		this.getMeta = this.getMeta.bind(this); | ||||
| 		this.registerSw = this.registerSw.bind(this); | ||||
| 		//#endregion
 | ||||
| 
 | ||||
| 		if (this.debug) { | ||||
| 			(window as any).os = this; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private googleMapsIniting = false; | ||||
| 
 | ||||
| 	public getGoogleMaps() { | ||||
| 		return new Promise((res, rej) => { | ||||
| 			if ((window as any).google && (window as any).google.maps) { | ||||
| 				res((window as any).google.maps); | ||||
| 			} else { | ||||
| 				this.once('init-google-maps', () => { | ||||
| 					res((window as any).google.maps); | ||||
| 				}); | ||||
| 
 | ||||
| 				//#region load google maps api
 | ||||
| 				if (!this.googleMapsIniting) { | ||||
| 					this.googleMapsIniting = true; | ||||
| 					(window as any).initGoogleMaps = () => { | ||||
| 						this.emit('init-google-maps'); | ||||
| 					}; | ||||
| 					const head = document.getElementsByTagName('head')[0]; | ||||
| 					const script = document.createElement('script'); | ||||
| 					script.setAttribute('src', `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&callback=initGoogleMaps`); | ||||
| 					script.setAttribute('async', 'true'); | ||||
| 					script.setAttribute('defer', 'true'); | ||||
| 					head.appendChild(script); | ||||
| 				} | ||||
| 				//#endregion
 | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public log(...args) { | ||||
| 		if (!this.debug) return; | ||||
| 		console.log.apply(null, args); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public logInfo(...args) { | ||||
| 		if (!this.debug) return; | ||||
| 		console.info.apply(null, args); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public logWarn(...args) { | ||||
| 		if (!this.debug) return; | ||||
| 		console.warn.apply(null, args); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public logError(...args) { | ||||
| 		if (!this.debug) return; | ||||
| 		console.error.apply(null, args); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public signout() { | ||||
| 		this.store.dispatch('logout'); | ||||
| 		location.href = '/'; | ||||
|  | @ -225,27 +158,10 @@ export default class MiOS extends EventEmitter { | |||
| 	 * Initialize MiOS (boot) | ||||
| 	 * @param callback A function that call when initialized | ||||
| 	 */ | ||||
| 	@autobind | ||||
| 	public async init(callback) { | ||||
| 		this.store = initStore(this); | ||||
| 
 | ||||
| 		//#region Init stream managers
 | ||||
| 		this.streams.serverStatsStream = new ServerStatsStreamManager(this); | ||||
| 		this.streams.notesStatsStream = new NotesStatsStreamManager(this); | ||||
| 		this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.store.state.i); | ||||
| 
 | ||||
| 		this.once('signedin', () => { | ||||
| 			// Init home stream manager
 | ||||
| 			this.stream = new HomeStreamManager(this, this.store.state.i); | ||||
| 
 | ||||
| 			// Init other stream manager
 | ||||
| 			this.streams.hybridTimelineStream = new HybridTimelineStreamManager(this, this.store.state.i); | ||||
| 			this.streams.globalTimelineStream = new GlobalTimelineStreamManager(this, this.store.state.i); | ||||
| 			this.streams.driveStream = new DriveStreamManager(this, this.store.state.i); | ||||
| 			this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.store.state.i); | ||||
| 			this.streams.reversiStream = new ReversiStreamManager(this, this.store.state.i); | ||||
| 		}); | ||||
| 		//#endregion
 | ||||
| 
 | ||||
| 		// ユーザーをフェッチしてコールバックする
 | ||||
| 		const fetchme = (token, cb) => { | ||||
| 			let me = null; | ||||
|  | @ -296,6 +212,8 @@ export default class MiOS extends EventEmitter { | |||
| 		const fetched = () => { | ||||
| 			this.emit('signedin'); | ||||
| 
 | ||||
| 			this.stream = new Stream(this); | ||||
| 
 | ||||
| 			// Finish init
 | ||||
| 			callback(); | ||||
| 
 | ||||
|  | @ -328,6 +246,8 @@ export default class MiOS extends EventEmitter { | |||
| 				} else { | ||||
| 					// Finish init
 | ||||
| 					callback(); | ||||
| 
 | ||||
| 					this.stream = new Stream(this); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
|  | @ -336,6 +256,7 @@ export default class MiOS extends EventEmitter { | |||
| 	/** | ||||
| 	 * Register service worker | ||||
| 	 */ | ||||
| 	@autobind | ||||
| 	private registerSw() { | ||||
| 		// Check whether service worker and push manager supported
 | ||||
| 		const isSwSupported = | ||||
|  | @ -418,7 +339,8 @@ export default class MiOS extends EventEmitter { | |||
| 	 * @param endpoint エンドポイント名 | ||||
| 	 * @param data パラメータ | ||||
| 	 */ | ||||
| 	public api(endpoint: string, data: { [x: string]: any } = {}): Promise<{ [x: string]: any }> { | ||||
| 	@autobind | ||||
| 	public api(endpoint: string, data: { [x: string]: any } = {}, forceFetch = false): Promise<{ [x: string]: any }> { | ||||
| 		if (++pending === 1) { | ||||
| 			spinner = document.createElement('div'); | ||||
| 			spinner.setAttribute('id', 'wait'); | ||||
|  | @ -430,13 +352,12 @@ export default class MiOS extends EventEmitter { | |||
| 		}; | ||||
| 
 | ||||
| 		const promise = new Promise((resolve, reject) => { | ||||
| 			const viaStream = this.stream && this.stream.hasConnection && this.store.state.device.apiViaStream; | ||||
| 			const viaStream = this.stream && this.store.state.device.apiViaStream && !forceFetch; | ||||
| 
 | ||||
| 			if (viaStream) { | ||||
| 				const stream = this.stream.borrow(); | ||||
| 				const id = Math.random().toString(); | ||||
| 
 | ||||
| 				stream.once(`api-res:${id}`, res => { | ||||
| 				this.stream.once(`api:${id}`, res => { | ||||
| 					if (res == null || Object.keys(res).length == 0) { | ||||
| 						resolve(null); | ||||
| 					} else if (res.res) { | ||||
|  | @ -446,11 +367,10 @@ export default class MiOS extends EventEmitter { | |||
| 					} | ||||
| 				}); | ||||
| 
 | ||||
| 				stream.send({ | ||||
| 					type: 'api', | ||||
| 					id, | ||||
| 					endpoint, | ||||
| 					data | ||||
| 				this.stream.send('api', { | ||||
| 					id: id, | ||||
| 					ep: endpoint, | ||||
| 					data: data | ||||
| 				}); | ||||
| 			} else { | ||||
| 				// Append a credential
 | ||||
|  | @ -503,6 +423,7 @@ export default class MiOS extends EventEmitter { | |||
| 	 * Misskeyのメタ情報を取得します | ||||
| 	 * @param force キャッシュを無視するか否か | ||||
| 	 */ | ||||
| 	@autobind | ||||
| 	public getMeta(force = false) { | ||||
| 		return new Promise<{ [x: string]: any }>(async (res, rej) => { | ||||
| 			if (this.isMetaFetching) { | ||||
|  | @ -530,16 +451,6 @@ export default class MiOS extends EventEmitter { | |||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	public connections: Connection[] = []; | ||||
| 
 | ||||
| 	public registerStreamConnection(connection: Connection) { | ||||
| 		this.connections.push(connection); | ||||
| 	} | ||||
| 
 | ||||
| 	public unregisterStreamConnection(connection: Connection) { | ||||
| 		this.connections = erase(connection, this.connections); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| class WindowSystem extends EventEmitter { | ||||
|  |  | |||
|  | @ -81,8 +81,7 @@ export default Vue.extend({ | |||
| 			hierarchyFolders: [], | ||||
| 			selectedFiles: [], | ||||
| 			info: null, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			connection: null | ||||
| 
 | ||||
| 			fetching: true, | ||||
| 			fetchingMoreFiles: false, | ||||
|  | @ -102,8 +101,7 @@ export default Vue.extend({ | |||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.streams.driveStream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.streams.driveStream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('drive'); | ||||
| 
 | ||||
| 		this.connection.on('file_created', this.onStreamDriveFileCreated); | ||||
| 		this.connection.on('file_updated', this.onStreamDriveFileUpdated); | ||||
|  | @ -124,12 +122,7 @@ export default Vue.extend({ | |||
| 		} | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('file_created', this.onStreamDriveFileCreated); | ||||
| 		this.connection.off('file_updated', this.onStreamDriveFileUpdated); | ||||
| 		this.connection.off('file_deleted', this.onStreamDriveFileDeleted); | ||||
| 		this.connection.off('folder_created', this.onStreamDriveFolderCreated); | ||||
| 		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); | ||||
| 		(this as any).os.streams.driveStream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onStreamDriveFileCreated(file) { | ||||
|  |  | |||
|  | @ -28,21 +28,17 @@ export default Vue.extend({ | |||
| 		return { | ||||
| 			u: this.user, | ||||
| 			wait: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 		this.connection.on('follow', this.onFollow); | ||||
| 		this.connection.on('unfollow', this.onUnfollow); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('follow', this.onFollow); | ||||
| 		this.connection.off('unfollow', this.onUnfollow); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 
 | ||||
|  |  | |||
|  | @ -92,12 +92,15 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue'; | |||
| import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './note.sub.vue'; | ||||
| import { sum } from '../../../../../prelude/array'; | ||||
| import noteSubscriber from '../../../common/scripts/note-subscriber'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XSub | ||||
| 	}, | ||||
| 
 | ||||
| 	mixins: [noteSubscriber('note')], | ||||
| 
 | ||||
| 	props: { | ||||
| 		note: { | ||||
| 			type: Object, | ||||
|  |  | |||
|  | @ -69,19 +69,20 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue'; | |||
| import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './note.sub.vue'; | ||||
| import { sum } from '../../../../../prelude/array'; | ||||
| import noteSubscriber from '../../../common/scripts/note-subscriber'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XSub | ||||
| 	}, | ||||
| 
 | ||||
| 	mixins: [noteSubscriber('note')], | ||||
| 
 | ||||
| 	props: ['note'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			showContent: false | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -115,86 +116,7 @@ export default Vue.extend({ | |||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.stream.use(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.capture(true); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.on('_connected_', this.onStreamConnected); | ||||
| 		} | ||||
| 
 | ||||
| 		// Draw map | ||||
| 		if (this.p.geo) { | ||||
| 			const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true; | ||||
| 			if (shouldShowMap) { | ||||
| 				(this as any).os.getGoogleMaps().then(maps => { | ||||
| 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); | ||||
| 					const map = new maps.Map(this.$refs.map, { | ||||
| 						center: uluru, | ||||
| 						zoom: 15 | ||||
| 					}); | ||||
| 					new maps.Marker({ | ||||
| 						position: uluru, | ||||
| 						map: map | ||||
| 					}); | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.decapture(true); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('_connected_', this.onStreamConnected); | ||||
| 			(this as any).os.stream.dispose(this.connectionId); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		capture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				const data = { | ||||
| 					type: 'capture', | ||||
| 					id: this.p.id | ||||
| 				} as any; | ||||
| 				if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) { | ||||
| 					data.read = true; | ||||
| 				} | ||||
| 				this.connection.send(data); | ||||
| 				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		decapture(withHandler = false) { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				this.connection.send({ | ||||
| 					type: 'decapture', | ||||
| 					id: this.p.id | ||||
| 				}); | ||||
| 				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onStreamConnected() { | ||||
| 			this.capture(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onStreamNoteUpdated(data) { | ||||
| 			const note = data.note; | ||||
| 			if (note.id == this.note.id) { | ||||
| 				this.$emit('update:note', note); | ||||
| 			} else if (note.id == this.note.renoteId) { | ||||
| 				this.note.renote = note; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		reply() { | ||||
| 			(this as any).apis.post({ | ||||
| 				reply: this.p | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
|  | @ -30,10 +31,10 @@ export default Vue.extend({ | |||
| 			fetchingMoreNotifications: false, | ||||
| 			notifications: [], | ||||
| 			moreNotifications: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		_notifications(): any[] { | ||||
| 			return (this.notifications as any).map(notification => { | ||||
|  | @ -45,9 +46,9 @@ export default Vue.extend({ | |||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 		this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 		this.connection.on('notification', this.onNotification); | ||||
| 
 | ||||
|  | @ -66,10 +67,11 @@ export default Vue.extend({ | |||
| 			this.$emit('fetched'); | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('notification', this.onNotification); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetchMoreNotifications() { | ||||
| 			this.fetchingMoreNotifications = true; | ||||
|  | @ -90,10 +92,11 @@ export default Vue.extend({ | |||
| 				this.fetchingMoreNotifications = false; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		onNotification(notification) { | ||||
| 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない | ||||
| 			this.connection.send({ | ||||
| 				type: 'read_notification', | ||||
| 				type: 'readNotification', | ||||
| 				id: notification.id | ||||
| 			}); | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,44 +24,47 @@ import { env } from '../../../config'; | |||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	props: ['func'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			hasGameInvitation: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			env: env | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		hasUnreadNotification(): boolean { | ||||
| 			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; | ||||
| 		}, | ||||
| 
 | ||||
| 		hasUnreadMessagingMessage(): boolean { | ||||
| 			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$store.commit('setUiHeaderHeight', this.$refs.root.offsetHeight); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.stream.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 			this.connection.on('reversi_invited', this.onReversiInvited); | ||||
| 			this.connection.on('reversiInvited', this.onReversiInvited); | ||||
| 			this.connection.on('reversi_no_invites', this.onReversiNoInvites); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('reversi_invited', this.onReversiInvited); | ||||
| 			this.connection.off('reversi_no_invites', this.onReversiNoInvites); | ||||
| 			(this as any).os.stream.dispose(this.connectionId); | ||||
| 			this.connection.dispose(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		onReversiInvited() { | ||||
| 			this.hasGameInvitation = true; | ||||
| 		}, | ||||
| 
 | ||||
| 		onReversiNoInvites() { | ||||
| 			this.hasGameInvitation = false; | ||||
| 		} | ||||
|  |  | |||
|  | @ -57,7 +57,6 @@ export default Vue.extend({ | |||
| 		return { | ||||
| 			hasGameInvitation: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			aboutUrl: `/docs/${lang}/about`, | ||||
| 			announcements: [] | ||||
| 		}; | ||||
|  | @ -79,19 +78,16 @@ export default Vue.extend({ | |||
| 		}); | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.stream.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 			this.connection.on('reversi_invited', this.onReversiInvited); | ||||
| 			this.connection.on('reversiInvited', this.onReversiInvited); | ||||
| 			this.connection.on('reversi_no_invites', this.onReversiNoInvites); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('reversi_invited', this.onReversiInvited); | ||||
| 			this.connection.off('reversi_no_invites', this.onReversiNoInvites); | ||||
| 			(this as any).os.stream.dispose(this.connectionId); | ||||
| 			this.connection.dispose(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,40 +23,43 @@ export default Vue.extend({ | |||
| 		XHeader, | ||||
| 		XNav | ||||
| 	}, | ||||
| 
 | ||||
| 	props: ['title'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			isDrawerOpening: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		'$store.state.uiHeaderHeight'() { | ||||
| 			this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.stream.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 
 | ||||
| 			this.connection.on('notification', this.onNotification); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('notification', this.onNotification); | ||||
| 			(this as any).os.stream.dispose(this.connectionId); | ||||
| 			this.connection.dispose(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		onNotification(notification) { | ||||
| 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない | ||||
| 			this.connection.send({ | ||||
| 				type: 'read_notification', | ||||
| 				type: 'readNotification', | ||||
| 				id: notification.id | ||||
| 			}); | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { UserListStream } from '../../../common/scripts/streaming/user-list'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,7 +13,6 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { HashtagStream } from '../../../common/scripts/streaming/hashtag'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
|  | @ -35,7 +34,6 @@ export default Vue.extend({ | |||
| 			existMore: false, | ||||
| 			streamManager: null, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			unreadCount: 0, | ||||
| 			date: null, | ||||
| 			baseQuery: { | ||||
|  | @ -68,69 +66,33 @@ export default Vue.extend({ | |||
| 			this.query = { | ||||
| 				query: this.tagTl.query | ||||
| 			}; | ||||
| 			this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||
| 			this.connection = (this as any).os.stream.connectToChannel('hashtag', { q: this.tagTl.query }); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.connection.close(); | ||||
| 			}); | ||||
| 		} else if (this.src == 'home') { | ||||
| 			this.endpoint = 'notes/timeline'; | ||||
| 			const onChangeFollowing = () => { | ||||
| 				this.fetch(); | ||||
| 			}; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('homeTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.connection.on('follow', onChangeFollowing); | ||||
| 			this.connection.on('unfollow', onChangeFollowing); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.connection.off('follow', onChangeFollowing); | ||||
| 				this.connection.off('unfollow', onChangeFollowing); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'local') { | ||||
| 			this.endpoint = 'notes/local-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.localTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('localTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.endpoint = 'notes/hybrid-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.hybridTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('hybridTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.endpoint = 'notes/global-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.globalTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('globalTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'mentions') { | ||||
| 			this.endpoint = 'notes/mentions'; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 			this.connection.on('mention', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('mention', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'messages') { | ||||
| 			this.endpoint = 'notes/mentions'; | ||||
| 			this.query = { | ||||
|  | @ -141,21 +103,15 @@ export default Vue.extend({ | |||
| 					prepend(note); | ||||
| 				} | ||||
| 			}; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection = (this as any).os.stream.useSharedConnection('main'); | ||||
| 			this.connection.on('mention', onNote); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('mention', onNote); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.$emit('beforeDestroy'); | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -14,7 +14,8 @@ | |||
|     "removeComments": false, | ||||
|     "noLib": false, | ||||
|     "strict": true, | ||||
|     "strictNullChecks": false | ||||
|     "strictNullChecks": false, | ||||
|     "experimentalDecorators": true | ||||
|   }, | ||||
|   "compileOnSave": false, | ||||
|   "include": [ | ||||
|  |  | |||
|  | @ -55,7 +55,7 @@ APIへリクエストすると、レスポンスがストリームから次の | |||
| 
 | ||||
| ```json | ||||
| { | ||||
| 	type: 'api-res:xxxxxxxxxxxxxxxx', | ||||
| 	type: 'api:xxxxxxxxxxxxxxxx', | ||||
| 	body: { | ||||
| 		... | ||||
| 	} | ||||
|  | @ -95,7 +95,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま | |||
| 
 | ||||
| ```json | ||||
| { | ||||
| 	type: 'note-updated', | ||||
| 	type: 'noteUpdated', | ||||
| 	body: { | ||||
| 		note: { | ||||
| 			... | ||||
|  | @ -108,7 +108,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま | |||
| 
 | ||||
| --- | ||||
| 
 | ||||
| このように、投稿の情報が更新されると、`note-updated`イベントが流れてくるようになります。`note-updated`イベントが発生するのは、以下の場合です: | ||||
| このように、投稿の情報が更新されると、`noteUpdated`イベントが流れてくるようになります。`noteUpdated`イベントが発生するのは、以下の場合です: | ||||
| 
 | ||||
| - 投稿にリアクションが付いた | ||||
| - 投稿に添付されたアンケートに投票がされた | ||||
|  | @ -153,7 +153,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま | |||
| 
 | ||||
| `body`プロパティの中に、投稿情報が含まれています。 | ||||
| 
 | ||||
| ### `read_all_notifications` | ||||
| ### `readAllNotifications` | ||||
| 
 | ||||
| 自分宛ての通知がすべて既読になったことを表すイベントです。このイベントを利用して、「通知があることを示すアイコン」のようなものをオフにしたりする等のケースが想定されます。 | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import * as mongo from 'mongodb'; | |||
| import Notification from './models/notification'; | ||||
| import Mute from './models/mute'; | ||||
| import { pack } from './models/notification'; | ||||
| import { publishUserStream } from './stream'; | ||||
| import { publishMainStream } from './stream'; | ||||
| import User from './models/user'; | ||||
| import pushSw from './push-sw'; | ||||
| 
 | ||||
|  | @ -30,7 +30,7 @@ export default ( | |||
| 	const packed = await pack(notification); | ||||
| 
 | ||||
| 	// Publish notification event
 | ||||
| 	publishUserStream(notifiee, 'notification', packed); | ||||
| 	publishMainStream(notifiee, 'notification', packed); | ||||
| 
 | ||||
| 	// Update flag
 | ||||
| 	User.update({ _id: notifiee }, { | ||||
|  | @ -54,7 +54,7 @@ export default ( | |||
| 			} | ||||
| 			//#endregion
 | ||||
| 
 | ||||
| 			publishUserStream(notifiee, 'unread_notification', packed); | ||||
| 			publishMainStream(notifiee, 'unreadNotification', packed); | ||||
| 
 | ||||
| 			pushSw(notifiee, 'notification', packed); | ||||
| 		} | ||||
|  |  | |||
|  | @ -9,6 +9,10 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any) | |||
| 
 | ||||
| 	const ep = endpoints.find(e => e.name === endpoint); | ||||
| 
 | ||||
| 	if (ep == null) { | ||||
| 		return rej('ENDPOINT_NOT_FOUND'); | ||||
| 	} | ||||
| 
 | ||||
| 	if (ep.meta.secure && !isSecure) { | ||||
| 		return rej('ACCESS_DENIED'); | ||||
| 	} | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import * as mongo from 'mongodb'; | ||||
| import Message from '../../../models/messaging-message'; | ||||
| import { IMessagingMessage as IMessage } from '../../../models/messaging-message'; | ||||
| import { publishUserStream } from '../../../stream'; | ||||
| import { publishMainStream } from '../../../stream'; | ||||
| import { publishMessagingStream } from '../../../stream'; | ||||
| import { publishMessagingIndexStream } from '../../../stream'; | ||||
| import User from '../../../models/user'; | ||||
|  | @ -71,6 +71,6 @@ export default ( | |||
| 		}); | ||||
| 
 | ||||
| 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | ||||
| 		publishUserStream(userId, 'read_all_messaging_messages'); | ||||
| 		publishMainStream(userId, 'readAllMessagingMessages'); | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import * as mongo from 'mongodb'; | ||||
| import { default as Notification, INotification } from '../../../models/notification'; | ||||
| import { publishUserStream } from '../../../stream'; | ||||
| import { publishMainStream } from '../../../stream'; | ||||
| import Mute from '../../../models/mute'; | ||||
| import User from '../../../models/user'; | ||||
| 
 | ||||
|  | @ -66,6 +66,6 @@ export default ( | |||
| 		}); | ||||
| 
 | ||||
| 		// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
 | ||||
| 		publishUserStream(userId, 'read_all_notifications'); | ||||
| 		publishMainStream(userId, 'readAllNotifications'); | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import $ from 'cafy'; import ID from '../../../../../misc/cafy-id'; | |||
| import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching'; | ||||
| import ReversiGame, { pack as packGame } from '../../../../../models/games/reversi/game'; | ||||
| import User, { ILocalUser } from '../../../../../models/user'; | ||||
| import { publishUserStream, publishReversiStream } from '../../../../../stream'; | ||||
| import { publishMainStream, publishReversiStream } from '../../../../../stream'; | ||||
| import { eighteight } from '../../../../../games/reversi/maps'; | ||||
| 
 | ||||
| export const meta = { | ||||
|  | @ -58,7 +58,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = | |||
| 		}); | ||||
| 
 | ||||
| 		if (other == 0) { | ||||
| 			publishUserStream(user._id, 'reversi_no_invites'); | ||||
| 			publishMainStream(user._id, 'reversi_no_invites'); | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Fetch child
 | ||||
|  | @ -94,6 +94,6 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = | |||
| 		// 招待
 | ||||
| 		publishReversiStream(child._id, 'invited', packed); | ||||
| 
 | ||||
| 		publishUserStream(child._id, 'reversi_invited', packed); | ||||
| 		publishMainStream(child._id, 'reversiInvited', packed); | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import $ from 'cafy'; | ||||
| import * as bcrypt from 'bcryptjs'; | ||||
| import User, { ILocalUser } from '../../../../models/user'; | ||||
| import { publishUserStream } from '../../../../stream'; | ||||
| import { publishMainStream } from '../../../../stream'; | ||||
| import generateUserToken from '../../common/generate-native-user-token'; | ||||
| 
 | ||||
| export const meta = { | ||||
|  | @ -33,5 +33,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, | |||
| 	res(); | ||||
| 
 | ||||
| 	// Publish event
 | ||||
| 	publishUserStream(user._id, 'my_token_regenerated'); | ||||
| 	publishMainStream(user._id, 'myTokenRegenerated'); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; | ||||
| import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack, ILocalUser } from '../../../../models/user'; | ||||
| import { publishUserStream } from '../../../../stream'; | ||||
| import { publishMainStream } from '../../../../stream'; | ||||
| import DriveFile from '../../../../models/drive-file'; | ||||
| import acceptAllFollowRequests from '../../../../services/following/requests/accept-all'; | ||||
| import { IApp } from '../../../../models/app'; | ||||
|  | @ -177,7 +177,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a | |||
| 	res(iObj); | ||||
| 
 | ||||
| 	// Publish meUpdated event
 | ||||
| 	publishUserStream(user._id, 'meUpdated', iObj); | ||||
| 	publishMainStream(user._id, 'meUpdated', iObj); | ||||
| 
 | ||||
| 	// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
 | ||||
| 	if (user.isLocked && ps.isLocked === false) { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import $ from 'cafy'; | ||||
| import User, { ILocalUser } from '../../../../models/user'; | ||||
| import { publishUserStream } from '../../../../stream'; | ||||
| import { publishMainStream } from '../../../../stream'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	requireCredential: true, | ||||
|  | @ -26,7 +26,7 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, | |||
| 	res(); | ||||
| 
 | ||||
| 	// Publish event
 | ||||
| 	publishUserStream(user._id, 'clientSettingUpdated', { | ||||
| 	publishMainStream(user._id, 'clientSettingUpdated', { | ||||
| 		key: name, | ||||
| 		value | ||||
| 	}); | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import $ from 'cafy'; | ||||
| import User, { ILocalUser } from '../../../../models/user'; | ||||
| import { publishUserStream } from '../../../../stream'; | ||||
| import { publishMainStream } from '../../../../stream'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	requireCredential: true, | ||||
|  | @ -25,5 +25,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, | |||
| 
 | ||||
| 	res(); | ||||
| 
 | ||||
| 	publishUserStream(user._id, 'home_updated', home); | ||||
| 	publishMainStream(user._id, 'homeUpdated', home); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import $ from 'cafy'; | ||||
| import User, { ILocalUser } from '../../../../models/user'; | ||||
| import { publishUserStream } from '../../../../stream'; | ||||
| import { publishMainStream } from '../../../../stream'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	requireCredential: true, | ||||
|  | @ -24,5 +24,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, | |||
| 
 | ||||
| 	res(); | ||||
| 
 | ||||
| 	publishUserStream(user._id, 'mobile_home_updated', home); | ||||
| 	publishMainStream(user._id, 'mobileHomeUpdated', home); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import $ from 'cafy'; | ||||
| import User, { ILocalUser } from '../../../../models/user'; | ||||
| import { publishUserStream } from '../../../../stream'; | ||||
| import { publishMainStream } from '../../../../stream'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	requireCredential: true, | ||||
|  | @ -73,7 +73,7 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, | |||
| 	//#endregion
 | ||||
| 
 | ||||
| 	if (widget) { | ||||
| 		publishUserStream(user._id, 'widgetUpdated', { | ||||
| 		publishMainStream(user._id, 'widgetUpdated', { | ||||
| 			id, data | ||||
| 		}); | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ import User, { ILocalUser } from '../../../../../models/user'; | |||
| import Mute from '../../../../../models/mute'; | ||||
| import DriveFile from '../../../../../models/drive-file'; | ||||
| import { pack } from '../../../../../models/messaging-message'; | ||||
| import { publishUserStream } from '../../../../../stream'; | ||||
| import { publishMainStream } from '../../../../../stream'; | ||||
| import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../stream'; | ||||
| import pushSw from '../../../../../push-sw'; | ||||
| 
 | ||||
|  | @ -88,12 +88,12 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = | |||
| 	// 自分のストリーム
 | ||||
| 	publishMessagingStream(message.userId, message.recipientId, 'message', messageObj); | ||||
| 	publishMessagingIndexStream(message.userId, 'message', messageObj); | ||||
| 	publishUserStream(message.userId, 'messaging_message', messageObj); | ||||
| 	publishMainStream(message.userId, 'messagingMessage', messageObj); | ||||
| 
 | ||||
| 	// 相手のストリーム
 | ||||
| 	publishMessagingStream(message.recipientId, message.userId, 'message', messageObj); | ||||
| 	publishMessagingIndexStream(message.recipientId, 'message', messageObj); | ||||
| 	publishUserStream(message.recipientId, 'messaging_message', messageObj); | ||||
| 	publishMainStream(message.recipientId, 'messagingMessage', messageObj); | ||||
| 
 | ||||
| 	// Update flag
 | ||||
| 	User.update({ _id: recipient._id }, { | ||||
|  | @ -117,8 +117,8 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = | |||
| 			} | ||||
| 			//#endregion
 | ||||
| 
 | ||||
| 			publishUserStream(message.recipientId, 'unread_messaging_message', messageObj); | ||||
| 			pushSw(message.recipientId, 'unread_messaging_message', messageObj); | ||||
| 			publishMainStream(message.recipientId, 'unreadMessagingMessage', messageObj); | ||||
| 			pushSw(message.recipientId, 'unreadMessagingMessage', messageObj); | ||||
| 		} | ||||
| 	}, 3000); | ||||
| 
 | ||||
|  |  | |||
|  | @ -72,7 +72,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = | |||
| 		$inc: inc | ||||
| 	}); | ||||
| 
 | ||||
| 	publishNoteStream(note._id, 'poll_voted'); | ||||
| 	publishNoteStream(note._id, 'pollVoted', { | ||||
| 		choice: choice, | ||||
| 		userId: user._id.toHexString() | ||||
| 	}); | ||||
| 
 | ||||
| 	// Notify
 | ||||
| 	notify(note.userId, user._id, 'poll_vote', { | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import Notification from '../../../../models/notification'; | ||||
| import { publishUserStream } from '../../../../stream'; | ||||
| import { publishMainStream } from '../../../../stream'; | ||||
| import User, { ILocalUser } from '../../../../models/user'; | ||||
| 
 | ||||
| export const meta = { | ||||
|  | @ -40,5 +40,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = | |||
| 	}); | ||||
| 
 | ||||
| 	// 全ての通知を読みましたよというイベントを発行
 | ||||
| 	publishUserStream(user._id, 'read_all_notifications'); | ||||
| 	publishMainStream(user._id, 'readAllNotifications'); | ||||
| }); | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs'; | |||
| import * as speakeasy from 'speakeasy'; | ||||
| import User, { ILocalUser } from '../../../models/user'; | ||||
| import Signin, { pack } from '../../../models/signin'; | ||||
| import { publishUserStream } from '../../../stream'; | ||||
| import { publishMainStream } from '../../../stream'; | ||||
| import signin from '../common/signin'; | ||||
| import config from '../../../config'; | ||||
| 
 | ||||
|  | @ -87,5 +87,5 @@ export default async (ctx: Koa.Context) => { | |||
| 	}); | ||||
| 
 | ||||
| 	// Publish signin event
 | ||||
| 	publishUserStream(user._id, 'signin', await pack(record)); | ||||
| 	publishMainStream(user._id, 'signin', await pack(record)); | ||||
| }; | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import * as uuid from 'uuid'; | |||
| import autwh from 'autwh'; | ||||
| import redis from '../../../db/redis'; | ||||
| import User, { pack, ILocalUser } from '../../../models/user'; | ||||
| import { publishUserStream } from '../../../stream'; | ||||
| import { publishMainStream } from '../../../stream'; | ||||
| import config from '../../../config'; | ||||
| import signin from '../common/signin'; | ||||
| 
 | ||||
|  | @ -49,7 +49,7 @@ router.get('/disconnect/twitter', async ctx => { | |||
| 	ctx.body = `Twitterの連携を解除しました :v:`; | ||||
| 
 | ||||
| 	// Publish i updated event
 | ||||
| 	publishUserStream(user._id, 'meUpdated', await pack(user, user, { | ||||
| 	publishMainStream(user._id, 'meUpdated', await pack(user, user, { | ||||
| 		detail: true, | ||||
| 		includeSecrets: true | ||||
| 	})); | ||||
|  | @ -174,7 +174,7 @@ if (config.twitter == null) { | |||
| 			ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; | ||||
| 
 | ||||
| 			// Publish i updated event
 | ||||
| 			publishUserStream(user._id, 'meUpdated', await pack(user, user, { | ||||
| 			publishMainStream(user._id, 'meUpdated', await pack(user, user, { | ||||
| 				detail: true, | ||||
| 				includeSecrets: true | ||||
| 			})); | ||||
|  |  | |||
							
								
								
									
										39
									
								
								src/server/api/stream/channel.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/server/api/stream/channel.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Connection from '.'; | ||||
| 
 | ||||
| /** | ||||
|  * Stream channel | ||||
|  */ | ||||
| export default abstract class Channel { | ||||
| 	protected connection: Connection; | ||||
| 	public id: string; | ||||
| 
 | ||||
| 	protected get user() { | ||||
| 		return this.connection.user; | ||||
| 	} | ||||
| 
 | ||||
| 	protected get subscriber() { | ||||
| 		return this.connection.subscriber; | ||||
| 	} | ||||
| 
 | ||||
| 	constructor(id: string, connection: Connection) { | ||||
| 		this.id = id; | ||||
| 		this.connection = connection; | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public send(typeOrPayload: any, payload?: any) { | ||||
| 		const type = payload === undefined ? typeOrPayload.type : typeOrPayload; | ||||
| 		const body = payload === undefined ? typeOrPayload.body : payload; | ||||
| 
 | ||||
| 		this.connection.sendMessageToWs('channel', { | ||||
| 			id: this.id, | ||||
| 			type: type, | ||||
| 			body: body | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	public abstract init(params: any): void; | ||||
| 	public dispose?(): void; | ||||
| 	public onMessage?(type: string, body: any): void; | ||||
| } | ||||
							
								
								
									
										12
									
								
								src/server/api/stream/channels/drive.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/server/api/stream/channels/drive.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		// Subscribe drive stream
 | ||||
| 		this.subscriber.on(`driveStream:${this.user._id}`, data => { | ||||
| 			this.send(data); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										309
									
								
								src/server/api/stream/channels/games/reversi-game.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										309
									
								
								src/server/api/stream/channels/games/reversi-game.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,309 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import * as CRC32 from 'crc-32'; | ||||
| import ReversiGame, { pack } from '../../../../../models/games/reversi/game'; | ||||
| import { publishReversiGameStream } from '../../../../../stream'; | ||||
| import Reversi from '../../../../../games/reversi/core'; | ||||
| import * as maps from '../../../../../games/reversi/maps'; | ||||
| import Channel from '../../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	private gameId: string; | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		this.gameId = params.gameId as string; | ||||
| 
 | ||||
| 		// Subscribe game stream
 | ||||
| 		this.subscriber.on(`reversiGameStream:${this.gameId}`, data => { | ||||
| 			this.send(data); | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public onMessage(type: string, body: any) { | ||||
| 		switch (type) { | ||||
| 			case 'accept': this.accept(true); break; | ||||
| 			case 'cancel-accept': this.accept(false); break; | ||||
| 			case 'update-settings': this.updateSettings(body.settings); break; | ||||
| 			case 'init-form': this.initForm(body); break; | ||||
| 			case 'update-form': this.updateForm(body.id, body.value); break; | ||||
| 			case 'message': this.message(body); break; | ||||
| 			case 'set': this.set(body.pos); break; | ||||
| 			case 'check': this.check(body.crc32); break; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async updateSettings(settings: any) { | ||||
| 		const game = await ReversiGame.findOne({ _id: this.gameId }); | ||||
| 
 | ||||
| 		if (game.isStarted) return; | ||||
| 		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; | ||||
| 		if (game.user1Id.equals(this.user._id) && game.user1Accepted) return; | ||||
| 		if (game.user2Id.equals(this.user._id) && game.user2Accepted) return; | ||||
| 
 | ||||
| 		await ReversiGame.update({ _id: this.gameId }, { | ||||
| 			$set: { | ||||
| 				settings | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		publishReversiGameStream(this.gameId, 'updateSettings', settings); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async initForm(form: any) { | ||||
| 		const game = await ReversiGame.findOne({ _id: this.gameId }); | ||||
| 
 | ||||
| 		if (game.isStarted) return; | ||||
| 		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; | ||||
| 
 | ||||
| 		const set = game.user1Id.equals(this.user._id) ? { | ||||
| 			form1: form | ||||
| 		} : { | ||||
| 				form2: form | ||||
| 			}; | ||||
| 
 | ||||
| 		await ReversiGame.update({ _id: this.gameId }, { | ||||
| 			$set: set | ||||
| 		}); | ||||
| 
 | ||||
| 		publishReversiGameStream(this.gameId, 'initForm', { | ||||
| 			userId: this.user._id, | ||||
| 			form | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async updateForm(id: string, value: any) { | ||||
| 		const game = await ReversiGame.findOne({ _id: this.gameId }); | ||||
| 
 | ||||
| 		if (game.isStarted) return; | ||||
| 		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; | ||||
| 
 | ||||
| 		const form = game.user1Id.equals(this.user._id) ? game.form2 : game.form1; | ||||
| 
 | ||||
| 		const item = form.find((i: any) => i.id == id); | ||||
| 
 | ||||
| 		if (item == null) return; | ||||
| 
 | ||||
| 		item.value = value; | ||||
| 
 | ||||
| 		const set = game.user1Id.equals(this.user._id) ? { | ||||
| 			form2: form | ||||
| 		} : { | ||||
| 				form1: form | ||||
| 			}; | ||||
| 
 | ||||
| 		await ReversiGame.update({ _id: this.gameId }, { | ||||
| 			$set: set | ||||
| 		}); | ||||
| 
 | ||||
| 		publishReversiGameStream(this.gameId, 'updateForm', { | ||||
| 			userId: this.user._id, | ||||
| 			id, | ||||
| 			value | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async message(message: any) { | ||||
| 		message.id = Math.random(); | ||||
| 		publishReversiGameStream(this.gameId, 'message', { | ||||
| 			userId: this.user._id, | ||||
| 			message | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async accept(accept: boolean) { | ||||
| 		const game = await ReversiGame.findOne({ _id: this.gameId }); | ||||
| 
 | ||||
| 		if (game.isStarted) return; | ||||
| 
 | ||||
| 		let bothAccepted = false; | ||||
| 
 | ||||
| 		if (game.user1Id.equals(this.user._id)) { | ||||
| 			await ReversiGame.update({ _id: this.gameId }, { | ||||
| 				$set: { | ||||
| 					user1Accepted: accept | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 			publishReversiGameStream(this.gameId, 'changeAccepts', { | ||||
| 				user1: accept, | ||||
| 				user2: game.user2Accepted | ||||
| 			}); | ||||
| 
 | ||||
| 			if (accept && game.user2Accepted) bothAccepted = true; | ||||
| 		} else if (game.user2Id.equals(this.user._id)) { | ||||
| 			await ReversiGame.update({ _id: this.gameId }, { | ||||
| 				$set: { | ||||
| 					user2Accepted: accept | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 			publishReversiGameStream(this.gameId, 'changeAccepts', { | ||||
| 				user1: game.user1Accepted, | ||||
| 				user2: accept | ||||
| 			}); | ||||
| 
 | ||||
| 			if (accept && game.user1Accepted) bothAccepted = true; | ||||
| 		} else { | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		if (bothAccepted) { | ||||
| 			// 3秒後、まだacceptされていたらゲーム開始
 | ||||
| 			setTimeout(async () => { | ||||
| 				const freshGame = await ReversiGame.findOne({ _id: this.gameId }); | ||||
| 				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; | ||||
| 				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; | ||||
| 
 | ||||
| 				let bw: number; | ||||
| 				if (freshGame.settings.bw == 'random') { | ||||
| 					bw = Math.random() > 0.5 ? 1 : 2; | ||||
| 				} else { | ||||
| 					bw = freshGame.settings.bw as number; | ||||
| 				} | ||||
| 
 | ||||
| 				function getRandomMap() { | ||||
| 					const mapCount = Object.entries(maps).length; | ||||
| 					const rnd = Math.floor(Math.random() * mapCount); | ||||
| 					return Object.values(maps)[rnd].data; | ||||
| 				} | ||||
| 
 | ||||
| 				const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); | ||||
| 
 | ||||
| 				await ReversiGame.update({ _id: this.gameId }, { | ||||
| 					$set: { | ||||
| 						startedAt: new Date(), | ||||
| 						isStarted: true, | ||||
| 						black: bw, | ||||
| 						'settings.map': map | ||||
| 					} | ||||
| 				}); | ||||
| 
 | ||||
| 				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
 | ||||
| 				const o = new Reversi(map, { | ||||
| 					isLlotheo: freshGame.settings.isLlotheo, | ||||
| 					canPutEverywhere: freshGame.settings.canPutEverywhere, | ||||
| 					loopedBoard: freshGame.settings.loopedBoard | ||||
| 				}); | ||||
| 
 | ||||
| 				if (o.isEnded) { | ||||
| 					let winner; | ||||
| 					if (o.winner === true) { | ||||
| 						winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; | ||||
| 					} else if (o.winner === false) { | ||||
| 						winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; | ||||
| 					} else { | ||||
| 						winner = null; | ||||
| 					} | ||||
| 
 | ||||
| 					await ReversiGame.update({ | ||||
| 						_id: this.gameId | ||||
| 					}, { | ||||
| 							$set: { | ||||
| 								isEnded: true, | ||||
| 								winnerId: winner | ||||
| 							} | ||||
| 						}); | ||||
| 
 | ||||
| 					publishReversiGameStream(this.gameId, 'ended', { | ||||
| 						winnerId: winner, | ||||
| 						game: await pack(this.gameId, this.user) | ||||
| 					}); | ||||
| 				} | ||||
| 				//#endregion
 | ||||
| 
 | ||||
| 				publishReversiGameStream(this.gameId, 'started', await pack(this.gameId, this.user)); | ||||
| 			}, 3000); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// 石を打つ
 | ||||
| 	@autobind | ||||
| 	private async set(pos: number) { | ||||
| 		const game = await ReversiGame.findOne({ _id: this.gameId }); | ||||
| 
 | ||||
| 		if (!game.isStarted) return; | ||||
| 		if (game.isEnded) return; | ||||
| 		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; | ||||
| 
 | ||||
| 		const o = new Reversi(game.settings.map, { | ||||
| 			isLlotheo: game.settings.isLlotheo, | ||||
| 			canPutEverywhere: game.settings.canPutEverywhere, | ||||
| 			loopedBoard: game.settings.loopedBoard | ||||
| 		}); | ||||
| 
 | ||||
| 		game.logs.forEach(log => { | ||||
| 			o.put(log.color, log.pos); | ||||
| 		}); | ||||
| 
 | ||||
| 		const myColor = | ||||
| 			(game.user1Id.equals(this.user._id) && game.black == 1) || (game.user2Id.equals(this.user._id) && game.black == 2) | ||||
| 				? true | ||||
| 				: false; | ||||
| 
 | ||||
| 		if (!o.canPut(myColor, pos)) return; | ||||
| 		o.put(myColor, pos); | ||||
| 
 | ||||
| 		let winner; | ||||
| 		if (o.isEnded) { | ||||
| 			if (o.winner === true) { | ||||
| 				winner = game.black == 1 ? game.user1Id : game.user2Id; | ||||
| 			} else if (o.winner === false) { | ||||
| 				winner = game.black == 1 ? game.user2Id : game.user1Id; | ||||
| 			} else { | ||||
| 				winner = null; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		const log = { | ||||
| 			at: new Date(), | ||||
| 			color: myColor, | ||||
| 			pos | ||||
| 		}; | ||||
| 
 | ||||
| 		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); | ||||
| 
 | ||||
| 		await ReversiGame.update({ | ||||
| 			_id: this.gameId | ||||
| 		}, { | ||||
| 				$set: { | ||||
| 					crc32, | ||||
| 					isEnded: o.isEnded, | ||||
| 					winnerId: winner | ||||
| 				}, | ||||
| 				$push: { | ||||
| 					logs: log | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 		publishReversiGameStream(this.gameId, 'set', Object.assign(log, { | ||||
| 			next: o.turn | ||||
| 		})); | ||||
| 
 | ||||
| 		if (o.isEnded) { | ||||
| 			publishReversiGameStream(this.gameId, 'ended', { | ||||
| 				winnerId: winner, | ||||
| 				game: await pack(this.gameId, this.user) | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async check(crc32: string) { | ||||
| 		const game = await ReversiGame.findOne({ _id: this.gameId }); | ||||
| 
 | ||||
| 		if (!game.isStarted) return; | ||||
| 
 | ||||
| 		// 互換性のため
 | ||||
| 		if (game.crc32 == null) return; | ||||
| 
 | ||||
| 		if (crc32 !== game.crc32) { | ||||
| 			this.send('rescue', await pack(game, this.user)); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										30
									
								
								src/server/api/stream/channels/games/reversi.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/server/api/stream/channels/games/reversi.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import * as mongo from 'mongodb'; | ||||
| import Matching, { pack } from '../../../../../models/games/reversi/matching'; | ||||
| import { publishMainStream } from '../../../../../stream'; | ||||
| import Channel from '../../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		// Subscribe reversi stream
 | ||||
| 		this.subscriber.on(`reversiStream:${this.user._id}`, data => { | ||||
| 			this.send(data); | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public async onMessage(type: string, body: any) { | ||||
| 		switch (type) { | ||||
| 			case 'ping': | ||||
| 				if (body.id == null) return; | ||||
| 				const matching = await Matching.findOne({ | ||||
| 					parentId: this.user._id, | ||||
| 					childId: new mongo.ObjectID(body.id) | ||||
| 				}); | ||||
| 				if (matching == null) return; | ||||
| 				publishMainStream(matching.childId, 'reversiInvited', await pack(matching, matching.childId)); | ||||
| 				break; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										39
									
								
								src/server/api/stream/channels/global-timeline.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/server/api/stream/channels/global-timeline.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Mute from '../../../../models/mute'; | ||||
| import { pack } from '../../../../models/note'; | ||||
| import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	private mutedUserIds: string[] = []; | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		// Subscribe events
 | ||||
| 		this.subscriber.on('globalTimeline', this.onNote); | ||||
| 
 | ||||
| 		const mute = await Mute.find({ muterId: this.user._id }); | ||||
| 		this.mutedUserIds = mute.map(m => m.muteeId.toString()); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async onNote(note: any) { | ||||
| 		// Renoteなら再pack
 | ||||
| 		if (note.renoteId != null) { | ||||
| 			note.renote = await pack(note.renoteId, this.user, { | ||||
| 				detail: true | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (shouldMuteThisNote(note, this.mutedUserIds)) return; | ||||
| 
 | ||||
| 		this.send('note', note); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public dispose() { | ||||
| 		// Unsubscribe events
 | ||||
| 		this.subscriber.off('globalTimeline', this.onNote); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										33
									
								
								src/server/api/stream/channels/hashtag.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/server/api/stream/channels/hashtag.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Mute from '../../../../models/mute'; | ||||
| import { pack } from '../../../../models/note'; | ||||
| import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null; | ||||
| 		const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; | ||||
| 
 | ||||
| 		const q: Array<string[]> = params.q; | ||||
| 
 | ||||
| 		// Subscribe stream
 | ||||
| 		this.subscriber.on('hashtag', async note => { | ||||
| 			const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase()))); | ||||
| 			if (!matched) return; | ||||
| 
 | ||||
| 			// Renoteなら再pack
 | ||||
| 			if (note.renoteId != null) { | ||||
| 				note.renote = await pack(note.renoteId, this.user, { | ||||
| 					detail: true | ||||
| 				}); | ||||
| 			} | ||||
| 
 | ||||
| 			// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 			if (shouldMuteThisNote(note, mutedUserIds)) return; | ||||
| 
 | ||||
| 			this.send('note', note); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										39
									
								
								src/server/api/stream/channels/home-timeline.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/server/api/stream/channels/home-timeline.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Mute from '../../../../models/mute'; | ||||
| import { pack } from '../../../../models/note'; | ||||
| import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	private mutedUserIds: string[] = []; | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		// Subscribe events
 | ||||
| 		this.subscriber.on(`homeTimeline:${this.user._id}`, this.onNote); | ||||
| 
 | ||||
| 		const mute = await Mute.find({ muterId: this.user._id }); | ||||
| 		this.mutedUserIds = mute.map(m => m.muteeId.toString()); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async onNote(note: any) { | ||||
| 		// Renoteなら再pack
 | ||||
| 		if (note.renoteId != null) { | ||||
| 			note.renote = await pack(note.renoteId, this.user, { | ||||
| 				detail: true | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (shouldMuteThisNote(note, this.mutedUserIds)) return; | ||||
| 
 | ||||
| 		this.send('note', note); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public dispose() { | ||||
| 		// Unsubscribe events
 | ||||
| 		this.subscriber.off(`homeTimeline:${this.user._id}`, this.onNote); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/server/api/stream/channels/hybrid-timeline.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/server/api/stream/channels/hybrid-timeline.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Mute from '../../../../models/mute'; | ||||
| import { pack } from '../../../../models/note'; | ||||
| import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	private mutedUserIds: string[] = []; | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		// Subscribe events
 | ||||
| 		this.subscriber.on('hybridTimeline', this.onNewNote); | ||||
| 		this.subscriber.on(`hybridTimeline:${this.user._id}`, this.onNewNote); | ||||
| 
 | ||||
| 		const mute = await Mute.find({ muterId: this.user._id }); | ||||
| 		this.mutedUserIds = mute.map(m => m.muteeId.toString()); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async onNewNote(note: any) { | ||||
| 		// Renoteなら再pack
 | ||||
| 		if (note.renoteId != null) { | ||||
| 			note.renote = await pack(note.renoteId, this.user, { | ||||
| 				detail: true | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (shouldMuteThisNote(note, this.mutedUserIds)) return; | ||||
| 
 | ||||
| 		this.send('note', note); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public dispose() { | ||||
| 		// Unsubscribe events
 | ||||
| 		this.subscriber.off('hybridTimeline', this.onNewNote); | ||||
| 		this.subscriber.off(`hybridTimeline:${this.user._id}`, this.onNewNote); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/server/api/stream/channels/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/server/api/stream/channels/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| import main from './main'; | ||||
| import homeTimeline from './home-timeline'; | ||||
| import localTimeline from './local-timeline'; | ||||
| import hybridTimeline from './hybrid-timeline'; | ||||
| import globalTimeline from './global-timeline'; | ||||
| import notesStats from './notes-stats'; | ||||
| import serverStats from './server-stats'; | ||||
| import userList from './user-list'; | ||||
| import messaging from './messaging'; | ||||
| import messagingIndex from './messaging-index'; | ||||
| import drive from './drive'; | ||||
| import hashtag from './hashtag'; | ||||
| import gamesReversi from './games/reversi'; | ||||
| import gamesReversiGame from './games/reversi-game'; | ||||
| 
 | ||||
| export default { | ||||
| 	main, | ||||
| 	homeTimeline, | ||||
| 	localTimeline, | ||||
| 	hybridTimeline, | ||||
| 	globalTimeline, | ||||
| 	notesStats, | ||||
| 	serverStats, | ||||
| 	userList, | ||||
| 	messaging, | ||||
| 	messagingIndex, | ||||
| 	drive, | ||||
| 	hashtag, | ||||
| 	gamesReversi, | ||||
| 	gamesReversiGame | ||||
| }; | ||||
							
								
								
									
										39
									
								
								src/server/api/stream/channels/local-timeline.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/server/api/stream/channels/local-timeline.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Mute from '../../../../models/mute'; | ||||
| import { pack } from '../../../../models/note'; | ||||
| import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	private mutedUserIds: string[] = []; | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		// Subscribe events
 | ||||
| 		this.subscriber.on('localTimeline', this.onNote); | ||||
| 
 | ||||
| 		const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null; | ||||
| 		this.mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private async onNote(note: any) { | ||||
| 		// Renoteなら再pack
 | ||||
| 		if (note.renoteId != null) { | ||||
| 			note.renote = await pack(note.renoteId, this.user, { | ||||
| 				detail: true | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (shouldMuteThisNote(note, this.mutedUserIds)) return; | ||||
| 
 | ||||
| 		this.send('note', note); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public dispose() { | ||||
| 		// Unsubscribe events
 | ||||
| 		this.subscriber.off('localTimeline', this.onNote); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										25
									
								
								src/server/api/stream/channels/main.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/server/api/stream/channels/main.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Mute from '../../../../models/mute'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		const mute = await Mute.find({ muterId: this.user._id }); | ||||
| 		const mutedUserIds = mute.map(m => m.muteeId.toString()); | ||||
| 
 | ||||
| 		// Subscribe main stream channel
 | ||||
| 		this.subscriber.on(`mainStream:${this.user._id}`, async data => { | ||||
| 			const { type, body } = data; | ||||
| 
 | ||||
| 			switch (type) { | ||||
| 				case 'notification': { | ||||
| 					if (!mutedUserIds.includes(body.userId)) { | ||||
| 						this.send('notification', body); | ||||
| 					} | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										12
									
								
								src/server/api/stream/channels/messaging-index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/server/api/stream/channels/messaging-index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		// Subscribe messaging index stream
 | ||||
| 		this.subscriber.on(`messagingIndexStream:${this.user._id}`, data => { | ||||
| 			this.send(data); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/server/api/stream/channels/messaging.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/server/api/stream/channels/messaging.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import read from '../../common/read-messaging-message'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	private otherpartyId: string; | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		this.otherpartyId = params.otherparty as string; | ||||
| 
 | ||||
| 		// Subscribe messaging stream
 | ||||
| 		this.subscriber.on(`messagingStream:${this.user._id}-${this.otherpartyId}`, data => { | ||||
| 			this.send(data); | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public onMessage(type: string, body: any) { | ||||
| 		switch (type) { | ||||
| 			case 'read': | ||||
| 				read(this.user._id, this.otherpartyId, body.id); | ||||
| 				break; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										34
									
								
								src/server/api/stream/channels/notes-stats.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/server/api/stream/channels/notes-stats.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Xev from 'xev'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| const ev = new Xev(); | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		ev.addListener('notesStats', this.onStats); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private onStats(stats: any) { | ||||
| 		this.send('stats', stats); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public onMessage(type: string, body: any) { | ||||
| 		switch (type) { | ||||
| 			case 'requestLog': | ||||
| 				ev.once(`notesStatsLog:${body.id}`, statsLog => { | ||||
| 					this.send('statsLog', statsLog); | ||||
| 				}); | ||||
| 				ev.emit('requestNotesStatsLog', body.id); | ||||
| 				break; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public dispose() { | ||||
| 		ev.removeListener('notesStats', this.onStats); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										37
									
								
								src/server/api/stream/channels/server-stats.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/server/api/stream/channels/server-stats.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Xev from 'xev'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| const ev = new Xev(); | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		ev.addListener('serverStats', this.onStats); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	private onStats(stats: any) { | ||||
| 		this.send('stats', stats); | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public onMessage(type: string, body: any) { | ||||
| 		switch (type) { | ||||
| 			case 'requestLog': | ||||
| 				ev.once(`serverStatsLog:${body.id}`, statsLog => { | ||||
| 					this.send('statsLog', statsLog); | ||||
| 				}); | ||||
| 				ev.emit('requestServerStatsLog', { | ||||
| 					id: body.id, | ||||
| 					length: body.length | ||||
| 				}); | ||||
| 				break; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public dispose() { | ||||
| 		ev.removeListener('serverStats', this.onStats); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										14
									
								
								src/server/api/stream/channels/user-list.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/server/api/stream/channels/user-list.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| import autobind from 'autobind-decorator'; | ||||
| import Channel from '../channel'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		const listId = params.listId as string; | ||||
| 
 | ||||
| 		// Subscribe stream
 | ||||
| 		this.subscriber.on(`userListStream:${listId}`, data => { | ||||
| 			this.send(data); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | @ -1,9 +0,0 @@ | |||
| import * as websocket from 'websocket'; | ||||
| import Xev from 'xev'; | ||||
| 
 | ||||
| export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void { | ||||
| 	// Subscribe drive stream
 | ||||
| 	subscriber.on(`drive-stream:${user._id}`, data => { | ||||
| 		connection.send(JSON.stringify(data)); | ||||
| 	}); | ||||
| } | ||||
|  | @ -1,332 +0,0 @@ | |||
| import * as websocket from 'websocket'; | ||||
| import Xev from 'xev'; | ||||
| import * as CRC32 from 'crc-32'; | ||||
| import ReversiGame, { pack } from '../../../../models/games/reversi/game'; | ||||
| import { publishReversiGameStream } from '../../../../stream'; | ||||
| import Reversi from '../../../../games/reversi/core'; | ||||
| import * as maps from '../../../../games/reversi/maps'; | ||||
| import { ParsedUrlQuery } from 'querystring'; | ||||
| 
 | ||||
| export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user?: any): void { | ||||
| 	const q = request.resourceURL.query as ParsedUrlQuery; | ||||
| 	const gameId = q.game as string; | ||||
| 
 | ||||
| 	// Subscribe game stream
 | ||||
| 	subscriber.on(`reversi-game-stream:${gameId}`, data => { | ||||
| 		connection.send(JSON.stringify(data)); | ||||
| 	}); | ||||
| 
 | ||||
| 	connection.on('message', async (data) => { | ||||
| 		const msg = JSON.parse(data.utf8Data); | ||||
| 
 | ||||
| 		switch (msg.type) { | ||||
| 			case 'accept': | ||||
| 				accept(true); | ||||
| 				break; | ||||
| 
 | ||||
| 			case 'cancel-accept': | ||||
| 				accept(false); | ||||
| 				break; | ||||
| 
 | ||||
| 			case 'update-settings': | ||||
| 				if (msg.settings == null) return; | ||||
| 				updateSettings(msg.settings); | ||||
| 				break; | ||||
| 
 | ||||
| 			case 'init-form': | ||||
| 				if (msg.body == null) return; | ||||
| 				initForm(msg.body); | ||||
| 				break; | ||||
| 
 | ||||
| 			case 'update-form': | ||||
| 				if (msg.id == null || msg.value === undefined) return; | ||||
| 				updateForm(msg.id, msg.value); | ||||
| 				break; | ||||
| 
 | ||||
| 			case 'message': | ||||
| 				if (msg.body == null) return; | ||||
| 				message(msg.body); | ||||
| 				break; | ||||
| 
 | ||||
| 			case 'set': | ||||
| 				if (msg.pos == null) return; | ||||
| 				set(msg.pos); | ||||
| 				break; | ||||
| 
 | ||||
| 			case 'check': | ||||
| 				if (msg.crc32 == null) return; | ||||
| 				check(msg.crc32); | ||||
| 				break; | ||||
| 		} | ||||
| 	}); | ||||
| 
 | ||||
| 	async function updateSettings(settings: any) { | ||||
| 		const game = await ReversiGame.findOne({ _id: gameId }); | ||||
| 
 | ||||
| 		if (game.isStarted) return; | ||||
| 		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; | ||||
| 		if (game.user1Id.equals(user._id) && game.user1Accepted) return; | ||||
| 		if (game.user2Id.equals(user._id) && game.user2Accepted) return; | ||||
| 
 | ||||
| 		await ReversiGame.update({ _id: gameId }, { | ||||
| 			$set: { | ||||
| 				settings | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		publishReversiGameStream(gameId, 'update-settings', settings); | ||||
| 	} | ||||
| 
 | ||||
| 	async function initForm(form: any) { | ||||
| 		const game = await ReversiGame.findOne({ _id: gameId }); | ||||
| 
 | ||||
| 		if (game.isStarted) return; | ||||
| 		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; | ||||
| 
 | ||||
| 		const set = game.user1Id.equals(user._id) ? { | ||||
| 			form1: form | ||||
| 		} : { | ||||
| 				form2: form | ||||
| 			}; | ||||
| 
 | ||||
| 		await ReversiGame.update({ _id: gameId }, { | ||||
| 			$set: set | ||||
| 		}); | ||||
| 
 | ||||
| 		publishReversiGameStream(gameId, 'init-form', { | ||||
| 			userId: user._id, | ||||
| 			form | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	async function updateForm(id: string, value: any) { | ||||
| 		const game = await ReversiGame.findOne({ _id: gameId }); | ||||
| 
 | ||||
| 		if (game.isStarted) return; | ||||
| 		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; | ||||
| 
 | ||||
| 		const form = game.user1Id.equals(user._id) ? game.form2 : game.form1; | ||||
| 
 | ||||
| 		const item = form.find((i: any) => i.id == id); | ||||
| 
 | ||||
| 		if (item == null) return; | ||||
| 
 | ||||
| 		item.value = value; | ||||
| 
 | ||||
| 		const set = game.user1Id.equals(user._id) ? { | ||||
| 			form2: form | ||||
| 		} : { | ||||
| 				form1: form | ||||
| 			}; | ||||
| 
 | ||||
| 		await ReversiGame.update({ _id: gameId }, { | ||||
| 			$set: set | ||||
| 		}); | ||||
| 
 | ||||
| 		publishReversiGameStream(gameId, 'update-form', { | ||||
| 			userId: user._id, | ||||
| 			id, | ||||
| 			value | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	async function message(message: any) { | ||||
| 		message.id = Math.random(); | ||||
| 		publishReversiGameStream(gameId, 'message', { | ||||
| 			userId: user._id, | ||||
| 			message | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	async function accept(accept: boolean) { | ||||
| 		const game = await ReversiGame.findOne({ _id: gameId }); | ||||
| 
 | ||||
| 		if (game.isStarted) return; | ||||
| 
 | ||||
| 		let bothAccepted = false; | ||||
| 
 | ||||
| 		if (game.user1Id.equals(user._id)) { | ||||
| 			await ReversiGame.update({ _id: gameId }, { | ||||
| 				$set: { | ||||
| 					user1Accepted: accept | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 			publishReversiGameStream(gameId, 'change-accepts', { | ||||
| 				user1: accept, | ||||
| 				user2: game.user2Accepted | ||||
| 			}); | ||||
| 
 | ||||
| 			if (accept && game.user2Accepted) bothAccepted = true; | ||||
| 		} else if (game.user2Id.equals(user._id)) { | ||||
| 			await ReversiGame.update({ _id: gameId }, { | ||||
| 				$set: { | ||||
| 					user2Accepted: accept | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 			publishReversiGameStream(gameId, 'change-accepts', { | ||||
| 				user1: game.user1Accepted, | ||||
| 				user2: accept | ||||
| 			}); | ||||
| 
 | ||||
| 			if (accept && game.user1Accepted) bothAccepted = true; | ||||
| 		} else { | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		if (bothAccepted) { | ||||
| 			// 3秒後、まだacceptされていたらゲーム開始
 | ||||
| 			setTimeout(async () => { | ||||
| 				const freshGame = await ReversiGame.findOne({ _id: gameId }); | ||||
| 				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; | ||||
| 				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; | ||||
| 
 | ||||
| 				let bw: number; | ||||
| 				if (freshGame.settings.bw == 'random') { | ||||
| 					bw = Math.random() > 0.5 ? 1 : 2; | ||||
| 				} else { | ||||
| 					bw = freshGame.settings.bw as number; | ||||
| 				} | ||||
| 
 | ||||
| 				function getRandomMap() { | ||||
| 					const mapCount = Object.entries(maps).length; | ||||
| 					const rnd = Math.floor(Math.random() * mapCount); | ||||
| 					return Object.values(maps)[rnd].data; | ||||
| 				} | ||||
| 
 | ||||
| 				const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); | ||||
| 
 | ||||
| 				await ReversiGame.update({ _id: gameId }, { | ||||
| 					$set: { | ||||
| 						startedAt: new Date(), | ||||
| 						isStarted: true, | ||||
| 						black: bw, | ||||
| 						'settings.map': map | ||||
| 					} | ||||
| 				}); | ||||
| 
 | ||||
| 				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
 | ||||
| 				const o = new Reversi(map, { | ||||
| 					isLlotheo: freshGame.settings.isLlotheo, | ||||
| 					canPutEverywhere: freshGame.settings.canPutEverywhere, | ||||
| 					loopedBoard: freshGame.settings.loopedBoard | ||||
| 				}); | ||||
| 
 | ||||
| 				if (o.isEnded) { | ||||
| 					let winner; | ||||
| 					if (o.winner === true) { | ||||
| 						winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; | ||||
| 					} else if (o.winner === false) { | ||||
| 						winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; | ||||
| 					} else { | ||||
| 						winner = null; | ||||
| 					} | ||||
| 
 | ||||
| 					await ReversiGame.update({ | ||||
| 						_id: gameId | ||||
| 					}, { | ||||
| 							$set: { | ||||
| 								isEnded: true, | ||||
| 								winnerId: winner | ||||
| 							} | ||||
| 						}); | ||||
| 
 | ||||
| 					publishReversiGameStream(gameId, 'ended', { | ||||
| 						winnerId: winner, | ||||
| 						game: await pack(gameId, user) | ||||
| 					}); | ||||
| 				} | ||||
| 				//#endregion
 | ||||
| 
 | ||||
| 				publishReversiGameStream(gameId, 'started', await pack(gameId, user)); | ||||
| 			}, 3000); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// 石を打つ
 | ||||
| 	async function set(pos: number) { | ||||
| 		const game = await ReversiGame.findOne({ _id: gameId }); | ||||
| 
 | ||||
| 		if (!game.isStarted) return; | ||||
| 		if (game.isEnded) return; | ||||
| 		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; | ||||
| 
 | ||||
| 		const o = new Reversi(game.settings.map, { | ||||
| 			isLlotheo: game.settings.isLlotheo, | ||||
| 			canPutEverywhere: game.settings.canPutEverywhere, | ||||
| 			loopedBoard: game.settings.loopedBoard | ||||
| 		}); | ||||
| 
 | ||||
| 		game.logs.forEach(log => { | ||||
| 			o.put(log.color, log.pos); | ||||
| 		}); | ||||
| 
 | ||||
| 		const myColor = | ||||
| 			(game.user1Id.equals(user._id) && game.black == 1) || (game.user2Id.equals(user._id) && game.black == 2) | ||||
| 				? true | ||||
| 				: false; | ||||
| 
 | ||||
| 		if (!o.canPut(myColor, pos)) return; | ||||
| 		o.put(myColor, pos); | ||||
| 
 | ||||
| 		let winner; | ||||
| 		if (o.isEnded) { | ||||
| 			if (o.winner === true) { | ||||
| 				winner = game.black == 1 ? game.user1Id : game.user2Id; | ||||
| 			} else if (o.winner === false) { | ||||
| 				winner = game.black == 1 ? game.user2Id : game.user1Id; | ||||
| 			} else { | ||||
| 				winner = null; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		const log = { | ||||
| 			at: new Date(), | ||||
| 			color: myColor, | ||||
| 			pos | ||||
| 		}; | ||||
| 
 | ||||
| 		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); | ||||
| 
 | ||||
| 		await ReversiGame.update({ | ||||
| 			_id: gameId | ||||
| 		}, { | ||||
| 				$set: { | ||||
| 					crc32, | ||||
| 					isEnded: o.isEnded, | ||||
| 					winnerId: winner | ||||
| 				}, | ||||
| 				$push: { | ||||
| 					logs: log | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 		publishReversiGameStream(gameId, 'set', Object.assign(log, { | ||||
| 			next: o.turn | ||||
| 		})); | ||||
| 
 | ||||
| 		if (o.isEnded) { | ||||
| 			publishReversiGameStream(gameId, 'ended', { | ||||
| 				winnerId: winner, | ||||
| 				game: await pack(gameId, user) | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	async function check(crc32: string) { | ||||
| 		const game = await ReversiGame.findOne({ _id: gameId }); | ||||
| 
 | ||||
| 		if (!game.isStarted) return; | ||||
| 
 | ||||
| 		// 互換性のため
 | ||||
| 		if (game.crc32 == null) return; | ||||
| 
 | ||||
| 		if (crc32 !== game.crc32) { | ||||
| 			connection.send(JSON.stringify({ | ||||
| 				type: 'rescue', | ||||
| 				body: await pack(game, user) | ||||
| 			})); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,28 +0,0 @@ | |||
| import * as mongo from 'mongodb'; | ||||
| import * as websocket from 'websocket'; | ||||
| import Xev from 'xev'; | ||||
| import Matching, { pack } from '../../../../models/games/reversi/matching'; | ||||
| import { publishUserStream } from '../../../../stream'; | ||||
| 
 | ||||
| export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void { | ||||
| 	// Subscribe reversi stream
 | ||||
| 	subscriber.on(`reversi-stream:${user._id}`, data => { | ||||
| 		connection.send(JSON.stringify(data)); | ||||
| 	}); | ||||
| 
 | ||||
| 	connection.on('message', async (data) => { | ||||
| 		const msg = JSON.parse(data.utf8Data); | ||||
| 
 | ||||
| 		switch (msg.type) { | ||||
| 			case 'ping': | ||||
| 				if (msg.id == null) return; | ||||
| 				const matching = await Matching.findOne({ | ||||
| 					parentId: user._id, | ||||
| 					childId: new mongo.ObjectID(msg.id) | ||||
| 				}); | ||||
| 				if (matching == null) return; | ||||
| 				publishUserStream(matching.childId, 'reversi_invited', await pack(matching, matching.childId)); | ||||
| 				break; | ||||
| 		} | ||||
| 	}); | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue