ai/src/ai.ts

470 lines
12 KiB
TypeScript
Raw Normal View History

2018-08-11 06:26:25 +00:00
// AI CORE
2019-05-10 02:55:07 +00:00
import * as fs from 'fs';
2019-01-14 15:14:22 +00:00
import autobind from 'autobind-decorator';
2018-08-26 21:16:56 +00:00
import * as loki from 'lokijs';
2018-08-11 06:26:25 +00:00
import * as request from 'request-promise-native';
2020-08-23 02:31:35 +00:00
import * as chalk from 'chalk';
import { v4 as uuid } from 'uuid';
2019-01-15 09:47:22 +00:00
const delay = require('timeout-as-promise');
2020-09-19 02:37:04 +00:00
import config from '@/config';
import Module from '@/module';
import Message from '@/message';
import Friend, { FriendDoc } from '@/friend';
import { User } from '@/misskey/user';
import Stream from '@/stream';
import log from '@/utils/log';
2020-09-02 12:54:01 +00:00
const pkg = require('../package.json');
2018-08-11 06:26:25 +00:00
2019-01-23 12:49:10 +00:00
type MentionHook = (msg: Message) => Promise<boolean | HandlerResult>;
2020-10-31 03:18:42 +00:00
type ContextHook = (key: any, msg: Message, data?: any) => Promise<void | boolean | HandlerResult>;
2019-02-01 17:06:51 +00:00
type TimeoutCallback = (data?: any) => void;
2019-01-14 15:14:22 +00:00
export type HandlerResult = {
2020-10-31 03:18:42 +00:00
reaction?: string | null;
immediate?: boolean;
2019-01-14 15:14:22 +00:00
};
export type InstallerResult = {
2019-01-15 03:01:58 +00:00
mentionHook?: MentionHook;
contextHook?: ContextHook;
2019-02-01 17:06:51 +00:00
timeoutCallback?: TimeoutCallback;
2019-01-14 15:14:22 +00:00
};
2020-08-29 06:52:33 +00:00
export type Meta = {
lastWakingAt: number;
};
2018-08-11 06:26:25 +00:00
/**
*
*/
export default class {
2020-09-02 12:54:01 +00:00
public readonly version = pkg._v;
2018-08-28 21:30:48 +00:00
public account: User;
2018-10-09 15:47:03 +00:00
public connection: Stream;
2019-01-14 15:14:22 +00:00
public modules: Module[] = [];
2019-01-15 03:01:58 +00:00
private mentionHooks: MentionHook[] = [];
private contextHooks: { [moduleName: string]: ContextHook } = {};
2019-02-01 17:06:51 +00:00
private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {};
2018-08-26 21:16:56 +00:00
public db: loki;
2020-08-29 06:52:33 +00:00
public lastSleepedAt: number;
private meta: loki.Collection<Meta>;
2018-08-26 21:16:56 +00:00
private contexts: loki.Collection<{
noteId?: string;
userId?: string;
module: string;
2020-09-02 12:54:01 +00:00
key: string | null;
2018-08-26 21:59:18 +00:00
data?: any;
}>;
2019-02-01 17:06:51 +00:00
private timers: loki.Collection<{
id: string;
module: string;
insertedAt: number;
delay: number;
data?: any;
}>;
2018-08-27 11:22:59 +00:00
public friends: loki.Collection<FriendDoc>;
2019-05-10 02:55:07 +00:00
public moduleData: loki.Collection<any>;
2018-08-26 21:16:56 +00:00
2019-01-15 17:48:14 +00:00
/**
*
* @param account 使
* @param modules
*/
2019-01-15 17:10:42 +00:00
constructor(account: User, modules: Module[]) {
2018-08-11 06:26:25 +00:00
this.account = account;
2019-01-15 17:10:42 +00:00
this.modules = modules;
2018-08-11 06:26:25 +00:00
2021-12-07 02:48:48 +00:00
let memoryDir = '.';
if (config.memoryDir) {
memoryDir = config.memoryDir;
}
2021-12-07 02:48:48 +00:00
const file = process.env.NODE_ENV === 'test' ? `${memoryDir}/test.memory.json` : `${memoryDir}/memory.json`;
2019-01-15 03:29:11 +00:00
2020-09-19 01:40:44 +00:00
this.log(`Lodaing the memory from ${file}...`);
this.db = new loki(file, {
2018-08-26 21:16:56 +00:00
autoload: true,
autosave: true,
autosaveInterval: 1000,
2019-01-14 15:14:22 +00:00
autoloadCallback: err => {
if (err) {
2019-01-15 03:29:11 +00:00
this.log(chalk.red(`Failed to load the memory: ${err}`));
2019-01-14 15:14:22 +00:00
} else {
2019-01-15 03:29:11 +00:00
this.log(chalk.green('The memory loaded successfully'));
2019-01-15 17:10:42 +00:00
this.run();
2019-01-14 15:14:22 +00:00
}
}
2018-08-26 21:16:56 +00:00
});
}
2019-01-14 15:14:22 +00:00
@autobind
public log(msg: string) {
2019-01-15 03:29:11 +00:00
log(chalk`[{magenta AiOS}]: ${msg}`);
2019-01-14 15:14:22 +00:00
}
@autobind
2019-01-15 01:23:54 +00:00
private run() {
2018-08-26 21:16:56 +00:00
//#region Init DB
2020-08-29 06:52:33 +00:00
this.meta = this.getCollection('meta', {});
2019-01-24 00:34:03 +00:00
this.contexts = this.getCollection('contexts', {
2018-08-29 07:26:33 +00:00
indices: ['key']
});
2019-02-01 17:06:51 +00:00
this.timers = this.getCollection('timers', {
indices: ['module']
});
2019-01-24 00:34:03 +00:00
this.friends = this.getCollection('friends', {
2018-08-29 07:26:33 +00:00
indices: ['userId']
});
2019-05-10 02:55:07 +00:00
this.moduleData = this.getCollection('moduleData', {
indices: ['module']
});
2018-08-26 21:16:56 +00:00
//#endregion
2020-08-29 06:52:33 +00:00
const meta = this.getMeta();
this.lastSleepedAt = meta.lastWakingAt;
2018-10-09 15:47:03 +00:00
// Init stream
this.connection = new Stream();
2018-08-11 06:26:25 +00:00
2018-10-09 15:47:03 +00:00
//#region Main stream
const mainStream = this.connection.useSharedConnection('main');
2018-08-11 06:26:25 +00:00
2018-10-09 15:47:03 +00:00
// メンションされたとき
2019-01-23 13:18:02 +00:00
mainStream.on('mention', async data => {
2018-10-09 15:47:03 +00:00
if (data.userId == this.account.id) return; // 自分は弾く
2019-01-14 15:14:22 +00:00
if (data.text && data.text.startsWith('@' + this.account.username)) {
2019-01-23 13:18:02 +00:00
// Misskeyのバグで投稿が非公開扱いになる
if (data.text == null) data = await this.api('notes/show', { noteId: data.id });
2019-01-15 09:47:22 +00:00
this.onReceiveMessage(new Message(this, data, false));
2018-10-09 15:47:03 +00:00
}
2018-08-13 21:14:47 +00:00
});
2018-10-09 15:47:03 +00:00
// 返信されたとき
2019-01-23 13:18:02 +00:00
mainStream.on('reply', async data => {
2018-10-09 15:47:03 +00:00
if (data.userId == this.account.id) return; // 自分は弾く
2019-01-23 13:18:02 +00:00
if (data.text && data.text.startsWith('@' + this.account.username)) return;
// Misskeyのバグで投稿が非公開扱いになる
if (data.text == null) data = await this.api('notes/show', { noteId: data.id });
2019-01-15 09:47:22 +00:00
this.onReceiveMessage(new Message(this, data, false));
2018-08-13 21:14:47 +00:00
});
2019-01-24 11:49:27 +00:00
// Renoteされたとき
mainStream.on('renote', async data => {
if (data.userId == this.account.id) return; // 自分は弾く
if (data.text == null && (data.files || []).length == 0) return;
// リアクションする
this.api('notes/reactions/create', {
noteId: data.id,
reaction: 'love'
});
});
2018-10-09 15:47:03 +00:00
// メッセージ
mainStream.on('messagingMessage', data => {
if (data.userId == this.account.id) return; // 自分は弾く
2019-01-15 09:47:22 +00:00
this.onReceiveMessage(new Message(this, data, true));
2018-08-13 21:14:47 +00:00
});
// 通知
mainStream.on('notification', data => {
this.onNotification(data);
});
2018-08-13 21:14:47 +00:00
//#endregion
2018-08-11 06:26:25 +00:00
2018-10-09 15:47:03 +00:00
// Install modules
2019-01-14 15:14:22 +00:00
this.modules.forEach(m => {
this.log(`Installing ${chalk.cyan.italic(m.name)}\tmodule...`);
2019-01-15 17:10:42 +00:00
m.init(this);
2019-01-14 15:14:22 +00:00
const res = m.install();
if (res != null) {
2019-01-15 03:01:58 +00:00
if (res.mentionHook) this.mentionHooks.push(res.mentionHook);
if (res.contextHook) this.contextHooks[m.name] = res.contextHook;
2019-02-01 17:06:51 +00:00
if (res.timeoutCallback) this.timeoutCallbacks[m.name] = res.timeoutCallback;
2019-01-14 15:14:22 +00:00
}
});
2019-02-01 17:06:51 +00:00
// タイマー監視
this.crawleTimer();
setInterval(this.crawleTimer, 1000);
2020-08-29 06:52:33 +00:00
setInterval(this.logWaking, 10000);
2019-01-14 15:14:22 +00:00
this.log(chalk.green.bold('Ai am now running!'));
2018-08-13 21:14:47 +00:00
}
2019-01-15 17:48:14 +00:00
/**
*
* ()
*/
2019-01-14 15:14:22 +00:00
@autobind
2019-01-15 09:47:22 +00:00
private async onReceiveMessage(msg: Message): Promise<void> {
2019-01-15 03:29:11 +00:00
this.log(chalk.gray(`<<< An message received: ${chalk.underline(msg.id)}`));
2018-08-11 06:26:25 +00:00
2019-01-15 09:52:20 +00:00
// Ignore message if the user is a bot
// To avoid infinity reply loop.
if (msg.user.isBot) {
return;
}
const isNoContext = msg.replyId == null;
2019-01-15 09:34:42 +00:00
// Look up the context
const context = isNoContext ? null : this.contexts.findOne({
noteId: msg.replyId
2018-08-11 06:26:25 +00:00
});
2020-09-02 12:54:01 +00:00
let reaction: string | null = 'love';
2020-10-31 03:18:42 +00:00
let immediate: boolean = false;
2019-01-15 17:48:14 +00:00
//#region
2020-10-31 03:18:42 +00:00
const invokeMentionHooks = async () => {
2020-09-02 12:54:01 +00:00
let res: boolean | HandlerResult | null = null;
2019-01-23 12:49:10 +00:00
for (const handler of this.mentionHooks) {
res = await handler(msg);
if (res === true || typeof res === 'object') break;
}
if (res != null && typeof res === 'object') {
2020-10-31 03:18:42 +00:00
if (res.reaction != null) reaction = res.reaction;
if (res.immediate != null) immediate = res.immediate;
}
};
// コンテキストがあればコンテキストフック呼び出し
// なければそれぞれのモジュールについてフックが引っかかるまで呼び出し
if (context != null) {
const handler = this.contextHooks[context.module];
const res = await handler(context.key, msg, context.data);
if (res != null && typeof res === 'object') {
if (res.reaction != null) reaction = res.reaction;
if (res.immediate != null) immediate = res.immediate;
}
if (res === false) {
await invokeMentionHooks();
}
2020-10-31 03:18:42 +00:00
} else {
await invokeMentionHooks();
}
2019-01-15 17:48:14 +00:00
//#endregion
2020-10-31 03:18:42 +00:00
if (!immediate) {
await delay(1000);
}
2019-01-15 09:47:22 +00:00
// リアクションする
if (reaction) {
this.api('notes/reactions/create', {
noteId: msg.id,
reaction: reaction
2019-01-15 09:47:22 +00:00
});
}
2018-08-11 06:26:25 +00:00
}
@autobind
private onNotification(notification: any) {
switch (notification.type) {
// リアクションされたら親愛度を少し上げる
// TODO: リアクション取り消しをよしなにハンドリングする
case 'reaction': {
const friend = new Friend(this, { user: notification.user });
friend.incLove(0.1);
break;
}
default:
break;
}
}
2019-02-01 17:06:51 +00:00
@autobind
private crawleTimer() {
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.timeoutCallbacks[timer.module](timer.data);
}
}
}
2020-08-29 06:52:33 +00:00
@autobind
private logWaking() {
this.setMeta({
lastWakingAt: Date.now(),
});
}
2019-01-24 00:34:03 +00:00
/**
*
*/
@autobind
public getCollection(name: string, opts?: any): loki.Collection {
let collection: loki.Collection;
collection = this.db.getCollection(name);
if (collection == null) {
collection = this.db.addCollection(name, opts);
}
return collection;
}
2019-02-01 17:06:51 +00:00
@autobind
2020-09-02 11:51:56 +00:00
public lookupFriend(userId: User['id']): Friend | null {
2019-02-01 17:06:51 +00:00
const doc = this.friends.findOne({
userId: userId
});
if (doc == null) return null;
const friend = new Friend(this, { doc: doc });
return friend;
}
2019-05-10 02:55:07 +00:00
/**
*
*/
@autobind
2019-05-12 04:01:08 +00:00
public async upload(file: Buffer | fs.ReadStream, meta: any) {
2019-05-10 02:55:07 +00:00
const res = await request.post({
url: `${config.apiUrl}/drive/files/create`,
formData: {
i: config.i,
2019-05-12 04:01:08 +00:00
file: {
value: file,
options: meta
}
2019-05-10 02:55:07 +00:00
},
json: true
});
return res;
}
2019-01-15 17:48:14 +00:00
/**
* 稿
*/
2019-01-14 15:14:22 +00:00
@autobind
public async post(param: any) {
const res = await this.api('notes/create', param);
return res.createdNote;
2018-08-11 06:26:25 +00:00
}
2019-01-15 17:48:14 +00:00
/**
*
*/
2019-01-14 15:14:22 +00:00
@autobind
public sendMessage(userId: any, param: any) {
return this.api('messaging/messages/create', Object.assign({
2018-08-11 06:26:25 +00:00
userId: userId,
}, param));
}
2019-01-15 17:48:14 +00:00
/**
* APIを呼び出します
*/
2019-01-14 15:14:22 +00:00
@autobind
public api(endpoint: string, param?: any) {
2018-08-11 06:26:25 +00:00
return request.post(`${config.apiUrl}/${endpoint}`, {
json: Object.assign({
i: config.i
}, param)
});
};
2019-01-15 17:48:14 +00:00
/**
*
* @param module 待ち受けるモジュール名
* @param key
* @param id ID稿ID
* @param data
*/
2019-01-14 15:14:22 +00:00
@autobind
public subscribeReply(module: Module, key: string | null, id: string, data?: any) {
this.contexts.insertOne({
noteId: id,
module: module.name,
key: key,
2018-08-26 21:59:18 +00:00
data: data
});
}
2019-01-15 17:48:14 +00:00
/**
*
* @param module 解除するモジュール名
* @param key
*/
2019-01-14 15:14:22 +00:00
@autobind
2020-09-02 12:54:01 +00:00
public unsubscribeReply(module: Module, key: string | null) {
2018-08-26 21:16:56 +00:00
this.contexts.findAndRemove({
key: key,
module: module.name
});
}
2019-02-01 17:06:51 +00:00
/**
*
*
* @param module モジュール名
* @param delay
* @param data
*/
@autobind
public setTimeoutWithPersistence(module: Module, delay: number, data?: any) {
const id = uuid();
this.timers.insertOne({
id: id,
module: module.name,
insertedAt: Date.now(),
delay: delay,
data: data
});
this.log(`Timer persisted: ${module.name} ${id} ${delay}ms`);
}
2020-08-29 06:52:33 +00:00
@autobind
public getMeta() {
const rec = this.meta.findOne();
if (rec) {
return rec;
} else {
const initial: Meta = {
lastWakingAt: Date.now(),
};
this.meta.insertOne(initial);
return initial;
}
}
@autobind
public setMeta(meta: Partial<Meta>) {
const rec = this.getMeta();
for (const [k, v] of Object.entries(meta)) {
rec[k] = v;
}
this.meta.update(rec);
}
2018-08-11 06:26:25 +00:00
}