diff --git a/src/ai.ts b/src/ai.ts index 701b97e..5bffc4d 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'; @@ -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; @@ -37,6 +38,11 @@ export type Meta = { lastWakingAt: number; }; +export type ModuleDataDoc = { + module: string; + data: Data; +} + /** * 藍 */ @@ -77,10 +83,8 @@ export default class 藍 { * @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; @@ -89,7 +93,7 @@ export default class 藍 { this.log(`Lodaing the memory from ${file}...`); - this.db = new loki(file, { + const db = new loki(file, { autoload: true, autosave: true, autosaveInterval: 1000, @@ -98,7 +102,7 @@ export default class 藍 { 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); } } }); @@ -106,11 +110,19 @@ export default class 藍 { @bindThis public log(msg: string) { - log(`[${chalk.magenta('AiOS')}]: ${msg}`); + 藍.log(msg); } @bindThis - private run() { + private 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', {}); @@ -187,7 +199,7 @@ export default class 藍 { 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; @@ -361,7 +373,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 +392,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/config.ts b/src/config.ts index 9466b0c..1b1989d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,9 +17,102 @@ type Config = { memoryDir?: string; }; -import config from '../config.json' assert { type: 'json' }; +import chalk from 'chalk'; +import uncheckedConfig from '../config.json' assert { type: 'json' }; +import { warn } from '@/utils/log.js'; -config.wsUrl = config.host.replace('http', 'ws'); -config.apiUrl = config.host + '/api'; +function warnWithPrefix(msg: string): void { + warn(`[Config]: ${chalk.red(msg)}`); +} -export default config as Config; +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; + } +} + +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; + } +} + +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; + } +} + +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] } { + (config as { [L in K]?: Config[K] })[key] = value; +} + +function validate(config: unknown): Config { + if (!(config instanceof Object)) { + warnWithPrefix('config.json: Root object required'); + } else if ( + 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'); + return config; + } + throw new TypeError('config.json has an invalid type'); +} + +const config = validate(uncheckedConfig); + +export default config; diff --git a/src/index.ts b/src/index.ts index 2f85f93..6f046e6 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 => { @@ -71,7 +72,7 @@ promiseRetry(retry => { log('Starting AiOS...'); // 藍起動 - new 藍(account, [ + 藍.start(account, [ new CoreModule(), new EmojiModule(), new EmojiReactModule(), diff --git a/src/message.ts b/src/message.ts index 104ca18..4bda18f 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: 藍; @@ -61,20 +62,34 @@ 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); }); } + 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)}`); 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/module.ts b/src/module.ts index 27bcf65..6684e91 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,29 +1,36 @@ 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; - protected ai: 藍; - private doc: any; + private maybeAi?: 藍; + + /** + * @deprecated + */ + public installed?: InstalledModule; public init(ai: 藍) { - this.ai = ai; - - this.doc = this.ai.moduleData.findOne({ - module: this.name - }); - - if (this.doc == null) { - this.doc = this.ai.moduleData.insertOne({ - module: this.name, - data: {} - }); - } + this.maybeAi = ai; } - 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'); + } + return this.maybeAi; + } + + /** + * @deprecated {@link InstalledModule#log} を使用すること + */ @bindThis protected log(msg: string) { this.ai.log(`[${this.name}]: ${msg}`); @@ -34,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) { @@ -43,6 +51,7 @@ export default abstract class Module { /** * 返信の待ち受けを解除します * @param key コンテキストを識別するためのキー + * @deprecated {@link InstalledModule#unsubscribeReply} を使用すること */ @bindThis protected unsubscribeReply(key: string | null) { @@ -54,20 +63,138 @@ 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() { + let doc = this.ai.moduleData.findOne({ + module: this.name + }); + if (doc == null) { + doc = this.ai.moduleData.insertOne({ + module: this.name, + data: {} + }); + } + 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; + } + + module.installed = 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/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/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 31f7b0f..ac88a01 100644 --- a/src/modules/keyword/index.ts +++ b/src/modules/keyword/index.ts @@ -1,9 +1,11 @@ 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'; +import { Note } from '@/misskey/note.js'; function kanaToHira(str: string) { return str.replace(/[\u30a1-\u30f6]/g, match => { @@ -15,31 +17,37 @@ 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 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); 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/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) { 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 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 {