This commit is contained in:
Take-John 2024-03-26 00:15:57 +09:00 committed by GitHub
commit ed2e3b5ff8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 154 additions and 32 deletions

100
src/ai.ts
View file

@ -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<boolean | HandlerResult>;
type ContextHook = (key: any, msg: Message, data?: any) => Promise<void | boolean | HandlerResult>;
@ -40,20 +41,31 @@ export type Meta = {
/**
*
*/
export default class {
export default interface extends Ai {
connection: Stream;
lastSleepedAt: number;
friends: loki.Collection<FriendDoc>;
moduleData: loki.Collection<any>;
}
/**
*
*/
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<Meta>;
private meta?: loki.Collection<Meta>;
private contexts: loki.Collection<{
private contexts?: loki.Collection<{
noteId?: string;
userId?: string;
module: string;
@ -61,7 +73,7 @@ export default class 藍 {
data?: any;
}>;
private timers: loki.Collection<{
private timers?: loki.Collection<{
id: string;
module: string;
insertedAt: number;
@ -69,8 +81,10 @@ 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;
/**
*
@ -137,6 +151,9 @@ export default class 藍 {
// Init stream
this.connection = new Stream();
// この時点から藍インスタンスに
this.setReady();
//#region Main stream
const mainStream = this.connection.useSharedConnection('main');
@ -204,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<void> {
this.requireReady();
this.log(chalk.gray(`<<< An message received: ${chalk.underline(msg.id)}`));
// Ignore message if the user is a bot
@ -221,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
});
@ -277,6 +322,8 @@ export default class 藍 {
@bindThis
private onNotification(notification: any) {
this.requireReady();
switch (notification.type) {
// リアクションされたら親愛度を少し上げる
// TODO: リアクション取り消しをよしなにハンドリングする
@ -293,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);
}
}
@ -329,6 +378,8 @@ export default class 藍 {
@bindThis
public lookupFriend(userId: User['id']): Friend | null {
this.requireReady();
const doc = this.friends.findOne({
userId: userId
});
@ -361,7 +412,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 +431,13 @@ export default class 藍 {
* APIを呼び出します
*/
@bindThis
public api(endpoint: string, param?: any) {
public api<ReturnType = unknown>(endpoint: string, param?: any) {
this.log(`API: ${endpoint}`);
return got.post(`${config.apiUrl}/${endpoint}`, {
json: Object.assign({
i: config.i
}, param)
}).json();
}).json<ReturnType>();
};
/**
@ -398,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,
@ -413,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
});
@ -428,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(),
@ -442,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) {
@ -464,6 +523,11 @@ export default class 藍 {
rec[k] = v;
}
this.meta.update(rec);
this.meta!.update(rec);
}
}
// FIXME:
// JS にコンパイルされたコードでインターフェイスであるはずの藍がインポートされてしまうので、
// 同名のクラスを定義することで実行時エラーが出ないようにしている
export default class {}

View file

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

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' };
@ -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<User>().catch(retry);
}, {
retries: 3
}).then(account => {
@ -71,7 +72,7 @@ promiseRetry(retry => {
log('Starting AiOS...');
// 藍起動
new (account, [
new Ai(account, [
new CoreModule(),
new EmojiModule(),
new EmojiReactModule(),

View file

@ -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<User>('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<Note>;
public async reply(text: string | null, opts?: {
file?: any;
cw?: string;
renote?: string;
immediate?: boolean;
}): Promise<void>;
@bindThis
public async reply(text: string | null, opts?: {
file?: any;
cw?: string;
renote?: string;
immediate?: boolean;
}) {
}): Promise<Note | void> {
if (text == null) return;
this.ai.log(`>>> Sending reply to ${chalk.underline(this.id)}`);

View file

@ -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}`);

View file

@ -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<T> = {
local: T,
remote: T
};
type UserFollowing = LocalRemotePair<{
followers: {
total: number[]
}
}>;
type Notes = LocalRemotePair<UserNotes>
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<UserNotes>('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<UserFollowing>('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<Notes>('charts/notes', {
span: 'day',
limit: 30,
});

View file

@ -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<LocalTimeline>('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);
}