From d0570d7fe3a3bf3c6b0312dece74bacc04c3534a Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 7 Oct 2018 11:06:17 +0900 Subject: [PATCH] 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 --- CHANGELOG.md | 82 +++++ package.json | 3 +- .../common/scripts/compose-notification.ts | 6 +- .../app/common/scripts/note-subscriber.ts | 105 ++++++ src/client/app/common/scripts/stream.ts | 318 +++++++++++++++++ .../app/common/scripts/streaming/drive.ts | 34 -- .../streaming/games/reversi/reversi-game.ts | 13 - .../streaming/games/reversi/reversi.ts | 31 -- .../scripts/streaming/global-timeline.ts | 34 -- .../app/common/scripts/streaming/hashtag.ts | 13 - .../app/common/scripts/streaming/home.ts | 126 ------- .../scripts/streaming/hybrid-timeline.ts | 34 -- .../scripts/streaming/local-timeline.ts | 34 -- .../scripts/streaming/messaging-index.ts | 34 -- .../app/common/scripts/streaming/messaging.ts | 20 -- .../common/scripts/streaming/notes-stats.ts | 30 -- .../common/scripts/streaming/server-stats.ts | 30 -- .../scripts/streaming/stream-manager.ts | 109 ------ .../app/common/scripts/streaming/stream.ts | 137 -------- .../app/common/scripts/streaming/user-list.ts | 17 - .../games/reversi/reversi.gameroom.vue | 8 +- .../games/reversi/reversi.index.vue | 9 +- .../components/games/reversi/reversi.vue | 8 +- .../views/components/messaging-room.vue | 8 +- .../app/common/views/components/messaging.vue | 10 +- .../app/common/views/components/signin.vue | 2 +- .../app/common/views/components/signup.vue | 4 +- .../views/components/stream-indicator.vue | 2 +- .../views/components/welcome-timeline.vue | 9 +- .../app/common/views/widgets/photo-stream.vue | 11 +- .../common/views/widgets/posts-monitor.vue | 8 +- .../app/common/views/widgets/server.vue | 8 +- src/client/app/config.ts | 2 +- src/client/app/desktop/script.ts | 84 ++--- .../app/desktop/views/components/drive.vue | 13 +- .../views/components/follow-button.vue | 11 +- .../app/desktop/views/components/home.vue | 6 +- .../desktop/views/components/note-detail.vue | 3 + .../desktop/views/components/notes.note.vue | 86 +---- .../views/components/notifications.vue | 14 +- .../views/components/settings.signins.vue | 12 +- .../views/components/timeline.core.vue | 61 +--- .../views/components/ui.header.nav.vue | 12 +- .../views/components/user-list-timeline.vue | 1 - .../views/pages/admin/admin.dashboard.vue | 8 +- .../desktop/views/pages/deck/deck.direct.vue | 10 +- .../views/pages/deck/deck.hashtag-tl.vue | 3 +- .../desktop/views/pages/deck/deck.list-tl.vue | 1 - .../views/pages/deck/deck.mentions.vue | 10 +- .../desktop/views/pages/deck/deck.note.vue | 68 +--- .../views/pages/deck/deck.notifications.vue | 11 +- .../app/desktop/views/pages/deck/deck.tl.vue | 21 +- src/client/app/mios.ts | 135 ++----- .../app/mobile/views/components/drive.vue | 13 +- .../mobile/views/components/follow-button.vue | 10 +- .../mobile/views/components/note-detail.vue | 3 + .../app/mobile/views/components/note.vue | 86 +---- .../mobile/views/components/notifications.vue | 17 +- .../app/mobile/views/components/ui.header.vue | 17 +- .../app/mobile/views/components/ui.nav.vue | 10 +- src/client/app/mobile/views/components/ui.vue | 17 +- .../views/components/user-list-timeline.vue | 1 - .../app/mobile/views/pages/home.timeline.vue | 60 +--- src/client/app/tsconfig.json | 3 +- src/docs/stream.ja-JP.md | 8 +- src/notify.ts | 6 +- src/server/api/call.ts | 4 + .../api/common/read-messaging-message.ts | 4 +- src/server/api/common/read-notification.ts | 4 +- .../api/endpoints/games/reversi/match.ts | 6 +- .../api/endpoints/i/regenerate_token.ts | 4 +- src/server/api/endpoints/i/update.ts | 4 +- .../api/endpoints/i/update_client_setting.ts | 4 +- src/server/api/endpoints/i/update_home.ts | 4 +- .../api/endpoints/i/update_mobile_home.ts | 4 +- src/server/api/endpoints/i/update_widget.ts | 4 +- .../endpoints/messaging/messages/create.ts | 10 +- src/server/api/endpoints/notes/polls/vote.ts | 5 +- .../notifications/mark_all_as_read.ts | 4 +- src/server/api/private/signin.ts | 4 +- src/server/api/service/twitter.ts | 6 +- src/server/api/stream/channel.ts | 39 ++ src/server/api/stream/channels/drive.ts | 12 + .../api/stream/channels/games/reversi-game.ts | 309 ++++++++++++++++ .../api/stream/channels/games/reversi.ts | 30 ++ .../api/stream/channels/global-timeline.ts | 39 ++ src/server/api/stream/channels/hashtag.ts | 33 ++ .../api/stream/channels/home-timeline.ts | 39 ++ .../api/stream/channels/hybrid-timeline.ts | 41 +++ src/server/api/stream/channels/index.ts | 31 ++ .../api/stream/channels/local-timeline.ts | 39 ++ src/server/api/stream/channels/main.ts | 25 ++ .../api/stream/channels/messaging-index.ts | 12 + src/server/api/stream/channels/messaging.ts | 26 ++ src/server/api/stream/channels/notes-stats.ts | 34 ++ .../api/stream/channels/server-stats.ts | 37 ++ src/server/api/stream/channels/user-list.ts | 14 + src/server/api/stream/drive.ts | 9 - src/server/api/stream/games/reversi-game.ts | 332 ------------------ src/server/api/stream/games/reversi.ts | 28 -- src/server/api/stream/global-timeline.ts | 27 -- src/server/api/stream/hashtag.ts | 40 --- src/server/api/stream/home.ts | 110 ------ src/server/api/stream/hybrid-timeline.ts | 38 -- src/server/api/stream/index.ts | 213 +++++++++++ src/server/api/stream/local-timeline.ts | 35 -- src/server/api/stream/messaging-index.ts | 9 - src/server/api/stream/messaging.ts | 25 -- src/server/api/stream/notes-stats.ts | 35 -- src/server/api/stream/server-stats.ts | 38 -- src/server/api/stream/user-list.ts | 13 - src/server/api/streaming.ts | 75 +--- src/services/drive/add-file.ts | 6 +- src/services/following/create.ts | 6 +- src/services/following/delete.ts | 4 +- src/services/following/requests/accept.ts | 6 +- src/services/following/requests/cancel.ts | 4 +- src/services/following/requests/create.ts | 6 +- src/services/following/requests/reject.ts | 4 +- src/services/note/create.ts | 14 +- src/services/note/delete.ts | 2 +- src/services/note/reaction/create.ts | 4 +- src/services/note/read.ts | 6 +- src/services/note/unread.ts | 6 +- src/stream.ts | 36 +- tsconfig.json | 3 +- 126 files changed, 1812 insertions(+), 2273 deletions(-) create mode 100644 src/client/app/common/scripts/note-subscriber.ts create mode 100644 src/client/app/common/scripts/stream.ts delete mode 100644 src/client/app/common/scripts/streaming/drive.ts delete mode 100644 src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts delete mode 100644 src/client/app/common/scripts/streaming/games/reversi/reversi.ts delete mode 100644 src/client/app/common/scripts/streaming/global-timeline.ts delete mode 100644 src/client/app/common/scripts/streaming/hashtag.ts delete mode 100644 src/client/app/common/scripts/streaming/home.ts delete mode 100644 src/client/app/common/scripts/streaming/hybrid-timeline.ts delete mode 100644 src/client/app/common/scripts/streaming/local-timeline.ts delete mode 100644 src/client/app/common/scripts/streaming/messaging-index.ts delete mode 100644 src/client/app/common/scripts/streaming/messaging.ts delete mode 100644 src/client/app/common/scripts/streaming/notes-stats.ts delete mode 100644 src/client/app/common/scripts/streaming/server-stats.ts delete mode 100644 src/client/app/common/scripts/streaming/stream-manager.ts delete mode 100644 src/client/app/common/scripts/streaming/stream.ts delete mode 100644 src/client/app/common/scripts/streaming/user-list.ts create mode 100644 src/server/api/stream/channel.ts create mode 100644 src/server/api/stream/channels/drive.ts create mode 100644 src/server/api/stream/channels/games/reversi-game.ts create mode 100644 src/server/api/stream/channels/games/reversi.ts create mode 100644 src/server/api/stream/channels/global-timeline.ts create mode 100644 src/server/api/stream/channels/hashtag.ts create mode 100644 src/server/api/stream/channels/home-timeline.ts create mode 100644 src/server/api/stream/channels/hybrid-timeline.ts create mode 100644 src/server/api/stream/channels/index.ts create mode 100644 src/server/api/stream/channels/local-timeline.ts create mode 100644 src/server/api/stream/channels/main.ts create mode 100644 src/server/api/stream/channels/messaging-index.ts create mode 100644 src/server/api/stream/channels/messaging.ts create mode 100644 src/server/api/stream/channels/notes-stats.ts create mode 100644 src/server/api/stream/channels/server-stats.ts create mode 100644 src/server/api/stream/channels/user-list.ts delete mode 100644 src/server/api/stream/drive.ts delete mode 100644 src/server/api/stream/games/reversi-game.ts delete mode 100644 src/server/api/stream/games/reversi.ts delete mode 100644 src/server/api/stream/global-timeline.ts delete mode 100644 src/server/api/stream/hashtag.ts delete mode 100644 src/server/api/stream/home.ts delete mode 100644 src/server/api/stream/hybrid-timeline.ts create mode 100644 src/server/api/stream/index.ts delete mode 100644 src/server/api/stream/local-timeline.ts delete mode 100644 src/server/api/stream/messaging-index.ts delete mode 100644 src/server/api/stream/messaging.ts delete mode 100644 src/server/api/stream/notes-stats.ts delete mode 100644 src/server/api/stream/server-stats.ts delete mode 100644 src/server/api/stream/user-list.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c492e4383..b26010b14 100644 --- a/CHANGELOG.md +++ b/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 ----- diff --git a/package.json b/package.json index 27bf5c0f1..dc76ad0f8 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts index f42af9437..65087cc98 100644 --- a/src/client/app/common/scripts/compose-notification.ts +++ b/src/client/app/common/scripts/compose-notification.ts @@ -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], diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts new file mode 100644 index 000000000..5fc82942d --- /dev/null +++ b/src/client/app/common/scripts/note-subscriber.ts @@ -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_); + }, + } +}); diff --git a/src/client/app/common/scripts/stream.ts b/src/client/app/common/scripts/stream.ts new file mode 100644 index 000000000..7dc130937 --- /dev/null +++ b/src/client/app/common/scripts/stream.ts @@ -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); + } +} diff --git a/src/client/app/common/scripts/streaming/drive.ts b/src/client/app/common/scripts/streaming/drive.ts deleted file mode 100644 index 50fff0573..000000000 --- a/src/client/app/common/scripts/streaming/drive.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts deleted file mode 100644 index adfa75ff3..000000000 --- a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts +++ /dev/null @@ -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 - }); - } -} diff --git a/src/client/app/common/scripts/streaming/games/reversi/reversi.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi.ts deleted file mode 100644 index 1f4fd8c63..000000000 --- a/src/client/app/common/scripts/streaming/games/reversi/reversi.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/client/app/common/scripts/streaming/global-timeline.ts b/src/client/app/common/scripts/streaming/global-timeline.ts deleted file mode 100644 index a639f1595..000000000 --- a/src/client/app/common/scripts/streaming/global-timeline.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/client/app/common/scripts/streaming/hashtag.ts b/src/client/app/common/scripts/streaming/hashtag.ts deleted file mode 100644 index 276b8f8d3..000000000 --- a/src/client/app/common/scripts/streaming/hashtag.ts +++ /dev/null @@ -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) - }); - } -} diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts deleted file mode 100644 index 26729507f..000000000 --- a/src/client/app/common/scripts/streaming/home.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/client/app/common/scripts/streaming/hybrid-timeline.ts b/src/client/app/common/scripts/streaming/hybrid-timeline.ts deleted file mode 100644 index cd290797c..000000000 --- a/src/client/app/common/scripts/streaming/hybrid-timeline.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/client/app/common/scripts/streaming/local-timeline.ts b/src/client/app/common/scripts/streaming/local-timeline.ts deleted file mode 100644 index 41c36aa14..000000000 --- a/src/client/app/common/scripts/streaming/local-timeline.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/client/app/common/scripts/streaming/messaging-index.ts b/src/client/app/common/scripts/streaming/messaging-index.ts deleted file mode 100644 index addcccb95..000000000 --- a/src/client/app/common/scripts/streaming/messaging-index.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/client/app/common/scripts/streaming/messaging.ts b/src/client/app/common/scripts/streaming/messaging.ts deleted file mode 100644 index a59377d86..000000000 --- a/src/client/app/common/scripts/streaming/messaging.ts +++ /dev/null @@ -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 - }); - }); - } -} diff --git a/src/client/app/common/scripts/streaming/notes-stats.ts b/src/client/app/common/scripts/streaming/notes-stats.ts deleted file mode 100644 index 9e3e78a70..000000000 --- a/src/client/app/common/scripts/streaming/notes-stats.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/client/app/common/scripts/streaming/server-stats.ts b/src/client/app/common/scripts/streaming/server-stats.ts deleted file mode 100644 index 9983dfcaf..000000000 --- a/src/client/app/common/scripts/streaming/server-stats.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/client/app/common/scripts/streaming/stream-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts deleted file mode 100644 index 8dd06f67d..000000000 --- a/src/client/app/common/scripts/streaming/stream-manager.ts +++ /dev/null @@ -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 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); - } - } -} diff --git a/src/client/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts deleted file mode 100644 index 4ab78f119..000000000 --- a/src/client/app/common/scripts/streaming/stream.ts +++ /dev/null @@ -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); - } -} diff --git a/src/client/app/common/scripts/streaming/user-list.ts b/src/client/app/common/scripts/streaming/user-list.ts deleted file mode 100644 index 30a52b98d..000000000 --- a/src/client/app/common/scripts/streaming/user-list.ts +++ /dev/null @@ -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 - }); - }); - } -} diff --git a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue index 1539c88de..0a18e0b19 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue @@ -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) { diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue index 3725aa6cb..a04016280 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.index.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue @@ -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(); } }, diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue index 6eb9511ce..f2156bc41 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.vue @@ -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); } }, diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue index 98661bc39..c2cd79e11 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/app/common/views/components/messaging-room.vue @@ -30,7 +30,6 @@