diff --git a/package-lock.json b/package-lock.json index eb6a8ed..0f6cee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,11 @@ "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" }, + "@types/lokijs": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/lokijs/-/lokijs-1.5.2.tgz", + "integrity": "sha512-ZF14v1P1Bjbw8VJRu+p4WS9V926CAOjWF4yq23QmSBWRPe0/GXlUKzSxjP1fi/xi8nrq6zr9ECo8Z/8KsRqroQ==" + }, "@types/node": { "version": "10.0.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.0.5.tgz", @@ -314,6 +319,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, + "lokijs": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/lokijs/-/lokijs-1.5.5.tgz", + "integrity": "sha1-HCH4KvdXkDf63nueSBNIXCNwi7Y=" + }, "make-error": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", @@ -514,6 +524,11 @@ "has-flag": "^3.0.0" } }, + "timeout-as-promise": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/timeout-as-promise/-/timeout-as-promise-1.0.0.tgz", + "integrity": "sha1-c2foEfyZKs/Nzaq/LlDfr4shV28=" + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", diff --git a/package.json b/package.json index 1529cdc..7ca8d9a 100644 --- a/package.json +++ b/package.json @@ -5,16 +5,19 @@ "build": "tsc" }, "dependencies": { + "@types/lokijs": "1.5.2", "@types/node": "10.0.5", "@types/promise-retry": "1.1.2", "@types/seedrandom": "2.4.27", "@types/ws": "5.1.2", + "lokijs": "1.5.5", "misskey-reversi": "0.0.5", "promise-retry": "1.1.1", "reconnecting-websocket": "4.0.0-rc5", "request": "2.87.0", "request-promise-native": "1.0.5", "seedrandom": "2.4.3", + "timeout-as-promise": "1.0.0", "ts-node": "6.0.3", "typescript": "2.8.3", "ws": "6.0.0" diff --git a/src/ai.ts b/src/ai.ts index ab907a3..b1d4839 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -6,13 +6,14 @@ import serifs from './serifs'; import config from './config'; import IModule from './module'; import MessageLike from './message-like'; +import { contexts } from './memory'; const ReconnectingWebSocket = require('../node_modules/reconnecting-websocket/dist/reconnecting-websocket-cjs.js'); /** * 藍 */ export default class 藍 { - private account: any; + public account: any; /** * ホームストリーム @@ -99,17 +100,31 @@ export default class 藍 { } }, 1000); - this.modules.filter(m => m.hasOwnProperty('onMention')).some(m => { - return m.onMention(msg); + const context = !msg.isMessage && msg.replyId == null ? null : contexts.findOne(msg.isMessage ? { + isMessage: true, + userId: msg.userId + } : { + isMessage: false, + noteId: msg.replyId }); + + if (context != null) { + const module = this.modules.find(m => m.name == context.module); + module.onReplyThisModule(msg); + } else { + this.modules.filter(m => m.hasOwnProperty('onMention')).some(m => { + return m.onMention(msg); + }); + } } - public post = (param: any) => { - this.api('notes/create', param); + public post = async (param: any) => { + const res = await this.api('notes/create', param); + return res.createdNote; } public sendMessage = (userId: any, param: any) => { - this.api('messaging/messages/create', Object.assign({ + return this.api('messaging/messages/create', Object.assign({ userId: userId, }, param)); } @@ -121,4 +136,25 @@ export default class 藍 { }, param) }); }; + + public subscribeReply = (module: IModule, key: string, isMessage: boolean, id: string) => { + contexts.insertOne(isMessage ? { + isMessage: true, + userId: id, + module: module.name, + key: key, + } : { + isMessage: false, + noteId: id, + module: module.name, + key: key, + }); + } + + public unsubscribeReply = (module: IModule, key: string) => { + contexts.findAndRemove({ + key: key, + module: module.name + }); + } } diff --git a/src/index.ts b/src/index.ts index 7676bc1..58083ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import ServerModule from './modules/server'; import PingModule from './modules/ping'; import EmojiModule from './modules/emoji'; import FortuneModule from './modules/fortune'; +import GuessingGameModule from './modules/guessing-game'; import * as request from 'request-promise-native'; const promiseRetry = require('promise-retry'); @@ -20,6 +21,7 @@ promiseRetry(retry => { ai.install(new PingModule()); ai.install(new EmojiModule()); ai.install(new FortuneModule()); + ai.install(new GuessingGameModule()); ai.install(new ServerModule()); ai.install(new ReversiModule()); }); diff --git a/src/memory.ts b/src/memory.ts new file mode 100644 index 0000000..0b748b0 --- /dev/null +++ b/src/memory.ts @@ -0,0 +1,17 @@ +// 藍の記憶 + +import * as loki from 'lokijs'; + +const db = new loki('ai'); + +export default db; + +export const contexts = db.addCollection<{ + isMessage: boolean; + noteId?: string; + userId?: string; + module: string; + key: string; +}>('contexts', { + indices: ['key'] +}); diff --git a/src/message-like.ts b/src/message-like.ts index b1d68bc..4d1a6b6 100644 --- a/src/message-like.ts +++ b/src/message-like.ts @@ -1,4 +1,5 @@ import 藍 from './ai'; +const delay = require('timeout-as-promise'); export default class MessageLike { private ai: 藍; @@ -21,27 +22,31 @@ export default class MessageLike { return this.messageOrNote.text; } + public get replyId() { + return this.messageOrNote.replyId; + } + constructor(ai: 藍, messageOrNote: any, isMessage: boolean) { this.ai = ai; this.messageOrNote = messageOrNote; this.isMessage = isMessage; } - public reply = (text: string, cw?: string) => { + public reply = async (text: string, cw?: string) => { console.log(`sending reply of ${this.id} ...`); - setTimeout(() => { - if (this.isMessage) { - this.ai.sendMessage(this.messageOrNote.userId, { - text: text - }); - } else { - this.ai.post({ - replyId: this.messageOrNote.id, - text: text, - cw: cw - }); - } - }, 2000); + await delay(2000); + + if (this.isMessage) { + return await this.ai.sendMessage(this.messageOrNote.userId, { + text: text + }); + } else { + return await this.ai.post({ + replyId: this.messageOrNote.id, + text: text, + cw: cw + }); + } } } diff --git a/src/module.ts b/src/module.ts index 2a88636..fd0702e 100644 --- a/src/module.ts +++ b/src/module.ts @@ -2,6 +2,8 @@ import 藍 from './ai'; import MessageLike from './message-like'; export default interface IModule { + name: string; install?: (ai: 藍) => void; onMention?: (msg: MessageLike) => boolean; + onReplyThisModule?: (msg: MessageLike) => void; } diff --git a/src/modules/emoji/index.ts b/src/modules/emoji/index.ts index 7ea2fa9..3666257 100644 --- a/src/modules/emoji/index.ts +++ b/src/modules/emoji/index.ts @@ -119,10 +119,12 @@ const faces = [ ] export default class EmojiModule implements IModule { + public name = 'emoji'; + public install = (ai: 藍) => { } public onMention = (msg: MessageLike) => { - if (msg.text && msg.text.includes('絵文字')) { + if (msg.text && (msg.text.includes('絵文字') || msg.text.includes('emoji'))) { const hand = hands[Math.floor(Math.random() * hands.length)]; const face = faces[Math.floor(Math.random() * faces.length)]; const emoji = Array.isArray(hand) ? hand[0] + face + hand[1] : hand + face + hand; diff --git a/src/modules/fortune/index.ts b/src/modules/fortune/index.ts index 626719b..12fd3c9 100644 --- a/src/modules/fortune/index.ts +++ b/src/modules/fortune/index.ts @@ -24,7 +24,9 @@ const items = [ '寿司' ]; -export default class EmojiModule implements IModule { +export default class FortuneModule implements IModule { + public name = 'fortune'; + public install = (ai: 藍) => { } public onMention = (msg: MessageLike) => { diff --git a/src/modules/guessing-game/index.ts b/src/modules/guessing-game/index.ts new file mode 100644 index 0000000..5d96c72 --- /dev/null +++ b/src/modules/guessing-game/index.ts @@ -0,0 +1,118 @@ +import 藍 from '../../ai'; +import IModule from '../../module'; +import MessageLike from '../../message-like'; +import serifs from '../../serifs'; +import db from '../../memory'; + +export const guesses = db.addCollection<{ + userId: string; + secret: number; + tries: number[]; + isEnded: boolean; + startedAt: number; + endedAt: number; +}>('guessingGame', { + indices: ['userId'] +}); + +export default class GuessingGameModule implements IModule { + public name = 'guessingGame'; + private ai: 藍; + + public install = (ai: 藍) => { + this.ai = ai; + } + + public onMention = (msg: MessageLike) => { + if (msg.text && msg.text.includes('数当て')) { + const exist = guesses.findOne({ + userId: msg.userId, + isEnded: false + }); + + if (exist != null) { + msg.reply(serifs.GUESSINGGAME_ARLEADY_STARTED); + } else { + const secret = Math.floor(Math.random() * 100); + + guesses.insertOne({ + userId: msg.userId, + secret: secret, + tries: [], + isEnded: false, + startedAt: Date.now(), + endedAt: null + }); + + msg.reply(serifs.GUESSINGGAME_STARTED).then(reply => { + this.ai.subscribeReply(this, msg.userId, msg.isMessage, msg.isMessage ? msg.userId : reply.id); + }); + } + return true; + } else { + return false; + } + } + + public onReplyThisModule = (msg: MessageLike) => { + if (msg.text == null) return; + + const exist = guesses.findOne({ + userId: msg.userId, + isEnded: false + }); + + if (msg.text.includes('やめ')) { + msg.reply(serifs.GUESSINGGAME_CANCEL); + exist.isEnded = true; + exist.endedAt = Date.now(); + guesses.update(exist); + this.ai.unsubscribeReply(this, msg.userId); + return; + } + + const guess = msg.text.toLowerCase().replace(this.ai.account.username.toLowerCase(), '').match(/[0-9]+/); + + if (guess == null) { + msg.reply(serifs.GUESSINGGAME_NAN).then(reply => { + this.ai.subscribeReply(this, msg.userId, msg.isMessage, reply.id); + }); + } else { + const g = parseInt(guess, 10); + + const firsttime = exist.tries.indexOf(g) === -1; + + let text: string; + let end = false; + + if (exist.secret < g) { + text = firsttime + ? serifs.GUESSINGGAME_LESS.replace('$', g.toString()) + : serifs.GUESSINGGAME_LESS_AGAIN.replace('$', g.toString()); + } else if (exist.secret > g) { + text = firsttime + ? serifs.GUESSINGGAME_GRATER.replace('$', g.toString()) + : serifs.GUESSINGGAME_GRATER_AGAIN.replace('$', g.toString()); + } else { + end = true; + text = serifs.GUESSINGGAME_CONGRATS.replace('{tries}', exist.tries.length.toString()); + } + + if (end) { + exist.isEnded = true; + exist.endedAt = Date.now(); + guesses.update(exist); + this.ai.unsubscribeReply(this, msg.userId); + } else { + exist.tries.push(g); + guesses.update(exist); + } + + msg.reply(text).then(reply => { + if (!end) { + this.ai.subscribeReply(this, msg.userId, msg.isMessage, reply.id); + } + }); + } + } +} diff --git a/src/modules/ping/index.ts b/src/modules/ping/index.ts index 3fac725..81ba0ad 100644 --- a/src/modules/ping/index.ts +++ b/src/modules/ping/index.ts @@ -3,6 +3,8 @@ import IModule from '../../module'; import MessageLike from '../../message-like'; export default class PingModule implements IModule { + public name = 'ping'; + public install = (ai: 藍) => { } public onMention = (msg: MessageLike) => { diff --git a/src/modules/reversi/index.ts b/src/modules/reversi/index.ts index 882b84b..a41a8ee 100644 --- a/src/modules/reversi/index.ts +++ b/src/modules/reversi/index.ts @@ -8,6 +8,8 @@ import MessageLike from '../../message-like'; import * as WebSocket from 'ws'; export default class ReversiModule implements IModule { + public name = 'reversi'; + private ai: 藍; /** @@ -38,7 +40,7 @@ export default class ReversiModule implements IModule { } public onMention = (msg: MessageLike) => { - if (msg.text && msg.text.includes('リバーシ')) { + if (msg.text && (msg.text.includes('リバーシ') || msg.text.toLowerCase().includes('reversi'))) { if (config.reversiEnabled) { msg.reply(serifs.REVERSI_OK); diff --git a/src/modules/server/index.ts b/src/modules/server/index.ts index e4686ab..8befc95 100644 --- a/src/modules/server/index.ts +++ b/src/modules/server/index.ts @@ -8,6 +8,8 @@ import MessageLike from '../../message-like'; const ReconnectingWebSocket = require('../../../node_modules/reconnecting-websocket/dist/reconnecting-websocket-cjs.js'); export default class ServerModule implements IModule { + public name = 'server'; + private ai: 藍; private connection?: any; private preventScheduleReboot = false; diff --git a/src/serifs.ts b/src/serifs.ts index bf383f4..fb88097 100644 --- a/src/serifs.ts +++ b/src/serifs.ts @@ -35,5 +35,50 @@ export default { */ EMOJI_SUGGEST: 'こんなのはどうですか?→$', - FORTUNE_CW: '私が今日のあなたの運勢を占いました...' + FORTUNE_CW: '私が今日のあなたの運勢を占いました...', + + /** + * 数当てゲームをやろうと言われたけど既にやっているとき + */ + GUESSINGGAME_ARLEADY_STARTED: 'え、ゲームは既に始まってますよ!', + + /** + * 数当てゲーム開始 + */ + GUESSINGGAME_STARTED: '0~100の秘密の数を当ててみてください♪', + + /** + * 数当てゲームで数字じゃない返信があったとき + */ + GUESSINGGAME_NAN: '数字でお願いします!「やめる」と言ってゲームをやめることもできますよ!', + + /** + * 数当てゲーム中止を要求されたとき + */ + GUESSINGGAME_CANCEL: 'わかりました~。ありがとうございました♪', + + /** + * 数当てゲームで小さい数を言われたとき + */ + GUESSINGGAME_GRATER: '$より大きいですね', + + /** + * 数当てゲームで小さい数を言われたとき(2度目) + */ + GUESSINGGAME_GRATER_AGAIN: 'もう一度言いますが$より大きいですよ!', + + /** + * 数当てゲームで大きい数を言われたとき + */ + GUESSINGGAME_LESS: '$より小さいですね', + + /** + * 数当てゲームで大きい数を言われたとき(2度目) + */ + GUESSINGGAME_LESS_AGAIN: 'もう一度言いますが$より小さいですよ!', + + /** + * 数当てゲームで正解したとき + */ + GUESSINGGAME_CONGRATS: '正解です🎉 ({tries}回目で当てました)', };