From f893fd3a3fdc516710ba9973fd6a97d3b6408efe Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Sun, 24 Mar 2024 19:07:03 +0900 Subject: [PATCH 01/14] =?UTF-8?q?config.ts=E3=81=AE=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=81=AB=E5=AF=BE=E5=87=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index 9466b0c..aac43af 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,9 +17,16 @@ type Config = { memoryDir?: string; }; -import config from '../config.json' assert { type: 'json' }; +import uncheckedConfig from '../config.json' assert { type: 'json' }; + +function validate(config: unknown): Config { + // TODO: as を使わずにしっかりと検証を行う + return config as Config; +} + +const config = validate(uncheckedConfig); config.wsUrl = config.host.replace('http', 'ws'); config.apiUrl = config.host + '/api'; -export default config as Config; +export default config; From a2ebc9401c67a31e07a29d402392046971b91071 Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:09:14 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=E8=97=8D#api=E3=81=AE=E5=9E=8B=E3=83=91?= =?UTF-8?q?=E3=83=A9=E3=83=A1=E3=83=BC=E3=82=BF=E3=81=ABReturnType?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai.ts | 7 ++++--- src/index.ts | 3 ++- src/message.ts | 2 +- src/modules/chart/index.ts | 27 ++++++++++++++++++++++++--- src/modules/keyword/index.ts | 11 +++++++++-- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/ai.ts b/src/ai.ts index 701b97e..e436533 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -17,6 +17,7 @@ import Stream from '@/stream.js'; import log from '@/utils/log.js'; import { sleep } from './utils/sleep.js'; import pkg from '../package.json' assert { type: 'json' }; +import { Note } from './misskey/note.js'; type MentionHook = (msg: Message) => Promise; type ContextHook = (key: any, msg: Message, data?: any) => Promise; @@ -361,7 +362,7 @@ export default class 藍 { */ @bindThis public async post(param: any) { - const res = await this.api('notes/create', param); + const res = await this.api<{ createdNote: Note }>('notes/create', param); return res.createdNote; } @@ -380,13 +381,13 @@ export default class 藍 { * APIを呼び出します */ @bindThis - public api(endpoint: string, param?: any) { + public api(endpoint: string, param?: any) { this.log(`API: ${endpoint}`); return got.post(`${config.apiUrl}/${endpoint}`, { json: Object.assign({ i: config.i }, param) - }).json(); + }).json(); }; /** diff --git a/src/index.ts b/src/index.ts index 2f85f93..c1a41c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,7 @@ import NotingModule from './modules/noting/index.js'; import PollModule from './modules/poll/index.js'; import ReminderModule from './modules/reminder/index.js'; import CheckCustomEmojisModule from './modules/check-custom-emojis/index.js'; +import { User } from './misskey/user.js'; console.log(' __ ____ _____ ___ '); console.log(' /__\\ (_ _)( _ )/ __)'); @@ -61,7 +62,7 @@ promiseRetry(retry => { json: { i: config.i } - }).json().catch(retry); + }).json().catch(retry); }, { retries: 3 }).then(account => { diff --git a/src/message.ts b/src/message.ts index 104ca18..8f6971c 100644 --- a/src/message.ts +++ b/src/message.ts @@ -61,7 +61,7 @@ export default class Message { this.friend = new Friend(ai, { user: this.user }); // メッセージなどに付いているユーザー情報は省略されている場合があるので完全なユーザー情報を持ってくる - this.ai.api('users/show', { + this.ai.api('users/show', { userId: this.userId }).then(user => { this.friend.updateUser(user); diff --git a/src/modules/chart/index.ts b/src/modules/chart/index.ts index 7605707..97d0f4c 100644 --- a/src/modules/chart/index.ts +++ b/src/modules/chart/index.ts @@ -6,6 +6,27 @@ import { renderChart } from './render-chart.js'; import { items } from '@/vocabulary.js'; import config from '@/config.js'; +type UserNotes = { + diffs: { + normal: number[], + reply: number[], + renote: number[] + } +}; + +type LocalRemotePair = { + local: T, + remote: T +}; + +type UserFollowing = LocalRemotePair<{ + followers: { + total: number[] + } +}>; + +type Notes = LocalRemotePair + export default class extends Module { public readonly name = 'chart'; @@ -48,7 +69,7 @@ export default class extends Module { let chart; if (type === 'userNotes') { - const data = await this.ai.api('charts/user/notes', { + const data = await this.ai.api('charts/user/notes', { span: 'day', limit: 30, userId: params.user.id @@ -65,7 +86,7 @@ export default class extends Module { }] }; } else if (type === 'followers') { - const data = await this.ai.api('charts/user/following', { + const data = await this.ai.api('charts/user/following', { span: 'day', limit: 30, userId: params.user.id @@ -80,7 +101,7 @@ export default class extends Module { }] }; } else if (type === 'notes') { - const data = await this.ai.api('charts/notes', { + const data = await this.ai.api('charts/notes', { span: 'day', limit: 30, }); diff --git a/src/modules/keyword/index.ts b/src/modules/keyword/index.ts index 31f7b0f..a1c7ad9 100644 --- a/src/modules/keyword/index.ts +++ b/src/modules/keyword/index.ts @@ -5,6 +5,12 @@ import config from '@/config.js'; import serifs from '@/serifs.js'; import { mecab } from './mecab.js'; +type LocalTimeline = { + userId: string; + text: string | null; + cw: string | null; +}[]; + function kanaToHira(str: string) { return str.replace(/[\u30a1-\u30f6]/g, match => { const chr = match.charCodeAt(0) - 0x60; @@ -35,7 +41,7 @@ export default class extends Module { @bindThis private async learn() { - const tl = await this.ai.api('notes/local-timeline', { + const tl = await this.ai.api('notes/local-timeline', { limit: 30 }); @@ -47,7 +53,8 @@ export default class extends Module { let keywords: string[][] = []; for (const note of interestedNotes) { - const tokens = await mecab(note.text, config.mecab, config.mecabDic); + // TODO: note.text に null チェックが必要? + const tokens = await mecab(note.text as string, config.mecab, config.mecabDic); const keywordsInThisNote = tokens.filter(token => token[2] == '固有名詞' && token[8] != null); keywords = keywords.concat(keywordsInThisNote); } From 11493a9009365ff8549a67a371e9b0a362426771 Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:31:48 +0900 Subject: [PATCH 03/14] =?UTF-8?q?Message#reply=E3=81=AE=E3=82=AA=E3=83=BC?= =?UTF-8?q?=E3=83=90=E3=83=BC=E3=83=AD=E3=83=BC=E3=83=89=E3=82=92=E5=AE=9A?= =?UTF-8?q?=E7=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/message.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/message.ts b/src/message.ts index 8f6971c..ac84f4a 100644 --- a/src/message.ts +++ b/src/message.ts @@ -8,6 +8,7 @@ import includes from '@/utils/includes.js'; import or from '@/utils/or.js'; import config from '@/config.js'; import { sleep } from '@/utils/sleep.js'; +import { Note } from './misskey/note.js'; export default class Message { private ai: 藍; @@ -68,13 +69,27 @@ export default class Message { }); } + public async reply(text: string, opts?: { + file?: any; + cw?: string; + renote?: string; + immediate?: boolean; + }): Promise; + + public async reply(text: string | null, opts?: { + file?: any; + cw?: string; + renote?: string; + immediate?: boolean; + }): Promise; + @bindThis public async reply(text: string | null, opts?: { file?: any; cw?: string; renote?: string; immediate?: boolean; - }) { + }): Promise { if (text == null) return; this.ai.log(`>>> Sending reply to ${chalk.underline(this.id)}`); From 94d671af0088a2926e1a5d54ac143ad500b4c40b Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:41:28 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=AE=E6=9B=B8=E3=81=8D=E6=96=B9=E3=82=92=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai.ts | 2 +- src/index.ts | 2 +- src/message.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ai.ts b/src/ai.ts index e436533..ae3315a 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -17,7 +17,7 @@ import Stream from '@/stream.js'; import log from '@/utils/log.js'; import { sleep } from './utils/sleep.js'; import pkg from '../package.json' assert { type: 'json' }; -import { Note } from './misskey/note.js'; +import { Note } from '@/misskey/note.js'; type MentionHook = (msg: Message) => Promise; type ContextHook = (key: any, msg: Message, data?: any) => Promise; diff --git a/src/index.ts b/src/index.ts index c1a41c4..9ca82e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,7 @@ import NotingModule from './modules/noting/index.js'; import PollModule from './modules/poll/index.js'; import ReminderModule from './modules/reminder/index.js'; import CheckCustomEmojisModule from './modules/check-custom-emojis/index.js'; -import { User } from './misskey/user.js'; +import { User } from '@/misskey/user.js'; console.log(' __ ____ _____ ___ '); console.log(' /__\\ (_ _)( _ )/ __)'); diff --git a/src/message.ts b/src/message.ts index ac84f4a..4bda18f 100644 --- a/src/message.ts +++ b/src/message.ts @@ -8,7 +8,7 @@ import includes from '@/utils/includes.js'; import or from '@/utils/or.js'; import config from '@/config.js'; import { sleep } from '@/utils/sleep.js'; -import { Note } from './misskey/note.js'; +import { Note } from '@/misskey/note.js'; export default class Message { private ai: 藍; From 1ca93c4edf6997a72716edf91c0cb138c5791f8d Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Mon, 25 Mar 2024 01:38:30 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=E8=97=8D=E3=82=92=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=82=BF=E3=83=BC=E3=83=95=E3=82=A7=E3=82=A4=E3=82=B9=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai.ts | 93 +++++++++++++++++++++++++++++++++++++++++++--------- src/index.ts | 4 +-- 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/ai.ts b/src/ai.ts index ae3315a..b7720ee 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -41,20 +41,31 @@ export type Meta = { /** * 藍 */ -export default class 藍 { +export default interface 藍 extends Ai { + connection: Stream; + lastSleepedAt: number; + + friends: loki.Collection; + moduleData: loki.Collection; +} + +/** + * 起動中の藍 + */ +export class Ai { public readonly version = pkg._v; public account: User; - public connection: Stream; + public connection?: Stream; public modules: Module[] = []; private mentionHooks: MentionHook[] = []; private contextHooks: { [moduleName: string]: ContextHook } = {}; private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {}; public db: loki; - public lastSleepedAt: number; + public lastSleepedAt?: number; - private meta: loki.Collection; + private meta?: loki.Collection; - private contexts: loki.Collection<{ + private contexts?: loki.Collection<{ noteId?: string; userId?: string; module: string; @@ -62,7 +73,7 @@ export default class 藍 { data?: any; }>; - private timers: loki.Collection<{ + private timers?: loki.Collection<{ id: string; module: string; insertedAt: number; @@ -70,8 +81,10 @@ export default class 藍 { data?: any; }>; - public friends: loki.Collection; - public moduleData: loki.Collection; + public friends?: loki.Collection; + public moduleData?: loki.Collection; + + private ready: boolean = false; /** * 藍インスタンスを生成します @@ -138,6 +151,9 @@ export default class 藍 { // Init stream this.connection = new Stream(); + // この時点から藍インスタンスに + this.setReady(); + //#region Main stream const mainStream = this.connection.useSharedConnection('main'); @@ -205,12 +221,40 @@ export default class 藍 { this.log(chalk.green.bold('Ai am now running!')); } + /** + * 準備が完了したフラグを立てる。 + */ + private setReady(): asserts this is 藍 { + // 呼び出すタイミングが正しいか検証 + if ( + this.connection == null || + this.lastSleepedAt == null || + this.meta == null || + this.contexts == null || + this.timers == null || + this.friends == null || + this.moduleData == null + ) { + throw new TypeError('Cannot set ready'); + } + + this.ready = true; + } + + public requireReady(): asserts this is 藍 { + if (!this.ready) { + throw new TypeError('Ai am not ready!'); + } + } + /** * ユーザーから話しかけられたとき * (メンション、リプライ、トークのメッセージ) */ @bindThis private async onReceiveMessage(msg: Message): Promise { + this.requireReady(); + this.log(chalk.gray(`<<< An message received: ${chalk.underline(msg.id)}`)); // Ignore message if the user is a bot @@ -222,7 +266,7 @@ export default class 藍 { const isNoContext = msg.replyId == null; // Look up the context - const context = isNoContext ? null : this.contexts.findOne({ + const context = isNoContext ? null : this.contexts!.findOne({ noteId: msg.replyId }); @@ -278,6 +322,8 @@ export default class 藍 { @bindThis private onNotification(notification: any) { + this.requireReady(); + switch (notification.type) { // リアクションされたら親愛度を少し上げる // TODO: リアクション取り消しをよしなにハンドリングする @@ -294,12 +340,14 @@ export default class 藍 { @bindThis private crawleTimer() { - const timers = this.timers.find(); + this.requireReady(); + + 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.timers!.remove(timer); this.timeoutCallbacks[timer.module](timer.data); } } @@ -330,6 +378,8 @@ export default class 藍 { @bindThis public lookupFriend(userId: User['id']): Friend | null { + this.requireReady(); + const doc = this.friends.findOne({ userId: userId }); @@ -399,7 +449,8 @@ export default class 藍 { */ @bindThis public subscribeReply(module: Module, key: string | null, id: string, data?: any) { - this.contexts.insertOne({ + this.requireReady(); + this.contexts!.insertOne({ noteId: id, module: module.name, key: key, @@ -414,7 +465,8 @@ export default class 藍 { */ @bindThis public unsubscribeReply(module: Module, key: string | null) { - this.contexts.findAndRemove({ + this.requireReady(); + this.contexts!.findAndRemove({ key: key, module: module.name }); @@ -429,8 +481,10 @@ export default class 藍 { */ @bindThis public setTimeoutWithPersistence(module: Module, delay: number, data?: any) { + this.requireReady(); + const id = uuid(); - this.timers.insertOne({ + this.timers!.insertOne({ id: id, module: module.name, insertedAt: Date.now(), @@ -443,6 +497,10 @@ export default class 藍 { @bindThis public getMeta() { + if (this.meta == null) { + throw new TypeError('meta has not been set'); + } + const rec = this.meta.findOne(); if (rec) { @@ -465,6 +523,11 @@ export default class 藍 { rec[k] = v; } - this.meta.update(rec); + this.meta!.update(rec); } } + +// FIXME: +// JS にコンパイルされたコードでインターフェイスであるはずの藍がインポートされてしまうので、 +// 同名のクラスを定義することで実行時エラーが出ないようにしている +export default class 藍 {} diff --git a/src/index.ts b/src/index.ts index 9ca82e2..7a01452 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import chalk from 'chalk'; import got from 'got'; import promiseRetry from 'promise-retry'; -import 藍 from './ai.js'; +import { Ai } from './ai.js'; import config from './config.js'; import _log from './utils/log.js'; import pkg from '../package.json' assert { type: 'json' }; @@ -72,7 +72,7 @@ promiseRetry(retry => { log('Starting AiOS...'); // 藍起動 - new 藍(account, [ + new Ai(account, [ new CoreModule(), new EmojiModule(), new EmojiReactModule(), From 1c842a4b95328e75b646b225bfa0112380bf6e4c Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Mon, 25 Mar 2024 23:45:20 +0900 Subject: [PATCH 06/14] =?UTF-8?q?Module#ai=E3=82=92=E3=82=B2=E3=83=83?= =?UTF-8?q?=E3=82=BF=E3=83=BC=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/module.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/module.ts b/src/module.ts index 27bcf65..6d4770e 100644 --- a/src/module.ts +++ b/src/module.ts @@ -4,11 +4,11 @@ import 藍, { InstallerResult } from '@/ai.js'; export default abstract class Module { public abstract readonly name: string; - protected ai: 藍; + private maybeAi?: 藍; private doc: any; public init(ai: 藍) { - this.ai = ai; + this.maybeAi = ai; this.doc = this.ai.moduleData.findOne({ module: this.name @@ -24,6 +24,13 @@ export default abstract class Module { public abstract install(): InstallerResult; + protected get ai(): 藍 { + if (this.maybeAi == null) { + throw new TypeError('This module has not been initialized'); + } + return this.maybeAi; + } + @bindThis protected log(msg: string) { this.ai.log(`[${this.name}]: ${msg}`); 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 07/14] =?UTF-8?q?InstalledModule=E6=8A=BD=E8=B1=A1?= =?UTF-8?q?=E3=82=AF=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 From fb0a9b0f623b29173b75d9ec5156e25b06d0652e Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Fri, 29 Mar 2024 19:36:36 +0900 Subject: [PATCH 08/14] =?UTF-8?q?Session=E3=81=AE=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=90=E5=9E=8B=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AB=E5=AF=BE?= =?UTF-8?q?=E5=87=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/reversi/back.ts | 39 ++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/modules/reversi/back.ts b/src/modules/reversi/back.ts index 69e0574..19d217d 100644 --- a/src/modules/reversi/back.ts +++ b/src/modules/reversi/back.ts @@ -11,6 +11,7 @@ import * as Reversi from './engine.js'; import config from '@/config.js'; import serifs from '@/serifs.js'; import type { User } from '@/misskey/user.js'; +import { Note } from '@/misskey/note.js'; function getUserName(user) { return user.name || user.username; @@ -24,11 +25,35 @@ const titles = [ ]; class Session { - private account: User; + private maybeAccount?: User; private game: any; private form: any; - private engine: Reversi.Game; - private botColor: Reversi.Color; + private maybeEngine?: Reversi.Game; + private maybeBotColor?: Reversi.Color; + + private get account(): User { + const maybeAccount = this.maybeAccount; + if (maybeAccount == null) { + throw new Error('Have not received "_init_" message'); + } + return maybeAccount; + } + + private get engine(): Reversi.Game { + const maybeEngine = this.maybeEngine; + if (maybeEngine == null) { + throw new Error('Have not received "started" message'); + } + return maybeEngine; + } + + private get botColor(): Reversi.Color { + const maybeBotColor = this.maybeBotColor; + if (maybeBotColor == null) { + throw new Error('Have not received "started" message'); + } + return maybeBotColor; + } private appliedOps: string[] = []; @@ -100,7 +125,7 @@ class Session { private onInit = (msg: any) => { this.game = msg.game; this.form = msg.form; - this.account = msg.account; + this.maybeAccount = msg.account; } /** @@ -121,7 +146,7 @@ class Session { }); // リバーシエンジン初期化 - this.engine = new Reversi.Game(this.game.map, { + this.maybeEngine = new Reversi.Game(this.game.map, { isLlotheo: this.game.isLlotheo, canPutEverywhere: this.game.canPutEverywhere, loopedBoard: this.game.loopedBoard @@ -198,7 +223,7 @@ class Session { //#endregion - this.botColor = this.game.user1Id == this.account.id && this.game.black == 1 || this.game.user2Id == this.account.id && this.game.black == 2; + this.maybeBotColor = this.game.user1Id == this.account.id && this.game.black == 1 || this.game.user2Id == this.account.id && this.game.black == 2; if (this.botColor) { this.think(); @@ -454,7 +479,7 @@ class Session { try { const res = await got.post(`${config.host}/api/notes/create`, { json: body - }).json(); + }).json<{ createdNote: Note }>(); return res.createdNote; } catch (e) { From 2f40fd4aa0aaf9a71949d86f093349c9409ea0b8 Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Sat, 30 Mar 2024 00:57:58 +0900 Subject: [PATCH 09/14] =?UTF-8?q?config=E3=81=AE=E5=9E=8B=E3=83=81?= =?UTF-8?q?=E3=82=A7=E3=83=83=E3=82=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.ts | 67 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/src/config.ts b/src/config.ts index aac43af..c307ef2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,15 +18,72 @@ type Config = { }; import uncheckedConfig from '../config.json' assert { type: 'json' }; +import log from '@/utils/log.js'; + +class Type { + public static readonly string = new Type('string'); + public static readonly boolean = new Type('boolean'); + + public readonly name: string; + + private constructor(name: string) { + this.name = name; + } + + check(value: unknown): value is T { + return typeof value == this.name; + } +} + +function checkProperty(config: Object, key: K, type: Type): config is { [J in K]: Config[K] } { + const result = key in config && type.check(config[key as string]); + if (!result) { + log(`config.json: Property ${key}: ${type.name} required`); + } + return result; +} + +function checkOptionalProperty(config: Object, key: K, type: Type): config is { [J in K]?: Config[K] } { + if (!(key in config)) { + return true; + } + const result = type.check(config[key as string]); + if (!result) { + log(`config.json: The type of property ${key} must be ${type.name}`); + } + return result; +} + +function setProperty(config: Object, key: K, value: Config[K]): asserts config is { [L in K]: Config[K] } { + (config as { [L in K]?: Config[K] })[key] = value; +} function validate(config: unknown): Config { - // TODO: as を使わずにしっかりと検証を行う - return config as Config; + if (!(config instanceof Object)) { + log('config.json: Root object required'); + } else if ( + checkProperty(config, 'host', Type.string) && + checkOptionalProperty(config, 'serverName', Type.string) && + checkProperty(config, 'i', Type.string) && + checkOptionalProperty(config, 'master', Type.string) && + checkProperty(config, 'keywordEnabled', Type.boolean) && + checkProperty(config, 'reversiEnabled', Type.boolean) && + checkProperty(config, 'notingEnabled', Type.boolean) && + checkProperty(config, 'chartEnabled', Type.boolean) && + checkProperty(config, 'serverMonitoring', Type.boolean) && + checkOptionalProperty(config, 'checkEmojisEnabled', Type.boolean) && + checkOptionalProperty(config, 'checkEmojisAtOnce', Type.boolean) && + checkOptionalProperty(config, 'mecab', Type.string) && + checkOptionalProperty(config, 'mecabDic', Type.string) && + checkOptionalProperty(config, 'memoryDir', Type.string) + ) { + setProperty(config, 'wsUrl', config.host.replace('http', 'ws')); + setProperty(config, 'apiUrl', config.host + '/api'); + return config; + } + throw new TypeError('config.json has an invalid type'); } const config = validate(uncheckedConfig); -config.wsUrl = config.host.replace('http', 'ws'); -config.apiUrl = config.host + '/api'; - export default config; From f2bb7ce3f20fabb7e09724e071fbb1b8dc941a55 Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Sat, 30 Mar 2024 14:58:29 +0900 Subject: [PATCH 10/14] =?UTF-8?q?loki=E3=81=8C=E8=AA=AD=E3=81=BF=E8=BE=BC?= =?UTF-8?q?=E3=81=BE=E3=82=8C=E3=82=8B=E3=81=BE=E3=81=A7=E8=97=8D=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=82=B9=E3=82=BF=E3=83=B3=E3=82=B9=E3=81=AE=E7=94=9F?= =?UTF-8?q?=E6=88=90=E3=82=92=E9=81=85=E3=82=89=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai.ts | 111 ++++++++++++++------------------------------------- src/index.ts | 4 +- 2 files changed, 31 insertions(+), 84 deletions(-) diff --git a/src/ai.ts b/src/ai.ts index 3ab54cd..f1616db 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -46,32 +46,21 @@ export type ModuleDataDoc = { /** * 藍 */ -export default interface 藍 extends Ai { - connection: Stream; - lastSleepedAt: number; - - friends: loki.Collection; - moduleData: loki.Collection; -} - -/** - * 起動中の藍 - */ -export class Ai { +export default class 藍 { public readonly version = pkg._v; public account: User; - public connection?: Stream; + public connection: Stream; public modules: Module[] = []; private mentionHooks: MentionHook[] = []; private contextHooks: { [moduleName: string]: ContextHook } = {}; private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {}; public installedModules: { [moduleName: string]: InstalledModule } = {}; public db: loki; - public lastSleepedAt?: number; + public lastSleepedAt: number; - private meta?: loki.Collection; + private meta: loki.Collection; - private contexts?: loki.Collection<{ + private contexts: loki.Collection<{ noteId?: string; userId?: string; module: string; @@ -79,7 +68,7 @@ export class Ai { data?: any; }>; - private timers?: loki.Collection<{ + private timers: loki.Collection<{ id: string; module: string; insertedAt: number; @@ -87,20 +76,16 @@ export class Ai { data?: any; }>; - public friends?: loki.Collection; - public moduleData?: loki.Collection; - - private ready: boolean = false; + public friends: loki.Collection; + public moduleData: loki.Collection; /** * 藍インスタンスを生成します * @param account 藍として使うアカウント * @param modules モジュール。先頭のモジュールほど高優先度 */ - constructor(account: User, modules: Module[]) { - this.account = account; - this.modules = modules; - + @bindThis + public static start(account: User, modules: Module[]) { let memoryDir = '.'; if (config.memoryDir) { memoryDir = config.memoryDir; @@ -109,7 +94,7 @@ export class Ai { this.log(`Lodaing the memory from ${file}...`); - this.db = new loki(file, { + const db = new loki(file, { autoload: true, autosave: true, autosaveInterval: 1000, @@ -118,7 +103,7 @@ export class Ai { this.log(chalk.red(`Failed to load the memory: ${err}`)); } else { this.log(chalk.green('The memory loaded successfully')); - this.run(); + new 藍(account, modules, db); } } }); @@ -126,11 +111,19 @@ export class Ai { @bindThis public log(msg: string) { - log(`[${chalk.magenta('AiOS')}]: ${msg}`); + 藍.log(msg); } @bindThis - private run() { + public static log(msg: string) { + log(`[${chalk.magenta('AiOS')}]: ${msg}`); + } + + private constructor(account: User, modules: Module[], db: loki) { + this.account = account; + this.modules = modules; + this.db = db; + //#region Init DB this.meta = this.getCollection('meta', {}); @@ -157,9 +150,6 @@ export class Ai { // Init stream this.connection = new Stream(); - // この時点から藍インスタンスに - this.setReady(); - //#region Main stream const mainStream = this.connection.useSharedConnection('main'); @@ -227,40 +217,12 @@ export class Ai { this.log(chalk.green.bold('Ai am now running!')); } - /** - * 準備が完了したフラグを立てる。 - */ - private setReady(): asserts this is 藍 { - // 呼び出すタイミングが正しいか検証 - if ( - this.connection == null || - this.lastSleepedAt == null || - this.meta == null || - this.contexts == null || - this.timers == null || - this.friends == null || - this.moduleData == null - ) { - throw new TypeError('Cannot set ready'); - } - - this.ready = true; - } - - public requireReady(): asserts this is 藍 { - if (!this.ready) { - throw new TypeError('Ai am not ready!'); - } - } - /** * ユーザーから話しかけられたとき * (メンション、リプライ、トークのメッセージ) */ @bindThis private async onReceiveMessage(msg: Message): Promise { - this.requireReady(); - this.log(chalk.gray(`<<< An message received: ${chalk.underline(msg.id)}`)); // Ignore message if the user is a bot @@ -272,7 +234,7 @@ export class Ai { const isNoContext = msg.replyId == null; // Look up the context - const context = isNoContext ? null : this.contexts!.findOne({ + const context = isNoContext ? null : this.contexts.findOne({ noteId: msg.replyId }); @@ -328,8 +290,6 @@ export class Ai { @bindThis private onNotification(notification: any) { - this.requireReady(); - switch (notification.type) { // リアクションされたら親愛度を少し上げる // TODO: リアクション取り消しをよしなにハンドリングする @@ -346,14 +306,12 @@ export class Ai { @bindThis private crawleTimer() { - this.requireReady(); - - const timers = this.timers!.find(); + 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.timers.remove(timer); this.timeoutCallbacks[timer.module](timer.data); } } @@ -384,8 +342,6 @@ export class Ai { @bindThis public lookupFriend(userId: User['id']): Friend | null { - this.requireReady(); - const doc = this.friends.findOne({ userId: userId }); @@ -455,8 +411,7 @@ export class Ai { */ @bindThis public subscribeReply(module: Module, key: string | null, id: string, data?: any) { - this.requireReady(); - this.contexts!.insertOne({ + this.contexts.insertOne({ noteId: id, module: module.name, key: key, @@ -471,8 +426,7 @@ export class Ai { */ @bindThis public unsubscribeReply(module: Module, key: string | null) { - this.requireReady(); - this.contexts!.findAndRemove({ + this.contexts.findAndRemove({ key: key, module: module.name }); @@ -487,10 +441,8 @@ export class Ai { */ @bindThis public setTimeoutWithPersistence(module: Module, delay: number, data?: any) { - this.requireReady(); - const id = uuid(); - this.timers!.insertOne({ + this.timers.insertOne({ id: id, module: module.name, insertedAt: Date.now(), @@ -529,11 +481,6 @@ export class Ai { rec[k] = v; } - this.meta!.update(rec); + this.meta.update(rec); } } - -// FIXME: -// JS にコンパイルされたコードでインターフェイスであるはずの藍がインポートされてしまうので、 -// 同名のクラスを定義することで実行時エラーが出ないようにしている -export default class 藍 {} diff --git a/src/index.ts b/src/index.ts index 7a01452..6f046e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import chalk from 'chalk'; import got from 'got'; import promiseRetry from 'promise-retry'; -import { Ai } from './ai.js'; +import 藍 from './ai.js'; import config from './config.js'; import _log from './utils/log.js'; import pkg from '../package.json' assert { type: 'json' }; @@ -72,7 +72,7 @@ promiseRetry(retry => { log('Starting AiOS...'); // 藍起動 - new Ai(account, [ + 藍.start(account, [ new CoreModule(), new EmojiModule(), new EmojiReactModule(), From 422fddafa46f2d3bbc32c8a4693175660d92fc6e Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Sat, 30 Mar 2024 15:06:17 +0900 Subject: [PATCH 11/14] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AB=E3=81=AA?= =?UTF-8?q?=E3=81=A3=E3=81=9Fnull=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ai.ts b/src/ai.ts index f1616db..ad38f6a 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -455,10 +455,6 @@ export default class 藍 { @bindThis public getMeta() { - if (this.meta == null) { - throw new TypeError('meta has not been set'); - } - const rec = this.meta.findOne(); if (rec) { From 68b7765d58877cc13d49616e50b595275a1fdc55 Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:07:08 +0900 Subject: [PATCH 12/14] =?UTF-8?q?log=E3=81=AE=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai.ts | 2 +- src/config.ts | 13 +++++++++---- src/utils/log.ts | 10 +++++++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/ai.ts b/src/ai.ts index ad38f6a..5d7476f 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -115,7 +115,7 @@ export default class 藍 { } @bindThis - public static log(msg: string) { + private static log(msg: string) { log(`[${chalk.magenta('AiOS')}]: ${msg}`); } diff --git a/src/config.ts b/src/config.ts index c307ef2..1f17f5c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,8 +17,13 @@ type Config = { memoryDir?: string; }; +import chalk from 'chalk'; import uncheckedConfig from '../config.json' assert { type: 'json' }; -import log from '@/utils/log.js'; +import { warn } from '@/utils/log.js'; + +function warnWithPrefix(msg: string): void { + warn(`[Config]: ${chalk.red(msg)}`); +} class Type { public static readonly string = new Type('string'); @@ -38,7 +43,7 @@ class Type { function checkProperty(config: Object, key: K, type: Type): config is { [J in K]: Config[K] } { const result = key in config && type.check(config[key as string]); if (!result) { - log(`config.json: Property ${key}: ${type.name} required`); + warnWithPrefix(`config.json: Property '${key}': ${type.name} required`); } return result; } @@ -49,7 +54,7 @@ function checkOptionalProperty(config: Object, key: K, t } const result = type.check(config[key as string]); if (!result) { - log(`config.json: The type of property ${key} must be ${type.name}`); + warnWithPrefix(`config.json: The type of property '${key}' must be ${type.name}`); } return result; } @@ -60,7 +65,7 @@ function setProperty(config: Object, key: K, value: Conf function validate(config: unknown): Config { if (!(config instanceof Object)) { - log('config.json: Root object required'); + warnWithPrefix('config.json: Root object required'); } else if ( checkProperty(config, 'host', Type.string) && checkOptionalProperty(config, 'serverName', Type.string) && diff --git a/src/utils/log.ts b/src/utils/log.ts index 8b9eba8..fac5a61 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -1,9 +1,17 @@ import chalk from 'chalk'; export default function(msg: string) { + console.log(createMessage(msg)); +} + +export function warn(msg: string) { + console.warn(createMessage(msg)); +} + +function createMessage(msg: string) { const now = new Date(); const date = `${zeroPad(now.getHours())}:${zeroPad(now.getMinutes())}:${zeroPad(now.getSeconds())}`; - console.log(`${chalk.gray(date)} ${msg}`); + return `${chalk.gray(date)} ${msg}`; } function zeroPad(num: number, length: number = 2): string { From d463a0db30f0728a1aaee8129004c273b7bdfc39 Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Sat, 30 Mar 2024 17:17:43 +0900 Subject: [PATCH 13/14] =?UTF-8?q?=E3=81=99=E3=81=B9=E3=81=A6=E3=81=AE?= =?UTF-8?q?=E3=83=97=E3=83=AD=E3=83=91=E3=83=86=E3=82=A3=E3=82=92=E3=83=81?= =?UTF-8?q?=E3=82=A7=E3=83=83=E3=82=AF=E3=81=97=E3=81=A6=E3=81=8B=E3=82=89?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E9=80=81=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.ts | 78 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/src/config.ts b/src/config.ts index 1f17f5c..1b1989d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -40,23 +40,44 @@ class Type { } } -function checkProperty(config: Object, key: K, type: Type): config is { [J in K]: Config[K] } { - const result = key in config && type.check(config[key as string]); - if (!result) { - warnWithPrefix(`config.json: Property '${key}': ${type.name} required`); +class OptionalProperty { + protected readonly key: K; + protected readonly type: Type + + public constructor(key: K, type: Type) { + this.key = key; + this.type = type; + } + + check(config: Object): config is { [J in K]?: Config[K] } { + const key = this.key; + if (!(key in config)) { + return true; + } + const result = this.type.check((config as { [J in K]?: unknown})[key]); + if (!result) { + warnWithPrefix(`config.json: The type of property '${key}' must be ${this.type.name}`); + } + return result; } - return result; } -function checkOptionalProperty(config: Object, key: K, type: Type): config is { [J in K]?: Config[K] } { - if (!(key in config)) { - return true; +class Property extends OptionalProperty { + check(config: Object): config is { [J in K]: Config[K] } { + const result = this.key in config && this.type.check((config as { [J in K]?: unknown })[this.key]); + if (!result) { + warnWithPrefix(`config.json: Property '${this.key}': ${this.type.name} required`); + } + return result; } - const result = type.check(config[key as string]); - if (!result) { - warnWithPrefix(`config.json: The type of property '${key}' must be ${type.name}`); - } - return result; +} + +type Intersection

= P extends [infer Q, ...infer R] ? Q & Intersection : unknown; + +function checkProperties

[]>(config: Object, ...properties: P): + config is object & Intersection<{ [I in keyof P]: P[I] extends OptionalProperty ? { [J in K]: Config[K] } : never }> { + // メッセージを表示するためすべてのプロパティをチェックしてから結果を返す + return properties.map(p => p.check(config)).every(c => c); } function setProperty(config: Object, key: K, value: Config[K]): asserts config is { [L in K]: Config[K] } { @@ -67,20 +88,23 @@ function validate(config: unknown): Config { if (!(config instanceof Object)) { warnWithPrefix('config.json: Root object required'); } else if ( - checkProperty(config, 'host', Type.string) && - checkOptionalProperty(config, 'serverName', Type.string) && - checkProperty(config, 'i', Type.string) && - checkOptionalProperty(config, 'master', Type.string) && - checkProperty(config, 'keywordEnabled', Type.boolean) && - checkProperty(config, 'reversiEnabled', Type.boolean) && - checkProperty(config, 'notingEnabled', Type.boolean) && - checkProperty(config, 'chartEnabled', Type.boolean) && - checkProperty(config, 'serverMonitoring', Type.boolean) && - checkOptionalProperty(config, 'checkEmojisEnabled', Type.boolean) && - checkOptionalProperty(config, 'checkEmojisAtOnce', Type.boolean) && - checkOptionalProperty(config, 'mecab', Type.string) && - checkOptionalProperty(config, 'mecabDic', Type.string) && - checkOptionalProperty(config, 'memoryDir', Type.string) + checkProperties( + config, + new Property('host', Type.string), + new OptionalProperty('serverName', Type.string), + new Property('i', Type.string), + new OptionalProperty('master', Type.string), + new Property('keywordEnabled', Type.boolean), + new Property('reversiEnabled', Type.boolean), + new Property('notingEnabled', Type.boolean), + new Property('chartEnabled', Type.boolean), + new Property('serverMonitoring', Type.boolean), + new OptionalProperty('checkEmojisEnabled', Type.boolean), + new OptionalProperty('checkEmojisAtOnce', Type.boolean), + new OptionalProperty('mecab', Type.string), + new OptionalProperty('mecabDic', Type.string), + new OptionalProperty('memoryDir', Type.string) + ) ) { setProperty(config, 'wsUrl', config.host.replace('http', 'ws')); setProperty(config, 'apiUrl', config.host + '/api'); From b2ab6778b04c8546672a242d934ac2399c041d2f Mon Sep 17 00:00:00 2001 From: takejohn <105504345+takejohn@users.noreply.github.com> Date: Sat, 30 Mar 2024 17:53:58 +0900 Subject: [PATCH 14/14] =?UTF-8?q?=E5=9E=8B=E3=81=AE=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/misskey/note.ts | 2 ++ src/modules/keyword/index.ts | 14 ++++---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/misskey/note.ts b/src/misskey/note.ts index 7aa0aff..f426677 100644 --- a/src/misskey/note.ts +++ b/src/misskey/note.ts @@ -1,6 +1,8 @@ export type Note = { id: string; text: string | null; + cw: string | null; + userId: string; reply: any | null; poll?: { choices: { diff --git a/src/modules/keyword/index.ts b/src/modules/keyword/index.ts index 3b6d42c..ac88a01 100644 --- a/src/modules/keyword/index.ts +++ b/src/modules/keyword/index.ts @@ -5,12 +5,7 @@ import config from '@/config.js'; import serifs from '@/serifs.js'; import { mecab } from './mecab.js'; import 藍 from '@/ai.js'; - -type LocalTimeline = { - userId: string; - text: string | null; - cw: string | null; -}[]; +import { Note } from '@/misskey/note.js'; function kanaToHira(str: string) { return str.replace(/[\u30a1-\u30f6]/g, match => { @@ -48,11 +43,11 @@ class Installed extends InstalledModule { @bindThis private async learn() { - const tl = await this.ai.api('notes/local-timeline', { + const tl = await this.ai.api('notes/local-timeline', { limit: 30 }); - const interestedNotes = tl.filter(note => + const interestedNotes = tl.filter((note): note is Note & { text: string } => note.userId !== this.ai.account.id && note.text != null && note.cw == null); @@ -60,8 +55,7 @@ class Installed extends InstalledModule { let keywords: string[][] = []; for (const note of interestedNotes) { - // TODO: note.text に null チェックが必要? - const tokens = await mecab(note.text as string, config.mecab, config.mecabDic); + const tokens = await mecab(note.text, config.mecab, config.mecabDic); const keywordsInThisNote = tokens.filter(token => token[2] == '固有名詞' && token[8] != null); keywords = keywords.concat(keywordsInThisNote); }