diff --git a/package-lock.json b/package-lock.json index dfde776..82c486c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,14 @@ "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz", "integrity": "sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE=" }, + "@types/uuid": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz", + "integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==", + "requires": { + "@types/node": "*" + } + }, "@types/ws": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz", diff --git a/package.json b/package.json index ef9b401..8169bd8 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@types/node": "10.0.5", "@types/promise-retry": "1.1.2", "@types/seedrandom": "2.4.27", + "@types/uuid": "3.4.4", "@types/ws": "6.0.1", "autobind-decorator": "2.1.0", "chalk": "2.4.2", @@ -25,6 +26,7 @@ "timeout-as-promise": "1.0.0", "ts-node": "6.0.3", "typescript": "2.8.3", + "uuid": "3.3.2", "ws": "6.0.0" } } diff --git a/src/ai.ts b/src/ai.ts index 618c962..5fccd4a 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -4,18 +4,20 @@ import autobind from 'autobind-decorator'; import * as loki from 'lokijs'; import * as request from 'request-promise-native'; import chalk from 'chalk'; +import * as uuid from 'uuid/v4'; const delay = require('timeout-as-promise'); import config from './config'; import Module from './module'; import Message from './message'; -import { FriendDoc } from './friend'; +import Friend, { FriendDoc } from './friend'; import { User } from './misskey/user'; import Stream from './stream'; import log from './utils/log'; type MentionHook = (msg: Message) => Promise; type ContextHook = (msg: Message, data?: any) => Promise; +type TimeoutCallback = (data?: any) => void; export type HandlerResult = { reaction: string; @@ -24,6 +26,7 @@ export type HandlerResult = { export type InstallerResult = { mentionHook?: MentionHook; contextHook?: ContextHook; + timeoutCallback?: TimeoutCallback; }; /** @@ -35,6 +38,7 @@ export default class 藍 { public modules: Module[] = []; private mentionHooks: MentionHook[] = []; private contextHooks: { [moduleName: string]: ContextHook } = {}; + private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {}; public db: loki; private contexts: loki.Collection<{ @@ -46,6 +50,14 @@ export default class 藍 { data?: any; }>; + private timers: loki.Collection<{ + id: string; + module: string; + insertedAt: number; + delay: number; + data?: any; + }>; + public friends: loki.Collection; /** @@ -86,6 +98,10 @@ export default class 藍 { indices: ['key'] }); + this.timers = this.getCollection('timers', { + indices: ['module'] + }); + this.friends = this.getCollection('friends', { indices: ['userId'] }); @@ -143,9 +159,14 @@ export default class 藍 { if (res != null) { if (res.mentionHook) this.mentionHooks.push(res.mentionHook); if (res.contextHook) this.contextHooks[m.name] = res.contextHook; + if (res.timeoutCallback) this.timeoutCallbacks[m.name] = res.timeoutCallback; } }); + // タイマー監視 + this.crawleTimer(); + setInterval(this.crawleTimer, 1000); + this.log(chalk.green.bold('Ai am now running!')); } @@ -218,6 +239,19 @@ export default class 藍 { } } + @autobind + private crawleTimer() { + const timers = this.timers.find(); + for (const timer of timers) { + // タイマーが時間切れかどうか + if (Date.now() - (timer.insertedAt + timer.delay) >= 0) { + this.log(`Timer expired: ${timer.module} ${timer.id}`); + this.timers.remove(timer); + this.timeoutCallbacks[timer.module](timer.data); + } + } + } + /** * データベースのコレクションを取得します */ @@ -234,6 +268,19 @@ export default class 藍 { return collection; } + @autobind + public lookupFriend(userId: User['id']): Friend { + const doc = this.friends.findOne({ + userId: userId + }); + + if (doc == null) return null; + + const friend = new Friend(this, { doc: doc }); + + return friend; + } + /** * 投稿します */ @@ -302,4 +349,25 @@ export default class 藍 { module: module.name }); } + + /** + * 指定したミリ秒経過後に、そのモジュールのタイムアウトコールバックを呼び出します。 + * このタイマーは記憶に永続化されるので、途中でプロセスを再起動しても有効です。 + * @param module モジュール名 + * @param delay ミリ秒 + * @param data オプションのデータ + */ + @autobind + public setTimeoutWithPersistence(module: Module, delay: number, data?: any) { + const id = uuid(); + this.timers.insertOne({ + id: id, + module: module.name, + insertedAt: Date.now(), + delay: delay, + data: data + }); + + this.log(`Timer persisted: ${module.name} ${id} ${delay}ms`); + } } diff --git a/src/friend.ts b/src/friend.ts index ebad8e0..70c48d9 100644 --- a/src/friend.ts +++ b/src/friend.ts @@ -12,6 +12,7 @@ export type FriendDoc = { lastLoveIncrementedAt?: string; todayLoveIncrements?: number; perModulesData?: any; + married?: boolean; }; export default class Friend { @@ -29,6 +30,10 @@ export default class Friend { return this.doc.love || 0; } + public get married() { + return this.doc.married; + } + public doc: FriendDoc; constructor(ai: 藍, opts: { user?: User, doc?: FriendDoc }) { diff --git a/src/module.ts b/src/module.ts index 331861d..5907e6f 100644 --- a/src/module.ts +++ b/src/module.ts @@ -37,4 +37,15 @@ export default abstract class Module { protected unsubscribeReply(key: string) { this.ai.unsubscribeReply(this, key); } + + /** + * 指定したミリ秒経過後に、タイムアウトコールバックを呼び出します。 + * このタイマーは記憶に永続化されるので、途中でプロセスを再起動しても有効です。 + * @param delay ミリ秒 + * @param data オプションのデータ + */ + @autobind + public setTimeoutWithPersistence(delay: number, data?: any) { + this.ai.setTimeoutWithPersistence(this, delay, data); + } } diff --git a/src/modules/timer/index.ts b/src/modules/timer/index.ts index c2db74b..680c93c 100644 --- a/src/modules/timer/index.ts +++ b/src/modules/timer/index.ts @@ -9,7 +9,8 @@ export default class extends Module { @autobind public install() { return { - mentionHook: this.mentionHook + mentionHook: this.mentionHook, + timeoutCallback: this.timeoutCallback, }; } @@ -23,34 +24,51 @@ export default class extends Module { const minutes = minutesQuery ? parseInt(minutesQuery[1], 10) : 0; const hours = hoursQuery ? parseInt(hoursQuery[1], 10) : 0; - if (secondsQuery || minutesQuery || hoursQuery) { - if ((seconds + minutes + hours) == 0) { - msg.reply(serifs.timer.invalid); - return true; - } - - const time = - (1000 * seconds) + - (1000 * 60 * minutes) + - (1000 * 60 * 60 * hours); - - if (time > 86400000) { - msg.reply(serifs.timer.tooLong); - return true; - } - - msg.reply(serifs.timer.set); - - const str = `${hours ? hoursQuery[0] : ''}${minutes ? minutesQuery[0] : ''}${seconds ? secondsQuery[0] : ''}`; - - setTimeout(() => { - const name = msg.friend.name; - msg.reply(serifs.timer.notify(str, name)); - }, time); + if (!(secondsQuery || minutesQuery || hoursQuery)) return false; + if ((seconds + minutes + hours) == 0) { + msg.reply(serifs.timer.invalid); return true; + } + + const time = + (1000 * seconds) + + (1000 * 60 * minutes) + + (1000 * 60 * 60 * hours); + + if (time > 86400000) { + msg.reply(serifs.timer.tooLong); + return true; + } + + msg.reply(serifs.timer.set); + + const str = `${hours ? hoursQuery[0] : ''}${minutes ? minutesQuery[0] : ''}${seconds ? secondsQuery[0] : ''}`; + + // タイマーセット + this.setTimeoutWithPersistence(time, { + isDm: msg.isDm, + msgId: msg.id, + userId: msg.friend.userId, + time: str + }); + + return true; + } + + @autobind + private timeoutCallback(data) { + const friend = this.ai.lookupFriend(data.userId); + const text = serifs.timer.notify(data.time, friend.name); + if (data.isDm) { + this.ai.sendMessage(friend.userId, { + text: text + }); } else { - return false; + this.ai.post({ + replyId: data.msgId, + text: text + }); } } }