* 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:
syuilo 2018-10-07 11:06:17 +09:00 committed by GitHub
parent 0b98a2364b
commit d0570d7fe3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
126 changed files with 1812 additions and 2273 deletions

View file

@ -5,6 +5,88 @@ ChangeLog
This document describes breaking changes only. 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 9.0.0
----- -----

View file

@ -83,6 +83,7 @@
"@types/websocket": "0.0.40", "@types/websocket": "0.0.40",
"@types/ws": "6.0.1", "@types/ws": "6.0.1",
"animejs": "2.2.0", "animejs": "2.2.0",
"autobind-decorator": "2.1.0",
"autosize": "4.0.2", "autosize": "4.0.2",
"autwh": "0.1.0", "autwh": "0.1.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
@ -225,8 +226,8 @@
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
"web-push": "3.3.3", "web-push": "3.3.3",
"webfinger.js": "2.6.6", "webfinger.js": "2.6.6",
"webpack-cli": "3.1.2",
"webpack": "4.20.2", "webpack": "4.20.2",
"webpack-cli": "3.1.2",
"websocket": "1.0.28", "websocket": "1.0.28",
"ws": "6.0.0", "ws": "6.0.0",
"xev": "2.0.1" "xev": "2.0.1"

View file

@ -13,21 +13,21 @@ type Notification = {
export default function(type, data): Notification { export default function(type, data): Notification {
switch (type) { switch (type) {
case 'drive_file_created': case 'driveFileCreated':
return { return {
title: '%i18n:common.notification.file-uploaded%', title: '%i18n:common.notification.file-uploaded%',
body: data.name, body: data.name,
icon: data.url icon: data.url
}; };
case 'unread_messaging_message': case 'unreadMessagingMessage':
return { return {
title: '%i18n:common.notification.message-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split("{}")[1] , title: '%i18n:common.notification.message-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split("{}")[1] ,
body: data.text, // TODO: getMessagingMessageSummary(data), body: data.text, // TODO: getMessagingMessageSummary(data),
icon: data.user.avatarUrl icon: data.user.avatarUrl
}; };
case 'reversi_invited': case 'reversiInvited':
return { return {
title: '%i18n:common.notification.reversi-invited%', 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], body: '%i18n:common.notification.reversi-invited-by%'.split("{}")[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split("{}")[1],

View 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_);
},
}
});

View 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);
}
}

View file

@ -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;
}
}

View file

@ -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
});
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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)
});
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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
});
});
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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
});
});
}
}

View file

