diff --git a/package-lock.json b/package-lock.json index cb6c684..e619121 100644 --- a/package-lock.json +++ b/package-lock.json @@ -359,6 +359,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, + "reconnecting-websocket": { + "version": "4.0.0-rc5", + "resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.0.0-rc5.tgz", + "integrity": "sha512-ew+Twq9j66vhRtW9mT0xIgkLCQsDpslAideVYuB1JjW4U9wm27XZfA786K6pCKcUFkDWmktL+uI92ITLdn2eOQ==" + }, "request": { "version": "2.87.0", "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", diff --git a/package.json b/package.json index e37db18..e539d8f 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@types/node": "10.0.5", "@types/ws": "5.1.2", "misskey-reversi": "0.0.5", + "reconnecting-websocket": "4.0.0-rc5", "request": "2.87.0", "request-promise-native": "1.0.5", "ts-node": "6.0.3", diff --git a/src/front.ts b/src/front.ts deleted file mode 100644 index b7c3be1..0000000 --- a/src/front.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * -AI- - * Botのフロントエンド(ストリームとの対話を担当) - * - * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから - * 切断されてしまうので、別々のプロセスで行うようにします - */ - -import * as childProcess from 'child_process'; -import * as WebSocket from 'ws'; -import * as request from 'request-promise-native'; - -const config = require('../config.json'); - -const wsUrl = config.host.replace('http', 'ws'); -const apiUrl = config.host + '/api'; - -/** - * ホームストリーム - */ -const homeStream = new WebSocket(`${wsUrl}/?i=${config.i}`); - -homeStream.addEventListener('open', () => { - console.log('home stream opened'); -}); - -homeStream.addEventListener('close', () => { - console.log('home stream closed'); - - process.exit(1); -}); - -homeStream.addEventListener('message', message => { - const msg = JSON.parse(message.data); - - // タイムライン上でなんか言われたまたは返信されたとき - if (msg.type == 'mention' || msg.type == 'reply') { - const note = msg.body; - - if (note.userId == config.id) return; - - // リアクションする - setTimeout(() => { - request.post(`${apiUrl}/notes/reactions/create`, { - json: { - i: config.i, - noteId: note.id, - reaction: 'love' - } - }); - }, 2000); - - if (note.text && note.text.indexOf('リバーシ') > -1) { - setTimeout(() => { - request.post(`${apiUrl}/notes/create`, { - json: { - i: config.i, - replyId: note.id, - text: '良いですよ~' - } - }); - - invite(note.userId); - }, 3000); - } - } - - // メッセージでなんか言われたとき - if (msg.type == 'messaging_message') { - const message = msg.body; - if (message.text) { - if (message.text.indexOf('リバーシ') > -1) { - request.post(`${apiUrl}/messaging/messages/create`, { - json: { - i: config.i, - userId: message.userId, - text: '良いですよ~' - } - }); - - invite(message.userId); - } - } - } -}); - -// ユーザーを対局に誘う -function invite(userId) { - request.post(`${apiUrl}/games/reversi/match`, { - json: { - i: config.i, - userId: userId - } - }); -} - -/** - * リバーシストリーム - */ -const reversiStream = new WebSocket(`${wsUrl}/games/reversi?i=${config.i}`); - -reversiStream.addEventListener('open', () => { - console.log('reversi stream opened'); -}); - -reversiStream.addEventListener('close', () => { - console.log('reversi stream closed'); -}); - -reversiStream.addEventListener('message', message => { - const msg = JSON.parse(message.data); - - // 招待されたとき - if (msg.type == 'invited') { - onInviteMe(msg.body.parent); - } - - // マッチしたとき - if (msg.type == 'matched') { - gameStart(msg.body); - } -}); - -/** - * ゲーム開始 - * @param game ゲーム情報 - */ -function gameStart(game) { - // ゲームストリームに接続 - const gw = new WebSocket(`${wsUrl}/games/reversi-game?i=${config.i}&game=${game.id}`); - - function send(msg) { - try { - gw.send(JSON.stringify(msg)); - } catch (e) { - console.error(e); - } - } - - gw.addEventListener('open', () => { - console.log('reversi game stream opened'); - - // フォーム - const form = [{ - id: 'publish', - type: 'switch', - label: '藍が対局情報を投稿するのを許可', - value: true - }, { - id: 'strength', - type: 'radio', - label: '強さ', - value: 3, - items: [{ - label: '接待', - value: 0 - }, { - label: '弱', - value: 2 - }, { - label: '中', - value: 3 - }, { - label: '強', - value: 4 - }, { - label: '最強', - value: 5 - }] - }]; - - //#region バックエンドプロセス開始 - const ai = childProcess.fork(__dirname + '/back.js'); - - // バックエンドプロセスに情報を渡す - ai.send({ - type: '_init_', - game, - form - }); - - ai.on('message', msg => { - if (msg.type == 'put') { - send({ - type: 'set', - pos: msg.pos - }); - } else if (msg.type == 'close') { - gw.close(); - } - }); - - // ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える - gw.addEventListener('message', message => { - const msg = JSON.parse(message.data); - ai.send(msg); - }); - //#endregion - - // フォーム初期化 - setTimeout(() => { - send({ - type: 'init-form', - body: form - }); - }, 1000); - - // どんな設定内容の対局でも受け入れる - setTimeout(() => { - send({ - type: 'accept' - }); - }, 2000); - }); - - gw.addEventListener('close', () => { - console.log('reversi game stream closed'); - }); -} - -/** - * リバーシの対局に招待されたとき - * @param inviter 誘ってきたユーザー - */ -async function onInviteMe(inviter) { - console.log(`Someone invited me: @${inviter.username}`); - - // 承認 - const game = await request.post(`${apiUrl}/games/reversi/match`, { - json: { - i: config.i, - userId: inviter.id - } - }); - - gameStart(game); -} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..899b09f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,319 @@ +import * as childProcess from 'child_process'; +import * as WebSocket from 'ws'; +import * as request from 'request-promise-native'; +const ReconnectingWebSocket = require('../node_modules/reconnecting-websocket/dist/reconnecting-websocket-cjs.js'); + +import serifs from './serifs'; +const config = require('../config.json'); + +const wsUrl = config.host.replace('http', 'ws'); +const apiUrl = config.host + '/api'; + +class MessageLike { + private ai: 藍; + private messageOrNote: any; + public isMessage: boolean; + + public get id() { + return this.messageOrNote.id; + } + + public get userId() { + return this.messageOrNote.userId; + } + + public get text() { + return this.messageOrNote.text; + } + + constructor(ai: 藍, messageOrNote: any, isMessage: boolean) { + this.ai = ai; + this.messageOrNote = messageOrNote; + this.isMessage = isMessage; + } + + public reply = (text: string) => { + setTimeout(() => { + if (this.isMessage) { + this.ai.sendMessage(this.messageOrNote.userId, { + text: text + }); + } else { + this.ai.post({ + replyId: this.messageOrNote.id, + text: text + }); + } + }, 2000); + } +} + +/** + * 藍 + */ +class 藍 { + + /** + * ホームストリーム + */ + private connection: any; + + /** + * リバーシストリーム + */ + private reversiConnection?: any; + + constructor() { + this.connection = new ReconnectingWebSocket(`${wsUrl}/?i=${config.i}`, [], { + WebSocket: WebSocket + }); + + this.connection.addEventListener('open', () => { + console.log('home stream opened'); + }); + + this.connection.addEventListener('close', () => { + console.log('home stream closed'); + }); + + this.connection.addEventListener('message', message => { + const msg = JSON.parse(message.data); + + this.onMessage(msg); + }); + + if (config.reversiEnabled) { + this.reversiConnection = new ReconnectingWebSocket(`${wsUrl}/games/reversi?i=${config.i}`, [], { + WebSocket: WebSocket + }); + + this.reversiConnection.addEventListener('open', () => { + console.log('reversi stream opened'); + }); + + this.reversiConnection.addEventListener('close', () => { + console.log('reversi stream closed'); + }); + + this.reversiConnection.addEventListener('message', message => { + const msg = JSON.parse(message.data); + + this.onReversiConnectionMessage(msg); + }); + } + } + + private onMessage = (msg: any) => { + switch (msg.type) { + // メンションされたとき + case 'mention': { + if (msg.body.userId == config.id) return; // 自分は弾く + this.onMention(new MessageLike(this, msg.body, false)); + break; + } + + // 返信されたとき + case 'reply': { + if (msg.body.userId == config.id) return; // 自分は弾く + this.onMention(new MessageLike(this, msg.body, false)); + break; + } + + // メッセージ + case 'messaging_message': { + if (msg.body.userId == config.id) return; // 自分は弾く + this.onMention(new MessageLike(this, msg.body, true)); + break; + } + + default: + break; + } + } + + private onReversiConnectionMessage = (msg: any) => { + switch (msg.type) { + + // 招待されたとき + case 'invited': { + this.onReversiInviteMe(msg.body.parent); + break; + } + + // マッチしたとき + case 'matched': { + this.onReversiGameStart(msg.body); + break; + } + + default: + break; + } + } + + private onReversiInviteMe = async (inviter: any) => { + console.log(`Someone invited me: @${inviter.username}`); + + if (config.reversiEnabled) { + // 承認 + const game = await request.post(`${apiUrl}/games/reversi/match`, { + json: { + i: config.i, + userId: inviter.id + } + }); + + this.onReversiGameStart(game); + } else { + // todo (リバーシできない旨をメッセージで伝えるなど) + } + } + + private onReversiGameStart = (game: any) => { + // ゲームストリームに接続 + const gw = new ReconnectingWebSocket(`${wsUrl}/games/reversi-game?i=${config.i}&game=${game.id}`, [], { + WebSocket: WebSocket + }); + + function send(msg) { + try { + gw.send(JSON.stringify(msg)); + } catch (e) { + console.error(e); + } + } + + gw.addEventListener('open', () => { + console.log('reversi game stream opened'); + + // フォーム + const form = [{ + id: 'publish', + type: 'switch', + label: '藍が対局情報を投稿するのを許可', + value: true + }, { + id: 'strength', + type: 'radio', + label: '強さ', + value: 3, + items: [{ + label: '接待', + value: 0 + }, { + label: '弱', + value: 2 + }, { + label: '中', + value: 3 + }, { + label: '強', + value: 4 + }, { + label: '最強', + value: 5 + }] + }]; + + //#region バックエンドプロセス開始 + const ai = childProcess.fork(__dirname + '/back.js'); + + // バックエンドプロセスに情報を渡す + ai.send({ + type: '_init_', + game, + form + }); + + ai.on('message', msg => { + if (msg.type == 'put') { + send({ + type: 'set', + pos: msg.pos + }); + } else if (msg.type == 'close') { + gw.close(); + } + }); + + // ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える + gw.addEventListener('message', message => { + const msg = JSON.parse(message.data); + ai.send(msg); + }); + //#endregion + + // フォーム初期化 + setTimeout(() => { + send({ + type: 'init-form', + body: form + }); + }, 1000); + + // どんな設定内容の対局でも受け入れる + setTimeout(() => { + send({ + type: 'accept' + }); + }, 2000); + }); + + gw.addEventListener('close', () => { + console.log('reversi game stream closed'); + }); + } + + private onMention = (x: MessageLike) => { + // リアクションする + if (!x.isMessage) { + setTimeout(() => { + request.post(`${apiUrl}/notes/reactions/create`, { + json: { + i: config.i, + noteId: x.id, + reaction: 'love' + } + }); + }, 1000); + } + + if (x.text && x.text.indexOf('リバーシ') > -1) { + if (config.reversiEnabled) { + x.reply(serifs.REVERSI_OK); + + request.post(`${apiUrl}/games/reversi/match`, { + json: { + i: config.i, + userId: x.userId + } + }); + } else { + x.reply(serifs.REVERSI_DECLINE); + } + } + } + + public post = (param: any) => { + setTimeout(() => { + request.post(`${apiUrl}/notes/create`, { + json: Object.assign({ + i: config.i + }, param) + }); + }, 2000); + } + + public sendMessage = (userId: any, param: any) => { + setTimeout(() => { + request.post(`${apiUrl}/messaging/messages/create`, { + json: Object.assign({ + i: config.i, + userId: userId, + }, param) + }); + }, 2000); + } +} + +const ai = new 藍(); diff --git a/src/serifs.ts b/src/serifs.ts new file mode 100644 index 0000000..26bb0df --- /dev/null +++ b/src/serifs.ts @@ -0,0 +1,4 @@ +export default { + REVERSI_OK: '良いですよ~', + REVERSI_DECLINE: 'ごめんなさい、今リバーシはするなと言われてます...' +}; diff --git a/tsconfig.json b/tsconfig.json index b070af7..b498c4f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "noFallthroughCasesInSwitch": true, "experimentalDecorators": true, "sourceMap": false, - "target": "es2017", + "target": "es6", "module": "commonjs", "removeComments": false, "noLib": false,