Compare commits

..

1 commit

Author SHA1 Message Date
Take-John 6f267b608a
Merge 2f40fd4aa0 into 830c9c2ecd 2024-03-30 00:58:05 +09:00
6 changed files with 128 additions and 104 deletions

115
src/ai.ts
View file

@ -46,21 +46,32 @@ export type ModuleDataDoc<Data = any> = {
/**
*
*/
export default class {
export default interface extends Ai {
connection: Stream;
lastSleepedAt: number;
friends: loki.Collection<FriendDoc>;
moduleData: loki.Collection<ModuleDataDoc>;
}
/**
*
*/
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 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;
@ -68,7 +79,7 @@ export default class 藍 {
data?: any;
}>;
private timers: loki.Collection<{
private timers?: loki.Collection<{
id: string;
module: string;
insertedAt: number;
@ -76,16 +87,20 @@ export default class 藍 {
data?: any;
}>;
public friends: loki.Collection<FriendDoc>;
public moduleData: loki.Collection<any>;
public friends?: loki.Collection<FriendDoc>;
public moduleData?: loki.Collection<any>;
private ready: boolean = false;
/**
*
* @param account 使
* @param modules
*/
@bindThis
public static start(account: User, modules: Module[]) {
constructor(account: User, modules: Module[]) {
this.account = account;
this.modules = modules;
let memoryDir = '.';
if (config.memoryDir) {
memoryDir = config.memoryDir;
@ -94,7 +109,7 @@ export default class 藍 {
this.log(`Lodaing the memory from ${file}...`);
const db = new loki(file, {
this.db = new loki(file, {
autoload: true,
autosave: true,
autosaveInterval: 1000,
@ -103,7 +118,7 @@ export default class 藍 {
this.log(chalk.red(`Failed to load the memory: ${err}`));
} else {
this.log(chalk.green('The memory loaded successfully'));
new (account, modules, db);
this.run();
}
}
});
@ -111,19 +126,11 @@ export default class 藍 {
@bindThis
public log(msg: string) {
.log(msg);
}
@bindThis
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;
@bindThis
private run() {
//#region Init DB
this.meta = this.getCollection('meta', {});
@ -150,6 +157,9 @@ export default class 藍 {
// Init stream
this.connection = new Stream();
// この時点から藍インスタンスに
this.setReady();
//#region Main stream
const mainStream = this.connection.useSharedConnection('main');
@ -217,12 +227,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<void> {
this.requireReady();
this.log(chalk.gray(`<<< An message received: ${chalk.underline(msg.id)}`));
// Ignore message if the user is a bot
@ -234,7 +272,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
});
@ -290,6 +328,8 @@ export default class 藍 {
@bindThis
private onNotification(notification: any) {
this.requireReady();
switch (notification.type) {
// リアクションされたら親愛度を少し上げる
// TODO: リアクション取り消しをよしなにハンドリングする
@ -306,12 +346,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);
}
}
@ -342,6 +384,8 @@ export default class 藍 {
@bindThis
public lookupFriend(userId: User['id']): Friend | null {
this.requireReady();
const doc = this.friends.findOne({
userId: userId
});
@ -411,7 +455,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,
@ -426,7 +471,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
});
@ -441,8 +487,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(),
@ -455,6 +503,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) {
@ -477,6 +529,11 @@ export default class 藍 {
rec[k] = v;
}
this.meta.update(rec);
this.meta!.update(rec);
}
}
// FIXME:
// JS にコンパイルされたコードでインターフェイスであるはずの藍がインポートされてしまうので、
// 同名のクラスを定義することで実行時エラーが出ないようにしている
export default class {}

View file

@ -17,13 +17,8 @@ type Config = {
memoryDir?: string;
};
import chalk from 'chalk';
import uncheckedConfig from '../config.json' assert { type: 'json' };
import { warn } from '@/utils/log.js';
function warnWithPrefix(msg: string): void {
warn(`[Config]: ${chalk.red(msg)}`);
}
import log from '@/utils/log.js';
class Type<T> {
public static readonly string = new Type<string>('string');
@ -40,44 +35,23 @@ class Type<T> {
}
}
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;
}
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;
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 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;
function checkOptionalProperty<K extends keyof Config>(config: Object, key: K, type: Type<Config[K]>): config is { [J in K]?: Config[K] } {
if (!(key in config)) {
return true;
}
}
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);
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<K extends keyof Config>(config: Object, key: K, value: Config[K]): asserts config is { [L in K]: Config[K] } {
@ -86,25 +60,22 @@ function setProperty<K extends keyof Config>(config: Object, key: K, value: Conf
function validate(config: unknown): Config {
if (!(config instanceof Object)) {
warnWithPrefix('config.json: Root object required');
log('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)
)
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');

View file

@ -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...');
// 藍起動
.start(account, [
new Ai(account, [
new CoreModule(),
new EmojiModule(),
new EmojiReactModule(),

View file

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

View file

@ -5,7 +5,12 @@ 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';
type LocalTimeline = {
userId: string;
text: string | null;
cw: string | null;
}[];
function kanaToHira(str: string) {
return str.replace(/[\u30a1-\u30f6]/g, match => {
@ -43,11 +48,11 @@ class Installed extends InstalledModule {
@bindThis
private async learn() {
const tl = await this.ai.api<Note[]>('notes/local-timeline', {
const tl = await this.ai.api<LocalTimeline>('notes/local-timeline', {
limit: 30
});
const interestedNotes = tl.filter((note): note is Note & { text: string } =>
const interestedNotes = tl.filter(note =>
note.userId !== this.ai.account.id &&
note.text != null &&
note.cw == null);
@ -55,7 +60,8 @@ class Installed extends InstalledModule {
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);
}

View file

@ -1,17 +1,9 @@
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())}`;
return `${chalk.gray(date)} ${msg}`;
console.log(`${chalk.gray(date)} ${msg}`);
}
function zeroPad(num: number, length: number = 2): string {