From c485a5b1ed720470df55d6936347af92df80c827 Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Fri, 29 Mar 2024 00:43:17 +0900 Subject: [PATCH] =?UTF-8?q?InstalledModule=E6=8A=BD=E8=B1=A1=E3=82=AF?= =?UTF-8?q?=E3=83=A9=E3=82=B9=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai.ts | 12 +- src/module.ts | 144 ++++++++++++++++++++--- src/modules/check-custom-emojis/index.ts | 22 ++-- src/modules/emoji-react/index.ts | 7 +- src/modules/guessing-game/index.ts | 47 ++++---- src/modules/kazutori/index.ts | 24 ++-- src/modules/keyword/index.ts | 21 ++-- src/modules/reminder/index.ts | 28 +++-- src/modules/server/index.ts | 22 ++-- 9 files changed, 239 insertions(+), 88 deletions(-) diff --git a/src/ai.ts b/src/ai.ts index b7720ee..3ab54cd 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -9,7 +9,7 @@ import chalk from 'chalk'; import { v4 as uuid } from 'uuid'; import config from '@/config.js'; -import Module from '@/module.js'; +import Module, { InstalledModule } from '@/module.js'; import Message from '@/message.js'; import Friend, { FriendDoc } from '@/friend.js'; import type { User } from '@/misskey/user.js'; @@ -38,6 +38,11 @@ export type Meta = { lastWakingAt: number; }; +export type ModuleDataDoc = { + module: string; + data: Data; +} + /** * 藍 */ @@ -46,7 +51,7 @@ export default interface 藍 extends Ai { lastSleepedAt: number; friends: loki.Collection; - moduleData: loki.Collection; + moduleData: loki.Collection; } /** @@ -60,6 +65,7 @@ export class Ai { private mentionHooks: MentionHook[] = []; private contextHooks: { [moduleName: string]: ContextHook } = {}; private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {}; + public installedModules: { [moduleName: string]: InstalledModule } = {}; public db: loki; public lastSleepedAt?: number; @@ -204,7 +210,7 @@ export class Ai { this.modules.forEach(m => { this.log(`Installing ${chalk.cyan.italic(m.name)}\tmodule...`); m.init(this); - const res = m.install(); + const res = m.install(this); if (res != null) { if (res.mentionHook) this.mentionHooks.push(res.mentionHook); if (res.contextHook) this.contextHooks[m.name] = res.contextHook; diff --git a/src/module.ts b/src/module.ts index 6d4770e..ce271f9 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,29 +1,26 @@ import { bindThis } from '@/decorators.js'; -import 藍, { InstallerResult } from '@/ai.js'; +import 藍, { HandlerResult, InstallerResult, ModuleDataDoc } from '@/ai.js'; +import Message from '@/message.js'; export default abstract class Module { public abstract readonly name: string; private maybeAi?: 藍; - private doc: any; + + /** + * @deprecated + */ + public installed?: InstalledModule; public init(ai: 藍) { this.maybeAi = ai; - - this.doc = this.ai.moduleData.findOne({ - module: this.name - }); - - if (this.doc == null) { - this.doc = this.ai.moduleData.insertOne({ - module: this.name, - data: {} - }); - } } - public abstract install(): InstallerResult; + public abstract install(ai: 藍): InstallerResult; + /** + * @deprecated {@link Module#install} の引数を使用すること + */ protected get ai(): 藍 { if (this.maybeAi == null) { throw new TypeError('This module has not been initialized'); @@ -31,6 +28,9 @@ export default abstract class Module { return this.maybeAi; } + /** + * @deprecated {@link InstalledModule#log} を使用すること + */ @bindThis protected log(msg: string) { this.ai.log(`[${this.name}]: ${msg}`); @@ -41,6 +41,7 @@ export default abstract class Module { * @param key コンテキストを識別するためのキー * @param id トークメッセージ上のコンテキストならばトーク相手のID、そうでないなら待ち受ける投稿のID * @param data コンテキストに保存するオプションのデータ + * @deprecated {@link InstalledModule#subscribeReply} を使用すること */ @bindThis protected subscribeReply(key: string | null, id: string, data?: any) { @@ -50,6 +51,7 @@ export default abstract class Module { /** * 返信の待ち受けを解除します * @param key コンテキストを識別するためのキー + * @deprecated {@link InstalledModule#unsubscribeReply} を使用すること */ @bindThis protected unsubscribeReply(key: string | null) { @@ -61,20 +63,132 @@ export default abstract class Module { * このタイマーは記憶に永続化されるので、途中でプロセスを再起動しても有効です。 * @param delay ミリ秒 * @param data オプションのデータ + * @deprecated {@link InstalledModule#setTimeoutWithPersistence} を使用すること */ @bindThis public setTimeoutWithPersistence(delay: number, data?: any) { this.ai.setTimeoutWithPersistence(this, delay, data); } + /** + * @deprecated {@link InstalledModule#getData} を使用すること + */ @bindThis protected getData() { + const doc = this.ai.moduleData.findOne({ + module: this.name + }); + return doc?.data; + } + + /** + * @deprecated {@link InstalledModule#setData} を使用すること + */ + @bindThis + protected setData(data: any) { + const doc = this.ai.moduleData.findOne({ + module: this.name + }); + if (doc == null) { + return; + } + doc.data = data; + this.ai.moduleData.update(doc); + if (this.installed != null) { + this.installed.updateDoc(); + } + } +} + +export abstract class InstalledModule implements InstallerResult { + protected readonly module: M; + + protected readonly ai: 藍; + + private doc: ModuleDataDoc; + + constructor(module: M, ai: 藍, initialData: any = {}) { + this.module = module; + this.ai = ai; + + const doc = this.ai.moduleData.findOne({ + module: module.name + }); + + if (doc == null) { + this.doc = this.ai.moduleData.insertOne({ + module: module.name, + data: initialData + }) as ModuleDataDoc; + } else { + this.doc = doc; + } + + ai.installedModules[module.name] = this; + } + + @bindThis + protected log(msg: string) { + this.ai.log(`[${this.module.name}]: ${msg}`); + } + + /** + * コンテキストを生成し、ユーザーからの返信を待ち受けます + * @param key コンテキストを識別するためのキー + * @param id トークメッセージ上のコンテキストならばトーク相手のID、そうでないなら待ち受ける投稿のID + * @param data コンテキストに保存するオプションのデータ + */ + @bindThis + protected subscribeReply(key: string | null, id: string, data?: any) { + this.ai.subscribeReply(this.module, key, id, data); + } + + /** + * 返信の待ち受けを解除します + * @param key コンテキストを識別するためのキー + */ + @bindThis + protected unsubscribeReply(key: string | null) { + this.ai.unsubscribeReply(this.module, key); + } + + /** + * 指定したミリ秒経過後に、タイムアウトコールバックを呼び出します。 + * このタイマーは記憶に永続化されるので、途中でプロセスを再起動しても有効です。 + * @param delay ミリ秒 + * @param data オプションのデータ + */ + @bindThis + public setTimeoutWithPersistence(delay: number, data?: any) { + this.ai.setTimeoutWithPersistence(this.module, delay, data); + } + + @bindThis + protected getData(): Data { return this.doc.data; } @bindThis - protected setData(data: any) { + protected setData(data: Data) { this.doc.data = data; this.ai.moduleData.update(this.doc); } + + /** + * @deprecated + */ + public updateDoc() { + const doc = this.ai.moduleData.findOne({ + module: this.module.name + }); + if (doc != null) { + this.doc = doc; + } + } + + mentionHook?(msg: Message): Promise; + + contextHook?(key: any, msg: Message, data?: any): Promise; + + timeoutCallback?(data?: any): void; } diff --git a/src/modules/check-custom-emojis/index.ts b/src/modules/check-custom-emojis/index.ts index 658a983..191a776 100644 --- a/src/modules/check-custom-emojis/index.ts +++ b/src/modules/check-custom-emojis/index.ts @@ -1,31 +1,35 @@ import { bindThis } from '@/decorators.js'; import loki from 'lokijs'; -import Module from '@/module.js'; +import Module, { InstalledModule } from '@/module.js'; import serifs from '@/serifs.js'; import config from '@/config.js'; import Message from '@/message.js'; +import 藍 from '@/ai.js'; export default class extends Module { public readonly name = 'checkCustomEmojis'; + @bindThis + public install(ai: 藍) { + if (!config.checkEmojisEnabled) return {}; + return new Installed(this, ai); + } +} + +class Installed extends InstalledModule { private lastEmoji: loki.Collection<{ id: string; updatedAt: number; }>; - @bindThis - public install() { - if (!config.checkEmojisEnabled) return {}; + constructor(module: Module, ai: 藍) { + super(module, ai); this.lastEmoji = this.ai.getCollection('lastEmoji', { indices: ['id'] }); this.timeCheck(); setInterval(this.timeCheck, 1000 * 60 * 3); - - return { - mentionHook: this.mentionHook - }; } @bindThis @@ -141,7 +145,7 @@ export default class extends Module { } @bindThis - private async mentionHook(msg: Message) { + public async mentionHook(msg: Message) { if (!msg.includes(['カスタムえもじチェック','カスタムえもじを調べて','カスタムえもじを確認'])) { return false; } else { diff --git a/src/modules/emoji-react/index.ts b/src/modules/emoji-react/index.ts index b131fa1..0a70a92 100644 --- a/src/modules/emoji-react/index.ts +++ b/src/modules/emoji-react/index.ts @@ -3,19 +3,16 @@ import { parse } from 'twemoji-parser'; import type { Note } from '@/misskey/note.js'; import Module from '@/module.js'; -import Stream from '@/stream.js'; import includes from '@/utils/includes.js'; import { sleep } from '@/utils/sleep.js'; export default class extends Module { public readonly name = 'emoji-react'; - private htl: ReturnType; - @bindThis public install() { - this.htl = this.ai.connection.useSharedConnection('homeTimeline'); - this.htl.on('note', this.onNote); + const htl = this.ai.connection.useSharedConnection('homeTimeline'); + htl.on('note', this.onNote); return {}; } diff --git a/src/modules/guessing-game/index.ts b/src/modules/guessing-game/index.ts index 93ad457..5f4229e 100644 --- a/src/modules/guessing-game/index.ts +++ b/src/modules/guessing-game/index.ts @@ -1,35 +1,42 @@ import { bindThis } from '@/decorators.js'; import loki from 'lokijs'; -import Module from '@/module.js'; +import Module, { InstalledModule } from '@/module.js'; import Message from '@/message.js'; import serifs from '@/serifs.js'; +import 藍, { InstallerResult } from '@/ai.js'; + +type Guesses = loki.Collection<{ + userId: string; + secret: number; + tries: number[]; + isEnded: boolean; + startedAt: number; + endedAt: number | null; +}> export default class extends Module { public readonly name = 'guessingGame'; - private guesses: loki.Collection<{ - userId: string; - secret: number; - tries: number[]; - isEnded: boolean; - startedAt: number; - endedAt: number | null; - }>; - @bindThis - public install() { - this.guesses = this.ai.getCollection('guessingGame', { + public install(ai: 藍) { + const guesses = ai.getCollection('guessingGame', { indices: ['userId'] }); - return { - mentionHook: this.mentionHook, - contextHook: this.contextHook - }; + return new Installed(this, ai, guesses); + } +} + +class Installed extends InstalledModule implements InstallerResult { + private guesses: Guesses; + + constructor(module: Module, ai: 藍, guesses: Guesses) { + super(module, ai); + this.guesses = guesses; } @bindThis - private async mentionHook(msg: Message) { + public async mentionHook(msg: Message) { if (!msg.includes(['数当て', '数あて'])) return false; const exist = this.guesses.findOne({ @@ -56,7 +63,7 @@ export default class extends Module { } @bindThis - private async contextHook(key: any, msg: Message) { + public async contextHook(key: any, msg: Message) { if (msg.text == null) return; const exist = this.guesses.findOne({ @@ -114,14 +121,14 @@ export default class extends Module { if (end) { exist.isEnded = true; exist.endedAt = Date.now(); - this.unsubscribeReply(key); + this.ai.unsubscribeReply(this.module, key); } this.guesses.update(exist); msg.reply(text).then(reply => { if (!end) { - this.subscribeReply(msg.userId, reply.id); + this.ai.subscribeReply(this.module, msg.userId, reply.id); } }); } diff --git a/src/modules/kazutori/index.ts b/src/modules/kazutori/index.ts index 0df52a2..4a3eb43 100644 --- a/src/modules/kazutori/index.ts +++ b/src/modules/kazutori/index.ts @@ -1,10 +1,11 @@ import { bindThis } from '@/decorators.js'; import loki from 'lokijs'; -import Module from '@/module.js'; +import Module, { InstalledModule } from '@/module.js'; import Message from '@/message.js'; import serifs from '@/serifs.js'; import type { User } from '@/misskey/user.js'; import { acct } from '@/utils/acct.js'; +import 藍, { InstallerResult } from '@/ai.js'; type Game = { votes: { @@ -25,23 +26,28 @@ const limitMinutes = 10; export default class extends Module { public readonly name = 'kazutori'; + @bindThis + public install(ai: 藍) { + return new Installed(this, ai); + } +} + +class Installed extends InstalledModule { + private games: loki.Collection; - @bindThis - public install() { + constructor(module: Module, ai: 藍) { + super(module, ai); this.games = this.ai.getCollection('kazutori'); this.crawleGameEnd(); setInterval(this.crawleGameEnd, 1000); - return { - mentionHook: this.mentionHook, - contextHook: this.contextHook - }; + return this; } @bindThis - private async mentionHook(msg: Message) { + public async mentionHook(msg: Message) { if (!msg.includes(['数取り'])) return false; const games = this.games.find({}); @@ -83,7 +89,7 @@ export default class extends Module { } @bindThis - private async contextHook(key: any, msg: Message) { + public async contextHook(key: any, msg: Message) { if (msg.text == null) return { reaction: 'hmm' }; diff --git a/src/modules/keyword/index.ts b/src/modules/keyword/index.ts index a1c7ad9..3b6d42c 100644 --- a/src/modules/keyword/index.ts +++ b/src/modules/keyword/index.ts @@ -1,9 +1,10 @@ import { bindThis } from '@/decorators.js'; import loki from 'lokijs'; -import Module from '@/module.js'; +import Module, { InstalledModule } from '@/module.js'; import config from '@/config.js'; import serifs from '@/serifs.js'; import { mecab } from './mecab.js'; +import 藍 from '@/ai.js'; type LocalTimeline = { userId: string; @@ -21,22 +22,28 @@ function kanaToHira(str: string) { export default class extends Module { public readonly name = 'keyword'; + @bindThis + public install(ai: 藍) { + if (config.keywordEnabled) { + new Installed(this, ai); + } + return {}; + } +} + +class Installed extends InstalledModule { private learnedKeywords: loki.Collection<{ keyword: string; learnedAt: number; }>; - @bindThis - public install() { - if (!config.keywordEnabled) return {}; - + constructor(module: Module, ai: 藍) { + super(module, ai); this.learnedKeywords = this.ai.getCollection('_keyword_learnedKeywords', { indices: ['userId'] }); setInterval(this.learn, 1000 * 60 * 60); - - return {}; } @bindThis diff --git a/src/modules/reminder/index.ts b/src/modules/reminder/index.ts index 0842edc..f437a0b 100644 --- a/src/modules/reminder/index.ts +++ b/src/modules/reminder/index.ts @@ -1,16 +1,24 @@ import { bindThis } from '@/decorators.js'; import loki from 'lokijs'; -import Module from '@/module.js'; +import Module, { InstalledModule } from '@/module.js'; import Message from '@/message.js'; import serifs, { getSerif } from '@/serifs.js'; import { acct } from '@/utils/acct.js'; import config from '@/config.js'; +import 藍 from '@/ai.js'; const NOTIFY_INTERVAL = 1000 * 60 * 60 * 12; export default class extends Module { public readonly name = 'reminder'; + @bindThis + public install(ai: 藍) { + return new Installed(this, ai); + } +} + +class Installed extends InstalledModule { private reminds: loki.Collection<{ userId: string; id: string; @@ -20,21 +28,15 @@ export default class extends Module { createdAt: number; }>; - @bindThis - public install() { + constructor(module: Module, ai: 藍) { + super(module, ai); this.reminds = this.ai.getCollection('reminds', { indices: ['userId', 'id'] }); - - return { - mentionHook: this.mentionHook, - contextHook: this.contextHook, - timeoutCallback: this.timeoutCallback, - }; } @bindThis - private async mentionHook(msg: Message) { + public async mentionHook(msg: Message) { let text = msg.extractedText.toLowerCase(); if (!text.startsWith('remind') && !text.startsWith('todo')) return false; @@ -99,7 +101,7 @@ export default class extends Module { } @bindThis - private async contextHook(key: any, msg: Message, data: any) { + public async contextHook(key: any, msg: Message, data: any) { if (msg.text == null) return; const remind = this.reminds.findOne({ @@ -129,7 +131,7 @@ export default class extends Module { } @bindThis - private async timeoutCallback(data) { + public async timeoutCallback(data) { const remind = this.reminds.findOne({ id: data.id }); @@ -147,7 +149,7 @@ export default class extends Module { renoteId: remind.thing == null && remind.quoteId ? remind.quoteId : remind.id, text: acct(friend.doc.user) + ' ' + serifs.reminder.notify(friend.name) }); - } catch (err) { + } catch (err: any) { // renote対象が消されていたらリマインダー解除 if (err.statusCode === 400) { this.unsubscribeReply(remind.thing == null && remind.quoteId ? remind.quoteId : remind.id); diff --git a/src/modules/server/index.ts b/src/modules/server/index.ts index 17883df..6b76f3e 100644 --- a/src/modules/server/index.ts +++ b/src/modules/server/index.ts @@ -1,24 +1,34 @@ import { bindThis } from '@/decorators.js'; -import Module from '@/module.js'; +import Module, { InstalledModule } from '@/module.js'; import serifs from '@/serifs.js'; import config from '@/config.js'; +import 藍 from '@/ai.js'; export default class extends Module { public readonly name = 'server'; + @bindThis + public install(ai: 藍) { + if (config.serverMonitoring) { + new Installed(this, ai); + } + return {}; + } +} + +class Installed extends InstalledModule { private connection?: any; private recentStat: any; private warned = false; - private lastWarnedAt: number; + private lastWarnedAt?: number; /** * 1秒毎のログ1分間分 */ private statsLogs: any[] = []; - @bindThis - public install() { - if (!config.serverMonitoring) return {}; + constructor(module: Module, ai: 藍) { + super(module, ai); this.connection = this.ai.connection.useSharedConnection('serverStats'); this.connection.on('stats', this.onStats); @@ -31,8 +41,6 @@ export default class extends Module { setInterval(() => { this.check(); }, 3000); - - return {}; } @bindThis