diff --git a/package.json b/package.json index 9ba3120..8ec1124 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "_v": "1.3.0", + "_v": "1.4.0", "main": "./built/index.js", "scripts": { "start": "node ./built", diff --git a/src/ai.ts b/src/ai.ts index 07b192c..90cb9a1 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -18,11 +18,12 @@ import log from '@/utils/log'; const pkg = require('../package.json'); type MentionHook = (msg: Message) => Promise; -type ContextHook = (msg: Message, data?: any) => Promise; +type ContextHook = (key: any, msg: Message, data?: any) => Promise; type TimeoutCallback = (data?: any) => void; export type HandlerResult = { - reaction: string | null; + reaction?: string | null; + immediate?: boolean; }; export type InstallerResult = { @@ -220,18 +221,10 @@ export default class 藍 { }); let reaction: string | null = 'love'; + let immediate: boolean = false; //#region - // コンテキストがあればコンテキストフック呼び出し - // なければそれぞれのモジュールについてフックが引っかかるまで呼び出し - if (context != null) { - const handler = this.contextHooks[context.module]; - const res = await handler(msg, context.data); - - if (res != null && typeof res === 'object') { - reaction = res.reaction; - } - } else { + const invokeMentionHooks = async () => { let res: boolean | HandlerResult | null = null; for (const handler of this.mentionHooks) { @@ -240,12 +233,33 @@ export default class 藍 { } if (res != null && typeof res === 'object') { - reaction = res.reaction; + if (res.reaction != null) reaction = res.reaction; + if (res.immediate != null) immediate = res.immediate; } + }; + + // コンテキストがあればコンテキストフック呼び出し + // なければそれぞれのモジュールについてフックが引っかかるまで呼び出し + if (context != null) { + const handler = this.contextHooks[context.module]; + const res = await handler(context.key, msg, context.data); + + if (res != null && typeof res === 'object') { + if (res.reaction != null) reaction = res.reaction; + if (res.immediate != null) immediate = res.immediate; + } + + if (res === false) { + await invokeMentionHooks(); + } + } else { + await invokeMentionHooks(); } //#endregion - await delay(1000); + if (!immediate) { + await delay(1000); + } if (msg.isDm) { // 既読にする diff --git a/src/index.ts b/src/index.ts index 9f5510d..dca5703 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ import ChartModule from './modules/chart'; import SleepReportModule from './modules/sleep-report'; import NotingModule from './modules/noting'; import PollModule from './modules/poll'; +import ReminderModule from './modules/reminder'; console.log(' __ ____ _____ ___ '); console.log(' /__\\ (_ _)( _ )/ __)'); @@ -86,6 +87,7 @@ promiseRetry(retry => { new SleepReportModule(), new NotingModule(), new PollModule(), + new ReminderModule(), ]); }).catch(e => { log(chalk.red('Failed to fetch the account')); diff --git a/src/message.ts b/src/message.ts index a441e03..05c7f86 100644 --- a/src/message.ts +++ b/src/message.ts @@ -30,6 +30,13 @@ export default class Message { return this.messageOrNote.text; } + public get quoteId(): string | null { + return this.messageOrNote.renoteId; + } + + /** + * メンション部分を除いたテキスト本文 + */ public get extractedText(): string { const host = new URL(config.host).host.replace(/\./g, '\\.'); return this.text diff --git a/src/modules/core/index.ts b/src/modules/core/index.ts index f094c72..adb38af 100644 --- a/src/modules/core/index.ts +++ b/src/modules/core/index.ts @@ -141,12 +141,12 @@ export default class extends Module { } @autobind - private async contextHook(msg: Message, data: any) { + private async contextHook(key: any, msg: Message, data: any) { if (msg.text == null) return; const done = () => { msg.reply(serifs.core.setNameOk(msg.friend.name)); - this.unsubscribeReply(msg.userId); + this.unsubscribeReply(key); }; if (msg.text.includes('はい')) { diff --git a/src/modules/guessing-game/index.ts b/src/modules/guessing-game/index.ts index 6063f0d..d75acaf 100644 --- a/src/modules/guessing-game/index.ts +++ b/src/modules/guessing-game/index.ts @@ -66,7 +66,7 @@ export default class extends Module { } @autobind - private async contextHook(msg: Message) { + private async contextHook(key: any, msg: Message) { if (msg.text == null) return; const exist = this.guesses.findOne({ @@ -76,7 +76,7 @@ export default class extends Module { // 処理の流れ上、実際にnullになることは無さそうだけど一応 if (exist == null) { - this.unsubscribeReply(msg.userId); + this.unsubscribeReply(key); return; } @@ -85,7 +85,7 @@ export default class extends Module { exist.isEnded = true; exist.endedAt = Date.now(); this.guesses.update(exist); - this.unsubscribeReply(msg.userId); + this.unsubscribeReply(key); return; } @@ -124,7 +124,7 @@ export default class extends Module { if (end) { exist.isEnded = true; exist.endedAt = Date.now(); - this.unsubscribeReply(msg.userId); + this.unsubscribeReply(key); } this.guesses.update(exist); diff --git a/src/modules/kazutori/index.ts b/src/modules/kazutori/index.ts index c41504e..2825d8a 100644 --- a/src/modules/kazutori/index.ts +++ b/src/modules/kazutori/index.ts @@ -4,6 +4,7 @@ import Module from '@/module'; import Message from '@/message'; import serifs from '@/serifs'; import { User } from '@/misskey/user'; +import { acct } from '@/utils/acct'; type Game = { votes: { @@ -82,7 +83,7 @@ export default class extends Module { } @autobind - private async contextHook(msg: Message) { + private async contextHook(key: any, msg: Message) { if (msg.text == null) return { reaction: 'hmm' }; @@ -172,12 +173,6 @@ export default class extends Module { return; } - function acct(user: Game['votes'][0]['user']): string { - return user.host - ? `@${user.username}@${user.host}` - : `@${user.username}`; - } - let results: string[] = []; let winner: Game['votes'][0]['user'] | null = null; diff --git a/src/modules/reminder/index.ts b/src/modules/reminder/index.ts new file mode 100644 index 0000000..dc1cccb --- /dev/null +++ b/src/modules/reminder/index.ts @@ -0,0 +1,140 @@ +import autobind from 'autobind-decorator'; +import * as loki from 'lokijs'; +import Module from '@/module'; +import Message from '@/message'; +import serifs from '@/serifs'; +import { acct } from '@/utils/acct'; + +const NOTIFY_INTERVAL = 1000 * 60 * 60 * 12; + +export default class extends Module { + public readonly name = 'reminder'; + + private reminds: loki.Collection<{ + userId: string; + id: string; + isDm: boolean; + thing: string | null; + quoteId: string | null; + times: number; // 催促した回数(使うのか?) + createdAt: number; + }>; + + @autobind + public install() { + this.reminds = this.ai.getCollection('reminds', { + indices: ['userId', 'id'] + }); + + return { + mentionHook: this.mentionHook, + contextHook: this.contextHook, + timeoutCallback: this.timeoutCallback, + }; + } + + @autobind + private async mentionHook(msg: Message) { + let text = msg.extractedText.toLowerCase(); + if (!text.startsWith('remind') && !text.startsWith('todo')) return false; + if (text.match(/^(.+?)\s(.+)/)) { + text = text.replace(/^(.+?)\s/, ''); + } else { + text = ''; + } + + const separatorIndex = text.indexOf(' ') > -1 ? text.indexOf(' ') : text.indexOf('\n'); + const thing = text.substr(separatorIndex + 1).trim(); + + if (thing === '' && msg.quoteId == null) { + msg.reply(serifs.reminder.invalid); + return true; + } + + const remind = this.reminds.insertOne({ + id: msg.id, + userId: msg.userId, + isDm: msg.isDm, + thing: thing === '' ? null : thing, + quoteId: msg.quoteId, + times: 0, + createdAt: Date.now(), + }); + + this.subscribeReply(msg.id, msg.isDm, msg.isDm ? msg.userId : msg.id, { + id: remind!.id + }); + + // タイマーセット + this.setTimeoutWithPersistence(NOTIFY_INTERVAL, { + id: msg.id, + }); + + return { + reaction: '🆗', + immediate: true, + }; + } + + @autobind + private async contextHook(key: any, msg: Message, data: any) { + if (msg.text == null) return; + + const remind = this.reminds.findOne({ + id: data.id, + }); + + if (remind == null) { + this.unsubscribeReply(key); + return; + } + + const done = msg.includes(['done', 'やった']); + const cancel = msg.includes(['やめる']); + + if (done || cancel) { + this.unsubscribeReply(key); + this.reminds.remove(remind); + msg.reply(done ? serifs.reminder.done(msg.friend.name) : serifs.reminder.cancel); + return; + } else { + if (msg.isDm) this.unsubscribeReply(key); + return false; + } + } + + @autobind + private async timeoutCallback(data) { + const remind = this.reminds.findOne({ + id: data.id + }); + if (remind == null) return; + + remind.times++; + this.reminds.update(remind); + + const friend = this.ai.lookupFriend(remind.userId); + if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応 + + let reply; + if (remind.isDm) { + this.ai.sendMessage(friend.userId, { + text: serifs.reminder.notifyWithThing(remind.thing, friend.name) + }); + } else { + reply = await this.ai.post({ + renoteId: remind.thing == null && remind.quoteId ? remind.quoteId : remind.id, + text: acct(friend.doc.user) + ' ' + serifs.reminder.notify(friend.name) + }); + } + + this.subscribeReply(remind.id, remind.isDm, remind.isDm ? remind.userId : reply.id, { + id: remind.id + }); + + // タイマーセット + this.setTimeoutWithPersistence(NOTIFY_INTERVAL, { + id: remind.id, + }); + } +} diff --git a/src/serifs.ts b/src/serifs.ts index 28184ad..716da15 100644 --- a/src/serifs.ts +++ b/src/serifs.ts @@ -335,6 +335,21 @@ export default { notify: (time, name) => name ? `${name}、${time}経ちましたよ!` : `${time}経ちましたよ!` }, + /** + * リマインダー + */ + reminder: { + invalid: 'うーん...?', + + notify: (name) => name ? `${name}、これやりましたか?` : `これやりましたか?`, + + notifyWithThing: (thing, name) => name ? `${name}、「${thing}」やりましたか?` : `「${thing}」やりましたか?`, + + done: (name) => name ? `よく出来ました、${name}♪` : `よく出来ました♪`, + + cancel: `わかりました。`, + }, + /** * バレンタイン */ diff --git a/src/utils/acct.ts b/src/utils/acct.ts new file mode 100644 index 0000000..b83a6dc --- /dev/null +++ b/src/utils/acct.ts @@ -0,0 +1,5 @@ +export function acct(user: { username: string; host?: string | null; }): string { + return user.host + ? `@${user.username}@${user.host}` + : `@${user.username}`; +} diff --git a/src/utils/includes.ts b/src/utils/includes.ts index 694b404..4213f5d 100644 --- a/src/utils/includes.ts +++ b/src/utils/includes.ts @@ -3,8 +3,8 @@ import { katakanaToHiragana, hankakuToZenkaku } from './japanese'; export default function(text: string, words: string[]): boolean { if (text == null) return false; - text = katakanaToHiragana(hankakuToZenkaku(text)); - words = words.map(word => katakanaToHiragana(word)); + text = katakanaToHiragana(hankakuToZenkaku(text)).toLowerCase(); + words = words.map(word => katakanaToHiragana(word).toLowerCase()); return words.some(word => text.includes(word)); } diff --git a/torisetu.md b/torisetu.md index e09e06f..e090dfd 100644 --- a/torisetu.md +++ b/torisetu.md @@ -15,6 +15,13 @@ ### タイマー 指定した時間、分、秒を経過したら教えてくれます。「3分40秒」のように単位を混ぜることもできます。 +### リマインダー +``` +@ai remind 部屋の掃除 +``` +のようにメンションを飛ばすと12時間置きに催促されます。その飛ばしたメンションか、藍ちゃんからの催促に「やった」と返信することでリマインダー解除されます。 +また、引用Renoteでメンションすることもできます。 + ### 福笑い 藍に「絵文字」と言うと、藍が考えた絵文字の組み合わせを教えてくれます。