Compare commits

...

6 commits

Author SHA1 Message Date
Take-John 9ad70138d2
Merge b2ab6778b0 into 830c9c2ecd 2024-03-30 17:54:35 +09:00
takejohn b2ab6778b0 型の調整 2024-03-30 17:53:58 +09:00
takejohn d463a0db30 すべてのプロパティをチェックしてからエラー送出 2024-03-30 17:17:43 +09:00
takejohn 68b7765d58 logの調整 2024-03-30 16:07:08 +09:00
takejohn 422fddafa4 不要になったnullチェックを削除 2024-03-30 15:06:17 +09:00
takejohn f2bb7ce3f2 lokiが読み込まれるまで藍インスタンスの生成を遅らせる 2024-03-30 14:58:29 +09:00
6 changed files with 104 additions and 128 deletions

115
src/ai.ts
View file

@ -46,32 +46,21 @@ export type ModuleDataDoc<Data = any> = {
/**
*
*/
export default interface extends Ai {
connection: Stream;
lastSleepedAt: number;
friends: loki.Collection<FriendDoc>;
moduleData: loki.Collection<ModuleDataDoc>;
}
/**
*
*/
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<Meta>;
private meta: loki.Collection<Meta>;
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<FriendDoc>;
public moduleData?: loki.Collection<any>;
private ready: boolean = false;
public friends: loki.Collection<FriendDoc>;
public moduleData: loki.Collection<any>;
/**
*
* @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() {
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', {});
@ -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<void> {
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(),
@ -503,10 +455,6 @@ export class Ai {
@bindThis
public getMeta() {
if (this.meta == null) {
throw new TypeError('meta has not been set');
}
const rec = this.meta.findOne();
if (rec) {
@ -529,11 +477,6 @@ export class Ai {
rec[k] = v;
}
this.meta!.update(rec);
this.meta.update(rec);
}
}
// FIXME:
// JS にコンパイルされたコードでインターフェイスであるはずの藍がインポートされてしまうので、
// 同名のクラスを定義することで実行時エラーが出ないようにしている
export default class {}

View file

@ -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<T> {
public static readonly string = new Type<string>('string');
@ -35,24 +40,45 @@ class Type<T> {
}
}
function checkProperty<K extends keyof Config>(config: Object, key: K, type: Type<Config[K]>): 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;
class OptionalProperty<K extends keyof Config> {
protected readonly key: K;
protected readonly type: Type<Config[K]>
public constructor(key: K, type: Type<Config[K]>) {
this.key = key;
this.type = type;
}
function checkOptionalProperty<K extends keyof Config>(config: Object, key: K, type: Type<Config[K]>): config is { [J in K]?: Config[K] } {
check(config: Object): config is { [J in K]?: Config[K] } {
const key = this.key;
if (!(key in config)) {
return true;
}
const result = type.check(config[key as string]);
const result = this.type.check((config as { [J in K]?: unknown})[key]);
if (!result) {
log(`config.json: The type of property ${key} must be ${type.name}`);
warnWithPrefix(`config.json: The type of property '${key}' must be ${this.type.name}`);
}
return result;
}
}
class Property<K extends keyof Config> extends OptionalProperty<K> {
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 unknown[]> = P extends [infer Q, ...infer R] ? Q & Intersection<R> : unknown;
function checkProperties<P extends OptionalProperty<keyof Config>[]>(config: Object, ...properties: P):
config is object & Intersection<{ [I in keyof P]: P[I] extends OptionalProperty<infer K> ? { [J in K]: Config[K] } : never }> {
// メッセージを表示するためすべてのプロパティをチェックしてから結果を返す
return properties.map(p => p.check(config)).every(c => c);
}
function setProperty<K extends keyof Config>(config: Object, key: K, value: Config[K]): asserts config is { [L in K]: Config[K] } {
(config as { [L in K]?: Config[K] })[key] = value;
@ -60,22 +86,25 @@ function setProperty<K extends keyof Config>(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) &&
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');

View file

@ -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(),

View file

@ -1,6 +1,8 @@
export type Note = {
id: string;
text: string | null;
cw: string | null;
userId: string;
reply: any | null;
poll?: {
choices: {

View file

@ -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<LocalTimeline>('notes/local-timeline', {
const tl = await this.ai.api<Note[]>('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);
}

View file

@ -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 {