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 1/6] =?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 2/6] =?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 3/6] =?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 4/6] =?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 5/6] =?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 6/6] =?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}`);