@ -9,7 +9,6 @@
import Vue from 'vue'; import Vue from 'vue';
import XGame from './reversi.game.vue'; import XGame from './reversi.game.vue';
import XRoom from './reversi.room.vue'; import XRoom from './reversi.room.vue';
import { ReversiGameStream } from '../../../../scripts/streaming/games/reversi/reversi-game';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -34,12 +33,13 @@ export default Vue.extend({
}, },
created() { created() {
this.g = this.game; 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); this.connection.on('started', this.onStarted);
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('started', this.onStarted); this.connection.dispose();
this.connection.close();
}, },
methods: { methods: {
onStarted(game) { onStarted(game) {

View file

@ -59,15 +59,13 @@ export default Vue.extend({
myGames: [], myGames: [],
matching: null, matching: null,
invitations: [], invitations: [],
connection: null, connection: null
connectionId: null
}; };
}, },
mounted() { mounted() {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.streams.reversiStream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('gamesReversi');
this.connectionId = (this as any).os.streams.reversiStream.use();
this.connection.on('invited', this.onInvited); this.connection.on('invited', this.onInvited);
@ -90,8 +88,7 @@ export default Vue.extend({
beforeDestroy() { beforeDestroy() {
if (this.connection) { if (this.connection) {
this.connection.off('invited', this.onInvited); this.connection.dispose();
(this as any).os.streams.reversiStream.dispose(this.connectionId);
} }
}, },

View file

@ -47,7 +47,6 @@ export default Vue.extend({
game: null, game: null,
matching: null, matching: null,
connection: null, connection: null,
connectionId: null,
pingClock: null pingClock: null
}; };
}, },
@ -66,8 +65,7 @@ export default Vue.extend({
this.fetch(); this.fetch();
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.streams.reversiStream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('gamesReversi');
this.connectionId = (this as any).os.streams.reversiStream.use();
this.connection.on('matched', this.onMatched); this.connection.on('matched', this.onMatched);
@ -84,9 +82,7 @@ export default Vue.extend({
beforeDestroy() { beforeDestroy() {
if (this.connection) { if (this.connection) {
this.connection.off('matched', this.onMatched); this.connection.dispose();
(this as any).os.streams.reversiStream.dispose(this.connectionId);
clearInterval(this.pingClock); clearInterval(this.pingClock);
} }
}, },

View file

@ -30,7 +30,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { MessagingStream } from '../../scripts/streaming/messaging';
import XMessage from './messaging-room.message.vue'; import XMessage from './messaging-room.message.vue';
import XForm from './messaging-room.form.vue'; import XForm from './messaging-room.form.vue';
import { url } from '../../../config'; import { url } from '../../../config';
@ -72,7 +71,7 @@ export default Vue.extend({
}, },
mounted() { 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('message', this.onMessage);
this.connection.on('read', this.onRead); this.connection.on('read', this.onRead);
@ -92,9 +91,7 @@ export default Vue.extend({
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('message', this.onMessage); this.connection.dispose();
this.connection.off('read', this.onRead);
this.connection.close();
if (this.isNaked) { if (this.isNaked) {
window.removeEventListener('scroll', this.onScroll); window.removeEventListener('scroll', this.onScroll);
@ -166,6 +163,7 @@ export default Vue.extend({
}, },
onMessage(message) { onMessage(message) {
console.log(message);
// //
if (this.$store.state.device.enableSounds) { if (this.$store.state.device.enableSounds) {
const sound = new Audio(`${url}/assets/message.mp3`); const sound = new Audio(`${url}/assets/message.mp3`);

View file

@ -71,13 +71,11 @@ export default Vue.extend({
messages: [], messages: [],
q: null, q: null,
result: [], result: [],
connection: null, connection: null
connectionId: null
}; };
}, },
mounted() { mounted() {
this.connection = (this as any).os.streams.messagingIndexStream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('messagingIndex');
this.connectionId = (this as any).os.streams.messagingIndexStream.use();
this.connection.on('message', this.onMessage); this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead); this.connection.on('read', this.onRead);
@ -88,9 +86,7 @@ export default Vue.extend({
}); });
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('message', this.onMessage); this.connection.dispose();
this.connection.off('read', this.onRead);
(this as any).os.streams.messagingIndexStream.dispose(this.connectionId);
}, },
methods: { methods: {
getAcct, getAcct,

View file

@ -56,7 +56,7 @@ export default Vue.extend({
username: this.username, username: this.username,
password: this.password, password: this.password,
token: this.user && this.user.twoFactorEnabled ? this.token : undefined token: this.user && this.user.twoFactorEnabled ? this.token : undefined
}).then(() => { }, true).then(() => {
location.reload(); location.reload();
}).catch(() => { }).catch(() => {
alert('%i18n:@login-failed%'); alert('%i18n:@login-failed%');

View file

@ -131,11 +131,11 @@ export default Vue.extend({
password: this.password, password: this.password,
invitationCode: this.invitationCode, invitationCode: this.invitationCode,
'g-recaptcha-response': this.meta.recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null 'g-recaptcha-response': this.meta.recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null
}).then(() => { }, true).then(() => {
(this as any).api('signin', { (this as any).api('signin', {
username: this.username, username: this.username,
password: this.password password: this.password
}).then(() => { }, true).then(() => {
location.href = '/'; location.href = '/';
}); });
}).catch(() => { }).catch(() => {

View file

@ -22,7 +22,7 @@ import * as anime from 'animejs';
export default Vue.extend({ export default Vue.extend({
computed: { computed: {
stream() { stream() {
return (this as any).os.stream; return (this as any).os.stream.useSharedConnection('main');
} }
}, },
created() { created() {

View file

@ -38,23 +38,20 @@ export default Vue.extend({
return { return {
fetching: true, fetching: true,
notes: [], notes: [],
connection: null, connection: null
connectionId: null
}; };
}, },
mounted() { mounted() {
this.fetch(); this.fetch();
this.connection = (this as any).os.streams.localTimelineStream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('localTimeline');
this.connectionId = (this as any).os.streams.localTimelineStream.use();
this.connection.on('note', this.onNote); this.connection.on('note', this.onNote);
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('note', this.onNote); this.connection.dispose();
(this as any).os.streams.localTimelineStream.dispose(this.connectionId);
}, },
methods: { methods: {

View file

@ -24,15 +24,13 @@ export default define({
return { return {
images: [], images: [],
fetching: true, fetching: true,
connection: null, connection: null
connectionId: null
}; };
}, },
mounted() { mounted() {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('drive_file_created', this.onDriveFileCreated); this.connection.on('driveFileCreated', this.onDriveFileCreated);
(this as any).api('drive/stream', { (this as any).api('drive/stream', {
type: 'image/*', type: 'image/*',
@ -43,8 +41,7 @@ export default define({
}); });
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('drive_file_created', this.onDriveFileCreated); this.connection.dispose();
(this as any).os.stream.dispose(this.connectionId);
}, },
methods: { methods: {
onDriveFileCreated(file) { onDriveFileCreated(file) {

View file

@ -82,7 +82,6 @@ export default define({
data() { data() {
return { return {
connection: null, connection: null,
connectionId: null,
viewBoxY: 30, viewBoxY: 30,
stats: [], stats: [],
fediGradientId: uuid(), fediGradientId: uuid(),
@ -110,8 +109,7 @@ export default define({
} }
}, },
mounted() { mounted() {
this.connection = (this as any).os.streams.notesStatsStream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('notesStats');
this.connectionId = (this as any).os.streams.notesStatsStream.use();
this.connection.on('stats', this.onStats); this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog); this.connection.on('statsLog', this.onStatsLog);
@ -121,9 +119,7 @@ export default define({
}); });
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('stats', this.onStats); this.connection.dispose();
this.connection.off('statsLog', this.onStatsLog);
(this as any).os.streams.notesStatsStream.dispose(this.connectionId);
}, },
methods: { methods: {
toggle() { toggle() {

View file

@ -45,8 +45,7 @@ export default define({
return { return {
fetching: true, fetching: true,
meta: null, meta: null,
connection: null, connection: null
connectionId: null
}; };
}, },
mounted() { mounted() {
@ -55,11 +54,10 @@ export default define({
this.fetching = false; this.fetching = false;
}); });
this.connection = (this as any).os.streams.serverStatsStream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('serverStats');
this.connectionId = (this as any).os.streams.serverStatsStream.use();
}, },
beforeDestroy() { beforeDestroy() {
(this as any).os.streams.serverStatsStream.dispose(this.connectionId); this.connection.dispose();
}, },
methods: { methods: {
toggle() { toggle() {

View file

@ -12,7 +12,7 @@ export const host = address.host;
export const hostname = address.hostname; export const hostname = address.hostname;
export const url = address.origin; export const url = address.origin;
export const apiUrl = url + '/api'; 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 lang = _LANG_;
export const langs = _LANGS_; export const langs = _LANGS_;
export const themeColor = _THEME_COLOR_; export const themeColor = _THEME_COLOR_;

View file

@ -9,7 +9,6 @@ import './style.styl';
import init from '../init'; import init from '../init';
import fuckAdBlock from '../common/scripts/fuck-ad-block'; import fuckAdBlock from '../common/scripts/fuck-ad-block';
import { HomeStreamManager } from '../common/scripts/streaming/home';
import composeNotification from '../common/scripts/compose-notification'; import composeNotification from '../common/scripts/compose-notification';
import chooseDriveFolder from './api/choose-drive-folder'; 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 MkReversi from './views/pages/games/reversi.vue';
import MkShare from './views/pages/share.vue'; import MkShare from './views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue'; import MkFollow from '../common/views/pages/follow.vue';
import MiOS from '../mios';
/** /**
* init * init
@ -102,23 +102,18 @@ init(async (launch) => {
} }
if ((Notification as any).permission == 'granted') { if ((Notification as any).permission == 'granted') {
registerNotifications(os.stream); registerNotifications(os);
} }
} }
}, true); }, true);
function registerNotifications(stream: HomeStreamManager) { function registerNotifications(os: MiOS) {
const stream = os.stream;
if (stream == null) return; if (stream == null) return;
if (stream.hasConnection) { const connection = stream.useSharedConnection('main');
attach(stream.borrow());
}
stream.on('connected', connection => {
attach(connection);
});
function attach(connection) {
connection.on('notification', notification => { connection.on('notification', notification => {
const _n = composeNotification('notification', notification); const _n = composeNotification('notification', notification);
const n = new Notification(_n.title, { const n = new Notification(_n.title, {
@ -128,8 +123,8 @@ function registerNotifications(stream: HomeStreamManager) {
setTimeout(n.close.bind(n), 6000); setTimeout(n.close.bind(n), 6000);
}); });
connection.on('drive_file_created', file => { connection.on('driveFileCreated', file => {
const _n = composeNotification('drive_file_created', file); const _n = composeNotification('driveFileCreated', file);
const n = new Notification(_n.title, { const n = new Notification(_n.title, {
body: _n.body, body: _n.body,
icon: _n.icon icon: _n.icon
@ -137,8 +132,8 @@ function registerNotifications(stream: HomeStreamManager) {
setTimeout(n.close.bind(n), 5000); setTimeout(n.close.bind(n), 5000);
}); });
connection.on('unread_messaging_message', message => { connection.on('unreadMessagingMessage', message => {
const _n = composeNotification('unread_messaging_message', message); const _n = composeNotification('unreadMessagingMessage', message);
const n = new Notification(_n.title, { const n = new Notification(_n.title, {
body: _n.body, body: _n.body,
icon: _n.icon icon: _n.icon
@ -152,12 +147,11 @@ function registerNotifications(stream: HomeStreamManager) {
setTimeout(n.close.bind(n), 7000); setTimeout(n.close.bind(n), 7000);
}); });
connection.on('reversi_invited', matching => { connection.on('reversiInvited', matching => {
const _n = composeNotification('reversi_invited', matching); const _n = composeNotification('reversiInvited', matching);
const n = new Notification(_n.title, { const n = new Notification(_n.title, {
body: _n.body, body: _n.body,
icon: _n.icon icon: _n.icon
}); });
}); });
} }
}

View file

@ -98,8 +98,7 @@ export default Vue.extend({
hierarchyFolders: [], hierarchyFolders: [],
selectedFiles: [], selectedFiles: [],
uploadings: [], uploadings: [],
connection: null, connection: null
connectionId: null,
/** /**
* ドロップされようとしているか * ドロップされようとしているか
@ -116,8 +115,7 @@ export default Vue.extend({
}; };
}, },
mounted() { mounted() {
this.connection = (this as any).os.streams.driveStream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('drive');
this.connectionId = (this as any).os.streams.driveStream.use();
this.connection.on('file_created', this.onStreamDriveFileCreated); this.connection.on('file_created', this.onStreamDriveFileCreated);
this.connection.on('file_updated', this.onStreamDriveFileUpdated); this.connection.on('file_updated', this.onStreamDriveFileUpdated);
@ -132,12 +130,7 @@ export default Vue.extend({
} }
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('file_created', this.onStreamDriveFileCreated); this.connection.dispose();
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);
}, },
methods: { methods: {
onContextmenu(e) { onContextmenu(e) {

View file

@ -34,23 +34,18 @@ export default Vue.extend({
return { return {
u: this.user, u: this.user,
wait: false, wait: false,
connection: null, connection: null
connectionId: null
}; };
}, },
mounted() { mounted() {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('follow', this.onFollow); this.connection.on('follow', this.onFollow);
this.connection.on('unfollow', this.onUnfollow); this.connection.on('unfollow', this.onUnfollow);
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('follow', this.onFollow); this.connection.dispose();
this.connection.off('unfollow', this.onUnfollow);
(this as any).os.stream.dispose(this.connectionId);
}, },
methods: { methods: {

View file

@ -141,7 +141,6 @@ export default Vue.extend({
data() { data() {
return { return {
connection: null, connection: null,
connectionId: null,
widgetAdderSelected: null, widgetAdderSelected: null,
trash: [] trash: []
}; };
@ -176,12 +175,11 @@ export default Vue.extend({
}, },
mounted() { mounted() {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
}, },
beforeDestroy() { beforeDestroy() {
(this as any).os.stream.dispose(this.connectionId); this.connection.dispose();
}, },
methods: { methods: {

View file

@ -93,12 +93,15 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './notes.note.sub.vue'; import XSub from './notes.note.sub.vue';
import { sum } from '../../../../../prelude/array'; import { sum } from '../../../../../prelude/array';
import noteSubscriber from '../../../common/scripts/note-subscriber';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XSub XSub
}, },
mixins: [noteSubscriber('note')],
props: { props: {
note: { note: {
type: Object, type: Object,

View file

@ -77,6 +77,7 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './notes.note.sub.vue'; import XSub from './notes.note.sub.vue';
import { sum } from '../../../../../prelude/array'; import { sum } from '../../../../../prelude/array';
import noteSubscriber from '../../../common/scripts/note-subscriber';
function focus(el, fn) { function focus(el, fn) {
const target = fn(el); const target = fn(el);
@ -94,6 +95,8 @@ export default Vue.extend({
XSub XSub
}, },
mixins: [noteSubscriber('note')],
props: { props: {
note: { note: {
type: Object, type: Object,
@ -104,9 +107,7 @@ export default Vue.extend({
data() { data() {
return { return {
showContent: false, showContent: false,
isDetailOpened: false, isDetailOpened: false
connection: null,
connectionId: null
}; };
}, },
@ -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: { 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) { reply(viaKeyboard = false) {
(this as any).os.new(MkPostFormWindow, { (this as any).os.new(MkPostFormWindow, {
reply: this.p, reply: this.p,

View file

@ -118,10 +118,10 @@ export default Vue.extend({
notifications: [], notifications: [],
moreNotifications: false, moreNotifications: false,
connection: null, connection: null,
connectionId: null,
getNoteSummary getNoteSummary
}; };
}, },
computed: { computed: {
_notifications(): any[] { _notifications(): any[] {
return (this.notifications as any).map(notification => { return (this.notifications as any).map(notification => {
@ -133,9 +133,9 @@ export default Vue.extend({
}); });
} }
}, },
mounted() { mounted() {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('notification', this.onNotification); this.connection.on('notification', this.onNotification);
@ -153,10 +153,11 @@ export default Vue.extend({
this.fetching = false; this.fetching = false;
}); });
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('notification', this.onNotification); this.connection.dispose();
(this as any).os.stream.dispose(this.connectionId);
}, },
methods: { methods: {
fetchMoreNotifications() { fetchMoreNotifications() {
this.fetchingMoreNotifications = true; this.fetchingMoreNotifications = true;
@ -177,10 +178,11 @@ export default Vue.extend({
this.fetchingMoreNotifications = false; this.fetchingMoreNotifications = false;
}); });
}, },
onNotification(notification) { onNotification(notification) {
// TODO: () // TODO: ()
this.connection.send({ this.connection.send({
type: 'read_notification', type: 'readNotification',
id: notification.id id: notification.id
}); });

View file

@ -23,25 +23,25 @@ export default Vue.extend({
return { return {
fetching: true, fetching: true,
signins: [], signins: [],
connection: null, connection: null
connectionId: null
}; };
}, },
mounted() { mounted() {
(this as any).api('i/signin_history').then(signins => { (this as any).api('i/signin_history').then(signins => {
this.signins = signins; this.signins = signins;
this.fetching = false; this.fetching = false;
}); });
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('signin', this.onSignin); this.connection.on('signin', this.onSignin);
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('signin', this.onSignin); this.connection.dispose();
(this as any).os.stream.dispose(this.connectionId);
}, },
methods: { methods: {
onSignin(signin) { onSignin(signin) {
this.signins.unshift(signin); this.signins.unshift(signin);

View file

@ -15,7 +15,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
const fetchLimit = 10; const fetchLimit = 10;
@ -35,9 +34,7 @@ export default Vue.extend({
fetching: true, fetching: true,
moreFetching: false, moreFetching: false,
existMore: false, existMore: false,
streamManager: null,
connection: null, connection: null,
connectionId: null,
date: null, date: null,
baseQuery: { baseQuery: {
includeMyRenotes: this.$store.state.settings.showMyRenotes, includeMyRenotes: this.$store.state.settings.showMyRenotes,
@ -69,69 +66,33 @@ export default Vue.extend({
this.query = { this.query = {
query: this.tagTl.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.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.connection.close();
});
} else if (this.src == 'home') { } else if (this.src == 'home') {
this.endpoint = 'notes/timeline'; this.endpoint = 'notes/timeline';
const onChangeFollowing = () => { const onChangeFollowing = () => {
this.fetch(); this.fetch();
}; };
this.streamManager = (this as any).os.stream; this.connection = (this as any).os.stream.useSharedConnection('homeTimeline');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend); this.connection.on('note', prepend);
this.connection.on('follow', onChangeFollowing); this.connection.on('follow', onChangeFollowing);
this.connection.on('unfollow', 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') { } else if (this.src == 'local') {
this.endpoint = 'notes/local-timeline'; this.endpoint = 'notes/local-timeline';
this.streamManager = (this as any).os.streams.localTimelineStream; this.connection = (this as any).os.stream.useSharedConnection('localTimeline');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend); this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'hybrid') { } else if (this.src == 'hybrid') {
this.endpoint = 'notes/hybrid-timeline'; this.endpoint = 'notes/hybrid-timeline';
this.streamManager = (this as any).os.streams.hybridTimelineStream; this.connection = (this as any).os.stream.useSharedConnection('hybridTimeline');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend); this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'global') { } else if (this.src == 'global') {
this.endpoint = 'notes/global-timeline'; this.endpoint = 'notes/global-timeline';
this.streamManager = (this as any).os.streams.globalTimelineStream; this.connection = (this as any).os.stream.useSharedConnection('globalTimeline');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend); this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'mentions') { } else if (this.src == 'mentions') {
this.endpoint = 'notes/mentions'; this.endpoint = 'notes/mentions';
this.streamManager = (this as any).os.stream; this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('mention', prepend); this.connection.on('mention', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('mention', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'messages') { } else if (this.src == 'messages') {
this.endpoint = 'notes/mentions'; this.endpoint = 'notes/mentions';
this.query = { this.query = {
@ -142,21 +103,15 @@ export default Vue.extend({
prepend(note); prepend(note);
} }
}; };
this.streamManager = (this as any).os.stream; this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('mention', onNote); this.connection.on('mention', onNote);
this.$once('beforeDestroy', () => {
this.connection.off('mention', onNote);
this.streamManager.dispose(this.connectionId);
});
} }
this.fetch(); this.fetch();
}, },
beforeDestroy() { beforeDestroy() {
this.$emit('beforeDestroy'); this.connection.dispose();
}, },
methods: { methods: {

View file

@ -42,8 +42,7 @@ export default Vue.extend({
data() { data() {
return { return {
hasGameInvitations: false, hasGameInvitations: false,
connection: null, connection: null
connectionId: null
}; };
}, },
computed: { computed: {
@ -53,18 +52,15 @@ export default Vue.extend({
}, },
mounted() { mounted() {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('reversi_invited', this.onReversiInvited); this.connection.on('reversiInvited', this.onReversiInvited);
this.connection.on('reversi_no_invites', this.onReversiNoInvites); this.connection.on('reversi_no_invites', this.onReversiNoInvites);
} }
}, },
beforeDestroy() { beforeDestroy() {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.off('reversi_invited', this.onReversiInvited); this.connection.dispose();
this.connection.off('reversi_no_invites', this.onReversiNoInvites);
(this as any).os.stream.dispose(this.connectionId);
} }
}, },
methods: { methods: {

View file

@ -6,7 +6,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { UserListStream } from '../../../common/scripts/streaming/user-list';
const fetchLimit = 10; const fetchLimit = 10;

View file

@ -56,13 +56,11 @@ export default Vue.extend({
disableLocalTimeline: false, disableLocalTimeline: false,
bannerUrl: null, bannerUrl: null,
inviteCode: null, inviteCode: null,
connection: null, connection: null
connectionId: null
}; };
}, },
created() { created() {
this.connection = (this as any).os.streams.serverStatsStream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('serverStats');
this.connectionId = (this as any).os.streams.serverStatsStream.use();
(this as any).os.getMeta().then(meta => { (this as any).os.getMeta().then(meta => {
this.disableRegistration = meta.disableRegistration; this.disableRegistration = meta.disableRegistration;
@ -75,7 +73,7 @@ export default Vue.extend({
}); });
}, },
beforeDestroy() { beforeDestroy() {
(this as any).os.streams.serverStatsStream.dispose(this.connectionId); this.connection.dispose();
}, },
methods: { methods: {
invite() { invite() {

View file

@ -21,23 +21,19 @@ export default Vue.extend({
fetching: true, fetching: true,
moreFetching: false, moreFetching: false,
existMore: false, existMore: false,
connection: null, connection: null
connectionId: null
}; };
}, },
mounted() { mounted() {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('mention', this.onNote); this.connection.on('mention', this.onNote);
this.fetch(); this.fetch();
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('mention', this.onNote); this.connection.dispose();
(this as any).os.stream.dispose(this.connectionId);
}, },
methods: { methods: {

View file

@ -5,7 +5,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XNotes from './deck.notes.vue'; import XNotes from './deck.notes.vue';
import { HashtagStream } from '../../../../common/scripts/streaming/hashtag';
const fetchLimit = 10; const fetchLimit = 10;
@ -48,7 +47,7 @@ export default Vue.extend({
mounted() { mounted() {
if (this.connection) this.connection.close(); 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.connection.on('note', this.onNote);
this.fetch(); this.fetch();

View file

@ -5,7 +5,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XNotes from './deck.notes.vue'; import XNotes from './deck.notes.vue';
import { UserListStream } from '../../../../common/scripts/streaming/user-list';
const fetchLimit = 10; const fetchLimit = 10;

View file

@ -21,23 +21,19 @@ export default Vue.extend({
fetching: true, fetching: true,
moreFetching: false, moreFetching: false,
existMore: false, existMore: false,
connection: null, connection: null
connectionId: null
}; };
}, },
mounted() { mounted() {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('mention', this.onNote); this.connection.on('mention', this.onNote);
this.fetch(); this.fetch();
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('mention', this.onNote); this.connection.dispose();
(this as any).os.stream.dispose(this.connectionId);
}, },
methods: { methods: {

View file

@ -70,12 +70,15 @@ import parse from '../../../../../../mfm/parse';
import MkNoteMenu from '../../../../common/views/components/note-menu.vue'; import MkNoteMenu from '../../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue'; import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue';
import XSub from './deck.note.sub.vue'; import XSub from './deck.note.sub.vue';
import noteSubscriber from '../../../../common/scripts/note-subscriber';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XSub XSub
}, },
mixins: [noteSubscriber('note')],
props: { props: {
note: { note: {
type: Object, type: Object,
@ -90,9 +93,7 @@ export default Vue.extend({
data() { data() {
return { return {
showContent: false, showContent: false
connection: null,
connectionId: null
}; };
}, },
@ -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: { 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() { reply() {
(this as any).apis.post({ (this as any).apis.post({
reply: this.p reply: this.p

View file

@ -38,8 +38,7 @@ export default Vue.extend({
notifications: [], notifications: [],
queue: [], queue: [],
moreNotifications: false, moreNotifications: false,
connection: null, connection: null
connectionId: null
}; };
}, },
@ -62,8 +61,7 @@ export default Vue.extend({
}, },
mounted() { mounted() {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('notification', this.onNotification); this.connection.on('notification', this.onNotification);
@ -86,8 +84,7 @@ export default Vue.extend({
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('notification', this.onNotification); this.connection.dispose();
(this as any).os.stream.dispose(this.connectionId);
this.column.$off('top', this.onTop); this.column.$off('top', this.onTop);
this.column.$off('bottom', this.onBottom); this.column.$off('bottom', this.onBottom);
@ -117,7 +114,7 @@ export default Vue.extend({
onNotification(notification) { onNotification(notification) {
// TODO: () // TODO: ()
this.connection.send({ this.connection.send({
type: 'read_notification', type: 'readNotification',
id: notification.id id: notification.id
}); });

View file

@ -36,18 +36,17 @@ export default Vue.extend({
fetching: true, fetching: true,
moreFetching: false, moreFetching: false,
existMore: false, existMore: false,
connection: null, connection: null
connectionId: null
}; };
}, },
computed: { computed: {
stream(): any { stream(): any {
switch (this.src) { switch (this.src) {
case 'home': return (this as any).os.stream; case 'home': return (this as any).os.stream.useSharedConnection('homeTimeline');
case 'local': return (this as any).os.streams.localTimelineStream; case 'local': return (this as any).os.stream.useSharedConnection('localTimeline');
case 'hybrid': return (this as any).os.streams.hybridTimelineStream; case 'hybrid': return (this as any).os.stream.useSharedConnection('hybridTimeline');
case 'global': return (this as any).os.streams.globalTimelineStream; case 'global': return (this as any).os.stream.useSharedConnection('globalTimeline');
} }
}, },
@ -68,8 +67,7 @@ export default Vue.extend({
}, },
mounted() { mounted() {
this.connection = this.stream.getConnection(); this.connection = this.stream;
this.connectionId = this.stream.use();
this.connection.on('note', this.onNote); this.connection.on('note', this.onNote);
if (this.src == 'home') { if (this.src == 'home') {
@ -81,12 +79,7 @@ export default Vue.extend({
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('note', this.onNote); this.connection.dispose();
if (this.src == 'home') {
this.connection.off('follow', this.onChangeFollowing);
this.connection.off('unfollow', this.onChangeFollowing);
}
this.stream.dispose(this.connectionId);
}, },
methods: { methods: {

View file

@ -1,3 +1,4 @@
import autobind from 'autobind-decorator';
import Vue from 'vue'; import Vue from 'vue';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
@ -5,19 +6,9 @@ import * as uuid from 'uuid';
import initStore from './store'; import initStore from './store';
import { apiUrl, version, lang } from './config'; import { apiUrl, version, lang } from './config';
import Progress from './common/scripts/loading'; 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 Err from './common/views/components/connect-failed.vue';
import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline'; import Stream from './common/scripts/stream';
import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline';
import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline';
import { erase } from '../../prelude/array';
//#region api requests //#region api requests
let spinner = null; let spinner = null;
@ -102,30 +93,7 @@ export default class MiOS extends EventEmitter {
/** /**
* A connection manager of home stream * A connection manager of home stream
*/ */
public stream: HomeStreamManager; public stream: Stream;
/**
* 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
};
/** /**
* A registration of service worker * A registration of service worker
@ -151,71 +119,36 @@ export default class MiOS extends EventEmitter {
this.shouldRegisterSw = shouldRegisterSw; 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) { if (this.debug) {
(window as any).os = this; (window as any).os = this;
} }
} }
private googleMapsIniting = false; @autobind
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
}
});
}
public log(...args) { public log(...args) {
if (!this.debug) return; if (!this.debug) return;
console.log.apply(null, args); console.log.apply(null, args);
} }
@autobind
public logInfo(...args) { public logInfo(...args) {
if (!this.debug) return; if (!this.debug) return;
console.info.apply(null, args); console.info.apply(null, args);
} }
@autobind
public logWarn(...args) { public logWarn(...args) {
if (!this.debug) return; if (!this.debug) return;
console.warn.apply(null, args); console.warn.apply(null, args);
} }
@autobind
public logError(...args) { public logError(...args) {
if (!this.debug) return; if (!this.debug) return;
console.error.apply(null, args); console.error.apply(null, args);
} }
@autobind
public signout() { public signout() {
this.store.dispatch('logout'); this.store.dispatch('logout');
location.href = '/'; location.href = '/';
@ -225,27 +158,10 @@ export default class MiOS extends EventEmitter {
* Initialize MiOS (boot) * Initialize MiOS (boot)
* @param callback A function that call when initialized * @param callback A function that call when initialized
*/ */
@autobind
public async init(callback) { public async init(callback) {
this.store = initStore(this); 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) => { const fetchme = (token, cb) => {
let me = null; let me = null;
@ -296,6 +212,8 @@ export default class MiOS extends EventEmitter {
const fetched = () => { const fetched = () => {
this.emit('signedin'); this.emit('signedin');
this.stream = new Stream(this);
// Finish init // Finish init
callback(); callback();
@ -328,6 +246,8 @@ export default class MiOS extends EventEmitter {
} else { } else {
// Finish init // Finish init
callback(); callback();
this.stream = new Stream(this);
} }
}); });
} }
@ -336,6 +256,7 @@ export default class MiOS extends EventEmitter {
/** /**
* Register service worker * Register service worker
*/ */
@autobind
private registerSw() { private registerSw() {
// Check whether service worker and push manager supported // Check whether service worker and push manager supported
const isSwSupported = const isSwSupported =
@ -418,7 +339,8 @@ export default class MiOS extends EventEmitter {
* @param endpoint * @param endpoint
* @param data * @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) { if (++pending === 1) {
spinner = document.createElement('div'); spinner = document.createElement('div');
spinner.setAttribute('id', 'wait'); spinner.setAttribute('id', 'wait');
@ -430,13 +352,12 @@ export default class MiOS extends EventEmitter {
}; };
const promise = new Promise((resolve, reject) => { 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) { if (viaStream) {
const stream = this.stream.borrow();
const id = Math.random().toString(); 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) { if (res == null || Object.keys(res).length == 0) {
resolve(null); resolve(null);
} else if (res.res) { } else if (res.res) {
@ -446,11 +367,10 @@ export default class MiOS extends EventEmitter {
} }
}); });
stream.send({ this.stream.send('api', {
type: 'api', id: id,
id, ep: endpoint,
endpoint, data: data
data
}); });
} else { } else {
// Append a credential // Append a credential
@ -503,6 +423,7 @@ export default class MiOS extends EventEmitter {
* Misskeyのメタ情報を取得します * Misskeyのメタ情報を取得します
* @param force * @param force
*/ */
@autobind
public getMeta(force = false) { public getMeta(force = false) {
return new Promise<{ [x: string]: any }>(async (res, rej) => { return new Promise<{ [x: string]: any }>(async (res, rej) => {
if (this.isMetaFetching) { 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 { class WindowSystem extends EventEmitter {

View file

@ -81,8 +81,7 @@ export default Vue.extend({
hierarchyFolders: [], hierarchyFolders: [],
selectedFiles: [], selectedFiles: [],
info: null, info: null,
connection: null, connection: null
connectionId: null,
fetching: true, fetching: true,
fetchingMoreFiles: false, fetchingMoreFiles: false,
@ -102,8 +101,7 @@ export default Vue.extend({
} }
}, },
mounted() { mounted() {
this.connection = (this as any).os.streams.driveStream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('drive');
this.connectionId = (this as any).os.streams.driveStream.use();
this.connection.on('file_created', this.onStreamDriveFileCreated); this.connection.on('file_created', this.onStreamDriveFileCreated);
this.connection.on('file_updated', this.onStreamDriveFileUpdated); this.connection.on('file_updated', this.onStreamDriveFileUpdated);
@ -124,12 +122,7 @@ export default Vue.extend({
} }
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('file_created', this.onStreamDriveFileCreated); this.connection.dispose();
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);
}, },
methods: { methods: {
onStreamDriveFileCreated(file) { onStreamDriveFileCreated(file) {

View file

@ -28,21 +28,17 @@ export default Vue.extend({
return { return {
u: this.user, u: this.user,
wait: false, wait: false,
connection: null, connection: null
connectionId: null
}; };
}, },
mounted() { mounted() {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('follow', this.onFollow); this.connection.on('follow', this.onFollow);
this.connection.on('unfollow', this.onUnfollow); this.connection.on('unfollow', this.onUnfollow);
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('follow', this.onFollow); this.connection.dispose();
this.connection.off('unfollow', this.onUnfollow);
(this as any).os.stream.dispose(this.connectionId);
}, },
methods: { methods: {

View file

@ -92,12 +92,15 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue'; import XSub from './note.sub.vue';
import { sum } from '../../../../../prelude/array'; import { sum } from '../../../../../prelude/array';
import noteSubscriber from '../../../common/scripts/note-subscriber';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XSub XSub
}, },
mixins: [noteSubscriber('note')],
props: { props: {
note: { note: {
type: Object, type: Object,

View file

@ -69,19 +69,20 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue'; import XSub from './note.sub.vue';
import { sum } from '../../../../../prelude/array'; import { sum } from '../../../../../prelude/array';
import noteSubscriber from '../../../common/scripts/note-subscriber';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XSub XSub
}, },
mixins: [noteSubscriber('note')],
props: ['note'], props: ['note'],
data() { data() {
return { return {
showContent: false, showContent: false
connection: null,
connectionId: null
}; };
}, },
@ -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: { 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() { reply() {
(this as any).apis.post({ (this as any).apis.post({
reply: this.p reply: this.p

View file

@ -23,6 +23,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
data() { data() {
return { return {
@ -30,10 +31,10 @@ export default Vue.extend({
fetchingMoreNotifications: false, fetchingMoreNotifications: false,
notifications: [], notifications: [],
moreNotifications: false, moreNotifications: false,
connection: null, connection: null
connectionId: null
}; };
}, },
computed: { computed: {
_notifications(): any[] { _notifications(): any[] {
return (this.notifications as any).map(notification => { return (this.notifications as any).map(notification => {
@ -45,9 +46,9 @@ export default Vue.extend({
}); });
} }
}, },
mounted() { mounted() {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('notification', this.onNotification); this.connection.on('notification', this.onNotification);
@ -66,10 +67,11 @@ export default Vue.extend({
this.$emit('fetched'); this.$emit('fetched');
}); });
}, },
beforeDestroy() { beforeDestroy() {
this.connection.off('notification', this.onNotification); this.connection.dispose();
(this as any).os.stream.dispose(this.connectionId);
}, },
methods: { methods: {
fetchMoreNotifications() { fetchMoreNotifications() {
this.fetchingMoreNotifications = true; this.fetchingMoreNotifications = true;
@ -90,10 +92,11 @@ export default Vue.extend({
this.fetchingMoreNotifications = false; this.fetchingMoreNotifications = false;
}); });
}, },
onNotification(notification) { onNotification(notification) {
// TODO: () // TODO: ()
this.connection.send({ this.connection.send({
type: 'read_notification', type: 'readNotification',
id: notification.id id: notification.id
}); });

View file

@ -24,44 +24,47 @@ import { env } from '../../../config';
export default Vue.extend({ export default Vue.extend({
props: ['func'], props: ['func'],
data() { data() {
return { return {
hasGameInvitation: false, hasGameInvitation: false,
connection: null, connection: null,
connectionId: null,
env: env env: env
}; };
}, },
computed: { computed: {
hasUnreadNotification(): boolean { hasUnreadNotification(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
}, },
hasUnreadMessagingMessage(): boolean { hasUnreadMessagingMessage(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
} }
}, },
mounted() { mounted() {
this.$store.commit('setUiHeaderHeight', this.$refs.root.offsetHeight); this.$store.commit('setUiHeaderHeight', this.$refs.root.offsetHeight);
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('reversi_invited', this.onReversiInvited); this.connection.on('reversiInvited', this.onReversiInvited);
this.connection.on('reversi_no_invites', this.onReversiNoInvites); this.connection.on('reversi_no_invites', this.onReversiNoInvites);
} }
}, },
beforeDestroy() { beforeDestroy() {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.off('reversi_invited', this.onReversiInvited); this.connection.dispose();
this.connection.off('reversi_no_invites', this.onReversiNoInvites);
(this as any).os.stream.dispose(this.connectionId);
} }
}, },
methods: { methods: {
onReversiInvited() { onReversiInvited() {
this.hasGameInvitation = true; this.hasGameInvitation = true;
}, },
onReversiNoInvites() { onReversiNoInvites() {
this.hasGameInvitation = false; this.hasGameInvitation = false;
} }

View file

@ -57,7 +57,6 @@ export default Vue.extend({
return { return {
hasGameInvitation: false, hasGameInvitation: false,
connection: null, connection: null,
connectionId: null,
aboutUrl: `/docs/${lang}/about`, aboutUrl: `/docs/${lang}/about`,
announcements: [] announcements: []
}; };
@ -79,19 +78,16 @@ export default Vue.extend({
}); });
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('reversi_invited', this.onReversiInvited); this.connection.on('reversiInvited', this.onReversiInvited);
this.connection.on('reversi_no_invites', this.onReversiNoInvites); this.connection.on('reversi_no_invites', this.onReversiNoInvites);
} }
}, },
beforeDestroy() { beforeDestroy() {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.off('reversi_invited', this.onReversiInvited); this.connection.dispose();
this.connection.off('reversi_no_invites', this.onReversiNoInvites);
(this as any).os.stream.dispose(this.connectionId);
} }
}, },

View file

@ -23,40 +23,43 @@ export default Vue.extend({
XHeader, XHeader,
XNav XNav
}, },
props: ['title'], props: ['title'],
data() { data() {
return { return {
isDrawerOpening: false, isDrawerOpening: false,
connection: null, connection: null
connectionId: null
}; };
}, },
watch: { watch: {
'$store.state.uiHeaderHeight'() { '$store.state.uiHeaderHeight'() {
this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
} }
}, },
mounted() { mounted() {
this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection(); this.connection = (this as any).os.stream.useSharedConnection('main');
this.connectionId = (this as any).os.stream.use();
this.connection.on('notification', this.onNotification); this.connection.on('notification', this.onNotification);
} }
}, },
beforeDestroy() { beforeDestroy() {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.off('notification', this.onNotification); this.connection.dispose();
(this as any).os.stream.dispose(this.connectionId);
} }
}, },
methods: { methods: {
onNotification(notification) { onNotification(notification) {
// TODO: () // TODO: ()
this.connection.send({ this.connection.send({
type: 'read_notification', type: 'readNotification',
id: notification.id id: notification.id
}); });

View file

@ -6,7 +6,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { UserListStream } from '../../../common/scripts/streaming/user-list';
const fetchLimit = 10; const fetchLimit = 10;

View file

@ -13,7 +13,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
const fetchLimit = 10; const fetchLimit = 10;
@ -35,7 +34,6 @@ export default Vue.extend({
existMore: false, existMore: false,
streamManager: null, streamManager: null,
connection: null, connection: null,
connectionId: null,
unreadCount: 0, unreadCount: 0,
date: null, date: null,
baseQuery: { baseQuery: {
@ -68,69 +66,33 @@ export default Vue.extend({
this.query = { this.query = {
query: this.tagTl.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.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.connection.close();
});
} else if (this.src == 'home') { } else if (this.src == 'home') {
this.endpoint = 'notes/timeline'; this.endpoint = 'notes/timeline';
const onChangeFollowing = () => { const onChangeFollowing = () => {
this.fetch(); this.fetch();
}; };
this.streamManager = (this as any).os.stream; this.connection = (this as any).os.stream.useSharedConnection('homeTimeline');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend); this.connection.on('note', prepend);
this.connection.on('follow', onChangeFollowing); this.connection.on('follow', onChangeFollowing);
this.connection.on('unfollow', 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') { } else if (this.src == 'local') {
this.endpoint = 'notes/local-timeline'; this.endpoint = 'notes/local-timeline';
this.streamManager = (this as any).os.streams.localTimelineStream; this.connection = (this as any).os.stream.useSharedConnection('localTimeline');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend); this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'hybrid') { } else if (this.src == 'hybrid') {
this.endpoint = 'notes/hybrid-timeline'; this.endpoint = 'notes/hybrid-timeline';
this.streamManager = (this as any).os.streams.hybridTimelineStream; this.connection = (this as any).os.stream.useSharedConnection('hybridTimeline');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend); this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'global') { } else if (this.src == 'global') {
this.endpoint = 'notes/global-timeline'; this.endpoint = 'notes/global-timeline';
this.streamManager = (this as any).os.streams.globalTimelineStream; this.connection = (this as any).os.stream.useSharedConnection('globalTimeline');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('note', prepend); this.connection.on('note', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('note', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'mentions') { } else if (this.src == 'mentions') {
this.endpoint = 'notes/mentions'; this.endpoint = 'notes/mentions';
this.streamManager = (this as any).os.stream; this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('mention', prepend); this.connection.on('mention', prepend);
this.$once('beforeDestroy', () => {
this.connection.off('mention', prepend);
this.streamManager.dispose(this.connectionId);
});
} else if (this.src == 'messages') { } else if (this.src == 'messages') {
this.endpoint = 'notes/mentions'; this.endpoint = 'notes/mentions';
this.query = { this.query = {
@ -141,21 +103,15 @@ export default Vue.extend({
prepend(note); prepend(note);
} }
}; };
this.streamManager = (this as any).os.stream; this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection = this.streamManager.getConnection();
this.connectionId = this.streamManager.use();
this.connection.on('mention', onNote); this.connection.on('mention', onNote);
this.$once('beforeDestroy', () => {
this.connection.off('mention', onNote);
this.streamManager.dispose(this.connectionId);
});
} }
this.fetch(); this.fetch();
}, },
beforeDestroy() { beforeDestroy() {
this.$emit('beforeDestroy'); this.connection.dispose();
}, },
methods: { methods: {

View file

@ -14,7 +14,8 @@
"removeComments": false, "removeComments": false,
"noLib": false, "noLib": false,
"strict": true, "strict": true,
"strictNullChecks": false "strictNullChecks": false,
"experimentalDecorators": true
}, },
"compileOnSave": false, "compileOnSave": false,
"include": [ "include": [

View file

@ -55,7 +55,7 @@ APIへリクエストすると、レスポンスがストリームから次の
```json ```json
{ {
type: 'api-res:xxxxxxxxxxxxxxxx', type: 'api:xxxxxxxxxxxxxxxx',
body: { body: {
... ...
} }
@ -95,7 +95,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
```json ```json
{ {
type: 'note-updated', type: 'noteUpdated',
body: { body: {
note: { note: {
... ...
@ -108,7 +108,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
--- ---
このように、投稿の情報が更新されると、`note-updated`イベントが流れてくるようになります。`note-updated`イベントが発生するのは、以下の場合です: このように、投稿の情報が更新されると、`noteUpdated`イベントが流れてくるようになります。`noteUpdated`イベントが発生するのは、以下の場合です:
- 投稿にリアクションが付いた - 投稿にリアクションが付いた
- 投稿に添付されたアンケートに投票がされた - 投稿に添付されたアンケートに投票がされた
@ -153,7 +153,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
`body`プロパティの中に、投稿情報が含まれています。 `body`プロパティの中に、投稿情報が含まれています。
### `read_all_notifications` ### `readAllNotifications`
自分宛ての通知がすべて既読になったことを表すイベントです。このイベントを利用して、「通知があることを示すアイコン」のようなものをオフにしたりする等のケースが想定されます。 自分宛ての通知がすべて既読になったことを表すイベントです。このイベントを利用して、「通知があることを示すアイコン」のようなものをオフにしたりする等のケースが想定されます。

View file

@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
import Notification from './models/notification'; import Notification from './models/notification';
import Mute from './models/mute'; import Mute from './models/mute';
import { pack } from './models/notification'; import { pack } from './models/notification';
import { publishUserStream } from './stream'; import { publishMainStream } from './stream';
import User from './models/user'; import User from './models/user';
import pushSw from './push-sw'; import pushSw from './push-sw';
@ -30,7 +30,7 @@ export default (
const packed = await pack(notification); const packed = await pack(notification);
// Publish notification event // Publish notification event
publishUserStream(notifiee, 'notification', packed); publishMainStream(notifiee, 'notification', packed);
// Update flag // Update flag
User.update({ _id: notifiee }, { User.update({ _id: notifiee }, {
@ -54,7 +54,7 @@ export default (
} }
//#endregion //#endregion
publishUserStream(notifiee, 'unread_notification', packed); publishMainStream(notifiee, 'unreadNotification', packed);
pushSw(notifiee, 'notification', packed); pushSw(notifiee, 'notification', packed);
} }

View file

@ -9,6 +9,10 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any)
const ep = endpoints.find(e => e.name === endpoint); const ep = endpoints.find(e => e.name === endpoint);
if (ep == null) {
return rej('ENDPOINT_NOT_FOUND');
}
if (ep.meta.secure && !isSecure) { if (ep.meta.secure && !isSecure) {
return rej('ACCESS_DENIED'); return rej('ACCESS_DENIED');
} }

View file

@ -1,7 +1,7 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import Message from '../../../models/messaging-message'; import Message from '../../../models/messaging-message';
import { IMessagingMessage as IMessage } 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 { publishMessagingStream } from '../../../stream';
import { publishMessagingIndexStream } from '../../../stream'; import { publishMessagingIndexStream } from '../../../stream';
import User from '../../../models/user'; import User from '../../../models/user';
@ -71,6 +71,6 @@ export default (
}); });
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishUserStream(userId, 'read_all_messaging_messages'); publishMainStream(userId, 'readAllMessagingMessages');
} }
}); });

View file

@ -1,6 +1,6 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import { default as Notification, INotification } from '../../../models/notification'; import { default as Notification, INotification } from '../../../models/notification';
import { publishUserStream } from '../../../stream'; import { publishMainStream } from '../../../stream';
import Mute from '../../../models/mute'; import Mute from '../../../models/mute';
import User from '../../../models/user'; import User from '../../../models/user';
@ -66,6 +66,6 @@ export default (
}); });
// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行 // 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
publishUserStream(userId, 'read_all_notifications'); publishMainStream(userId, 'readAllNotifications');
} }
}); });

View file

@ -2,7 +2,7 @@ import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching'; import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching';
import ReversiGame, { pack as packGame } from '../../../../../models/games/reversi/game'; import ReversiGame, { pack as packGame } from '../../../../../models/games/reversi/game';
import User, { ILocalUser } from '../../../../../models/user'; import User, { ILocalUser } from '../../../../../models/user';
import { publishUserStream, publishReversiStream } from '../../../../../stream'; import { publishMainStream, publishReversiStream } from '../../../../../stream';
import { eighteight } from '../../../../../games/reversi/maps'; import { eighteight } from '../../../../../games/reversi/maps';
export const meta = { export const meta = {
@ -58,7 +58,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
}); });
if (other == 0) { if (other == 0) {
publishUserStream(user._id, 'reversi_no_invites'); publishMainStream(user._id, 'reversi_no_invites');
} }
} else { } else {
// Fetch child // Fetch child
@ -94,6 +94,6 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
// 招待 // 招待
publishReversiStream(child._id, 'invited', packed); publishReversiStream(child._id, 'invited', packed);
publishUserStream(child._id, 'reversi_invited', packed); publishMainStream(child._id, 'reversiInvited', packed);
} }
}); });

View file

@ -1,7 +1,7 @@
import $ from 'cafy'; import $ from 'cafy';
import * as bcrypt from 'bcryptjs'; import * as bcrypt from 'bcryptjs';
import User, { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
import { publishUserStream } from '../../../../stream'; import { publishMainStream } from '../../../../stream';
import generateUserToken from '../../common/generate-native-user-token'; import generateUserToken from '../../common/generate-native-user-token';
export const meta = { export const meta = {
@ -33,5 +33,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
res(); res();
// Publish event // Publish event
publishUserStream(user._id, 'my_token_regenerated'); publishMainStream(user._id, 'myTokenRegenerated');
}); });

View file

@ -1,6 +1,6 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack, ILocalUser } from '../../../../models/user'; 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 DriveFile from '../../../../models/drive-file';
import acceptAllFollowRequests from '../../../../services/following/requests/accept-all'; import acceptAllFollowRequests from '../../../../services/following/requests/accept-all';
import { IApp } from '../../../../models/app'; import { IApp } from '../../../../models/app';
@ -177,7 +177,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a
res(iObj); res(iObj);
// Publish meUpdated event // Publish meUpdated event
publishUserStream(user._id, 'meUpdated', iObj); publishMainStream(user._id, 'meUpdated', iObj);
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
if (user.isLocked && ps.isLocked === false) { if (user.isLocked && ps.isLocked === false) {

View file

@ -1,6 +1,6 @@
import $ from 'cafy'; import $ from 'cafy';
import User, { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
import { publishUserStream } from '../../../../stream'; import { publishMainStream } from '../../../../stream';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -26,7 +26,7 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
res(); res();
// Publish event // Publish event
publishUserStream(user._id, 'clientSettingUpdated', { publishMainStream(user._id, 'clientSettingUpdated', {
key: name, key: name,
value value
}); });

View file

@ -1,6 +1,6 @@
import $ from 'cafy'; import $ from 'cafy';
import User, { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
import { publishUserStream } from '../../../../stream'; import { publishMainStream } from '../../../../stream';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -25,5 +25,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
res(); res();
publishUserStream(user._id, 'home_updated', home); publishMainStream(user._id, 'homeUpdated', home);
}); });

View file

@ -1,6 +1,6 @@
import $ from 'cafy'; import $ from 'cafy';
import User, { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
import { publishUserStream } from '../../../../stream'; import { publishMainStream } from '../../../../stream';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -24,5 +24,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
res(); res();
publishUserStream(user._id, 'mobile_home_updated', home); publishMainStream(user._id, 'mobileHomeUpdated', home);
}); });

View file

@ -1,6 +1,6 @@
import $ from 'cafy'; import $ from 'cafy';
import User, { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
import { publishUserStream } from '../../../../stream'; import { publishMainStream } from '../../../../stream';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -73,7 +73,7 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
//#endregion //#endregion
if (widget) { if (widget) {
publishUserStream(user._id, 'widgetUpdated', { publishMainStream(user._id, 'widgetUpdated', {
id, data id, data
}); });

View file

@ -6,7 +6,7 @@ import User, { ILocalUser } from '../../../../../models/user';
import Mute from '../../../../../models/mute'; import Mute from '../../../../../models/mute';
import DriveFile from '../../../../../models/drive-file'; import DriveFile from '../../../../../models/drive-file';
import { pack } from '../../../../../models/messaging-message'; import { pack } from '../../../../../models/messaging-message';
import { publishUserStream } from '../../../../../stream'; import { publishMainStream } from '../../../../../stream';
import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../stream'; import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../stream';
import pushSw from '../../../../../push-sw'; 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); publishMessagingStream(message.userId, message.recipientId, 'message', messageObj);
publishMessagingIndexStream(message.userId, '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); publishMessagingStream(message.recipientId, message.userId, 'message', messageObj);
publishMessagingIndexStream(message.recipientId, 'message', messageObj); publishMessagingIndexStream(message.recipientId, 'message', messageObj);
publishUserStream(message.recipientId, 'messaging_message', messageObj); publishMainStream(message.recipientId, 'messagingMessage', messageObj);
// Update flag // Update flag
User.update({ _id: recipient._id }, { User.update({ _id: recipient._id }, {
@ -117,8 +117,8 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
} }
//#endregion //#endregion
publishUserStream(message.recipientId, 'unread_messaging_message', messageObj); publishMainStream(message.recipientId, 'unreadMessagingMessage', messageObj);
pushSw(message.recipientId, 'unread_messaging_message', messageObj); pushSw(message.recipientId, 'unreadMessagingMessage', messageObj);
} }
}, 3000); }, 3000);

View file

@ -72,7 +72,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
$inc: inc $inc: inc
}); });
publishNoteStream(note._id, 'poll_voted'); publishNoteStream(note._id, 'pollVoted', {
choice: choice,
userId: user._id.toHexString()
});
// Notify // Notify
notify(note.userId, user._id, 'poll_vote', { notify(note.userId, user._id, 'poll_vote', {

View file

@ -1,5 +1,5 @@
import Notification from '../../../../models/notification'; import Notification from '../../../../models/notification';
import { publishUserStream } from '../../../../stream'; import { publishMainStream } from '../../../../stream';
import User, { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
export const meta = { 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');
}); });

View file

@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs';
import * as speakeasy from 'speakeasy'; import * as speakeasy from 'speakeasy';
import User, { ILocalUser } from '../../../models/user'; import User, { ILocalUser } from '../../../models/user';
import Signin, { pack } from '../../../models/signin'; import Signin, { pack } from '../../../models/signin';
import { publishUserStream } from '../../../stream'; import { publishMainStream } from '../../../stream';
import signin from '../common/signin'; import signin from '../common/signin';
import config from '../../../config'; import config from '../../../config';
@ -87,5 +87,5 @@ export default async (ctx: Koa.Context) => {
}); });
// Publish signin event // Publish signin event
publishUserStream(user._id, 'signin', await pack(record)); publishMainStream(user._id, 'signin', await pack(record));
}; };

View file

@ -4,7 +4,7 @@ import * as uuid from 'uuid';
import autwh from 'autwh'; import autwh from 'autwh';
import redis from '../../../db/redis'; import redis from '../../../db/redis';
import User, { pack, ILocalUser } from '../../../models/user'; import User, { pack, ILocalUser } from '../../../models/user';
import { publishUserStream } from '../../../stream'; import { publishMainStream } from '../../../stream';
import config from '../../../config'; import config from '../../../config';
import signin from '../common/signin'; import signin from '../common/signin';
@ -49,7 +49,7 @@ router.get('/disconnect/twitter', async ctx => {
ctx.body = `Twitterの連携を解除しました :v:`; ctx.body = `Twitterの連携を解除しました :v:`;
// Publish i updated event // Publish i updated event
publishUserStream(user._id, 'meUpdated', await pack(user, user, { publishMainStream(user._id, 'meUpdated', await pack(user, user, {
detail: true, detail: true,
includeSecrets: true includeSecrets: true
})); }));
@ -174,7 +174,7 @@ if (config.twitter == null) {
ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
// Publish i updated event // Publish i updated event
publishUserStream(user._id, 'meUpdated', await pack(user, user, { publishMainStream(user._id, 'meUpdated', await pack(user, user, {
detail: true, detail: true,
includeSecrets: true includeSecrets: true
})); }));

View 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;
}

View 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);
});
}
}

View 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));
}
}
}

View 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;
}
}
}

View 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);
}
}

View 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);
});
}
}

View 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);
}
}

View 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);
}
}

View 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
};

View 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);
}
}

View 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;
}
}
});
}
}

View 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);
});
}
}

View 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;
}
}
}

View 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);
}
}

View 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);
}
}

View 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);
});
}
}

View file

@ -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));
});
}

View file

@ -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)
}));
}
}
}

View file

@ -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