mirror of
https://github.com/syuilo/ai.git
synced 2024-11-23 13:38:00 +00:00
Merge 1ca93c4edf
into 830c9c2ecd
This commit is contained in:
commit
f078a88547
100
src/ai.ts
100
src/ai.ts
|
@ -17,6 +17,7 @@ import Stream from '@/stream.js';
|
||||||
import log from '@/utils/log.js';
|
import log from '@/utils/log.js';
|
||||||
import { sleep } from './utils/sleep.js';
|
import { sleep } from './utils/sleep.js';
|
||||||
import pkg from '../package.json' assert { type: 'json' };
|
import pkg from '../package.json' assert { type: 'json' };
|
||||||
|
import { Note } from '@/misskey/note.js';
|
||||||
|
|
||||||
type MentionHook = (msg: Message) => Promise<boolean | HandlerResult>;
|
type MentionHook = (msg: Message) => Promise<boolean | HandlerResult>;
|
||||||
type ContextHook = (key: any, msg: Message, data?: any) => Promise<void | 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 readonly version = pkg._v;
|
||||||
public account: User;
|
public account: User;
|
||||||
public connection: Stream;
|
public connection?: Stream;
|
||||||
public modules: Module[] = [];
|
public modules: Module[] = [];
|
||||||
private mentionHooks: MentionHook[] = [];
|
private mentionHooks: MentionHook[] = [];
|
||||||
private contextHooks: { [moduleName: string]: ContextHook } = {};
|
private contextHooks: { [moduleName: string]: ContextHook } = {};
|
||||||
private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {};
|
private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {};
|
||||||
public db: loki;
|
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;
|
noteId?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
module: string;
|
module: string;
|
||||||
|
@ -61,7 +73,7 @@ export default class 藍 {
|
||||||
data?: any;
|
data?: any;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
private timers: loki.Collection<{
|
private timers?: loki.Collection<{
|
||||||
id: string;
|
id: string;
|
||||||
module: string;
|
module: string;
|
||||||
insertedAt: number;
|
insertedAt: number;
|
||||||
|
@ -69,8 +81,10 @@ export default class 藍 {
|
||||||
data?: any;
|
data?: any;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
public friends: loki.Collection<FriendDoc>;
|
public friends?: loki.Collection<FriendDoc>;
|
||||||
public moduleData: loki.Collection<any>;
|
public moduleData?: loki.Collection<any>;
|
||||||
|
|
||||||
|
private ready: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 藍インスタンスを生成します
|
* 藍インスタンスを生成します
|
||||||
|
@ -137,6 +151,9 @@ export default class 藍 {
|
||||||
// Init stream
|
// Init stream
|
||||||
this.connection = new Stream();
|
this.connection = new Stream();
|
||||||
|
|
||||||
|
// この時点から藍インスタンスに
|
||||||
|
this.setReady();
|
||||||
|
|
||||||
//#region Main stream
|
//#region Main stream
|
||||||
const mainStream = this.connection.useSharedConnection('main');
|
const mainStream = this.connection.useSharedConnection('main');
|
||||||
|
|
||||||
|
@ -204,12 +221,40 @@ export default class 藍 {
|
||||||
this.log(chalk.green.bold('Ai am now running!'));
|
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
|
@bindThis
|
||||||
private async onReceiveMessage(msg: Message): Promise<void> {
|
private async onReceiveMessage(msg: Message): Promise<void> {
|
||||||
|
this.requireReady();
|
||||||
|
|
||||||
this.log(chalk.gray(`<<< An message received: ${chalk.underline(msg.id)}`));
|
this.log(chalk.gray(`<<< An message received: ${chalk.underline(msg.id)}`));
|
||||||
|
|
||||||
// Ignore message if the user is a bot
|
// Ignore message if the user is a bot
|
||||||
|
@ -221,7 +266,7 @@ export default class 藍 {
|
||||||
const isNoContext = msg.replyId == null;
|
const isNoContext = msg.replyId == null;
|
||||||
|
|
||||||
// Look up the context
|
// Look up the context
|
||||||
const context = isNoContext ? null : this.contexts.findOne({
|
const context = isNoContext ? null : this.contexts!.findOne({
|
||||||
noteId: msg.replyId
|
noteId: msg.replyId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -277,6 +322,8 @@ export default class 藍 {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private onNotification(notification: any) {
|
private onNotification(notification: any) {
|
||||||
|
this.requireReady();
|
||||||
|
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
// リアクションされたら親愛度を少し上げる
|
// リアクションされたら親愛度を少し上げる
|
||||||
// TODO: リアクション取り消しをよしなにハンドリングする
|
// TODO: リアクション取り消しをよしなにハンドリングする
|
||||||
|
@ -293,12 +340,14 @@ export default class 藍 {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private crawleTimer() {
|
private crawleTimer() {
|
||||||
const timers = this.timers.find();
|
this.requireReady();
|
||||||
|
|
||||||
|
const timers = this.timers!.find();
|
||||||
for (const timer of timers) {
|
for (const timer of timers) {
|
||||||
// タイマーが時間切れかどうか
|
// タイマーが時間切れかどうか
|
||||||
if (Date.now() - (timer.insertedAt + timer.delay) >= 0) {
|
if (Date.now() - (timer.insertedAt + timer.delay) >= 0) {
|
||||||
this.log(`Timer expired: ${timer.module} ${timer.id}`);
|
this.log(`Timer expired: ${timer.module} ${timer.id}`);
|
||||||
this.timers.remove(timer);
|
this.timers!.remove(timer);
|
||||||
this.timeoutCallbacks[timer.module](timer.data);
|
this.timeoutCallbacks[timer.module](timer.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -329,6 +378,8 @@ export default class 藍 {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public lookupFriend(userId: User['id']): Friend | null {
|
public lookupFriend(userId: User['id']): Friend | null {
|
||||||
|
this.requireReady();
|
||||||
|
|
||||||
const doc = this.friends.findOne({
|
const doc = this.friends.findOne({
|
||||||
userId: userId
|
userId: userId
|
||||||
});
|
});
|
||||||
|
@ -361,7 +412,7 @@ export default class 藍 {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async post(param: any) {
|
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;
|
return res.createdNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,13 +431,13 @@ export default class 藍 {
|
||||||
* APIを呼び出します
|
* APIを呼び出します
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public api(endpoint: string, param?: any) {
|
public api<ReturnType = unknown>(endpoint: string, param?: any) {
|
||||||
this.log(`API: ${endpoint}`);
|
this.log(`API: ${endpoint}`);
|
||||||
return got.post(`${config.apiUrl}/${endpoint}`, {
|
return got.post(`${config.apiUrl}/${endpoint}`, {
|
||||||
json: Object.assign({
|
json: Object.assign({
|
||||||
i: config.i
|
i: config.i
|
||||||
}, param)
|
}, param)
|
||||||
}).json();
|
}).json<ReturnType>();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -398,7 +449,8 @@ export default class 藍 {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public subscribeReply(module: Module, key: string | null, id: string, data?: any) {
|
public subscribeReply(module: Module, key: string | null, id: string, data?: any) {
|
||||||
this.contexts.insertOne({
|
this.requireReady();
|
||||||
|
this.contexts!.insertOne({
|
||||||
noteId: id,
|
noteId: id,
|
||||||
module: module.name,
|
module: module.name,
|
||||||
key: key,
|
key: key,
|
||||||
|
@ -413,7 +465,8 @@ export default class 藍 {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public unsubscribeReply(module: Module, key: string | null) {
|
public unsubscribeReply(module: Module, key: string | null) {
|
||||||
this.contexts.findAndRemove({
|
this.requireReady();
|
||||||
|
this.contexts!.findAndRemove({
|
||||||
key: key,
|
key: key,
|
||||||
module: module.name
|
module: module.name
|
||||||
});
|
});
|
||||||
|
@ -428,8 +481,10 @@ export default class 藍 {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public setTimeoutWithPersistence(module: Module, delay: number, data?: any) {
|
public setTimeoutWithPersistence(module: Module, delay: number, data?: any) {
|
||||||
|
this.requireReady();
|
||||||
|
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
this.timers.insertOne({
|
this.timers!.insertOne({
|
||||||
id: id,
|
id: id,
|
||||||
module: module.name,
|
module: module.name,
|
||||||
insertedAt: Date.now(),
|
insertedAt: Date.now(),
|
||||||
|
@ -442,6 +497,10 @@ export default class 藍 {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getMeta() {
|
public getMeta() {
|
||||||
|
if (this.meta == null) {
|
||||||
|
throw new TypeError('meta has not been set');
|
||||||
|
}
|
||||||
|
|
||||||
const rec = this.meta.findOne();
|
const rec = this.meta.findOne();
|
||||||
|
|
||||||
if (rec) {
|
if (rec) {
|
||||||
|
@ -464,6 +523,11 @@ export default class 藍 {
|
||||||
rec[k] = v;
|
rec[k] = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.meta.update(rec);
|
this.meta!.update(rec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME:
|
||||||
|
// JS にコンパイルされたコードでインターフェイスであるはずの藍がインポートされてしまうので、
|
||||||
|
// 同名のクラスを定義することで実行時エラーが出ないようにしている
|
||||||
|
export default class 藍 {}
|
||||||
|
|
|
@ -17,9 +17,16 @@ type Config = {
|
||||||
memoryDir?: string;
|
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.wsUrl = config.host.replace('http', 'ws');
|
||||||
config.apiUrl = config.host + '/api';
|
config.apiUrl = config.host + '/api';
|
||||||
|
|
||||||
export default config as Config;
|
export default config;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import chalk from 'chalk';
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
import promiseRetry from 'promise-retry';
|
import promiseRetry from 'promise-retry';
|
||||||
|
|
||||||
import 藍 from './ai.js';
|
import { Ai } from './ai.js';
|
||||||
import config from './config.js';
|
import config from './config.js';
|
||||||
import _log from './utils/log.js';
|
import _log from './utils/log.js';
|
||||||
import pkg from '../package.json' assert { type: 'json' };
|
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 PollModule from './modules/poll/index.js';
|
||||||
import ReminderModule from './modules/reminder/index.js';
|
import ReminderModule from './modules/reminder/index.js';
|
||||||
import CheckCustomEmojisModule from './modules/check-custom-emojis/index.js';
|
import CheckCustomEmojisModule from './modules/check-custom-emojis/index.js';
|
||||||
|
import { User } from '@/misskey/user.js';
|
||||||
|
|
||||||
console.log(' __ ____ _____ ___ ');
|
console.log(' __ ____ _____ ___ ');
|
||||||
console.log(' /__\\ (_ _)( _ )/ __)');
|
console.log(' /__\\ (_ _)( _ )/ __)');
|
||||||
|
@ -61,7 +62,7 @@ promiseRetry(retry => {
|
||||||
json: {
|
json: {
|
||||||
i: config.i
|
i: config.i
|
||||||
}
|
}
|
||||||
}).json().catch(retry);
|
}).json<User>().catch(retry);
|
||||||
}, {
|
}, {
|
||||||
retries: 3
|
retries: 3
|
||||||
}).then(account => {
|
}).then(account => {
|
||||||
|
@ -71,7 +72,7 @@ promiseRetry(retry => {
|
||||||
log('Starting AiOS...');
|
log('Starting AiOS...');
|
||||||
|
|
||||||
// 藍起動
|
// 藍起動
|
||||||
new 藍(account, [
|
new Ai(account, [
|
||||||
new CoreModule(),
|
new CoreModule(),
|
||||||
new EmojiModule(),
|
new EmojiModule(),
|
||||||
new EmojiReactModule(),
|
new EmojiReactModule(),
|
||||||
|
|
|
@ -8,6 +8,7 @@ import includes from '@/utils/includes.js';
|
||||||
import or from '@/utils/or.js';
|
import or from '@/utils/or.js';
|
||||||
import config from '@/config.js';
|
import config from '@/config.js';
|
||||||
import { sleep } from '@/utils/sleep.js';
|
import { sleep } from '@/utils/sleep.js';
|
||||||
|
import { Note } from '@/misskey/note.js';
|
||||||
|
|
||||||
export default class Message {
|
export default class Message {
|
||||||
private ai: 藍;
|
private ai: 藍;
|
||||||
|
@ -61,20 +62,34 @@ export default class Message {
|
||||||
this.friend = new Friend(ai, { user: this.user });
|
this.friend = new Friend(ai, { user: this.user });
|
||||||
|
|
||||||
// メッセージなどに付いているユーザー情報は省略されている場合があるので完全なユーザー情報を持ってくる
|
// メッセージなどに付いているユーザー情報は省略されている場合があるので完全なユーザー情報を持ってくる
|
||||||
this.ai.api('users/show', {
|
this.ai.api<User>('users/show', {
|
||||||
userId: this.userId
|
userId: this.userId
|
||||||
}).then(user => {
|
}).then(user => {
|
||||||
this.friend.updateUser(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
|
@bindThis
|
||||||
public async reply(text: string | null, opts?: {
|
public async reply(text: string | null, opts?: {
|
||||||
file?: any;
|
file?: any;
|
||||||
cw?: string;
|
cw?: string;
|
||||||
renote?: string;
|
renote?: string;
|
||||||
immediate?: boolean;
|
immediate?: boolean;
|
||||||
}) {
|
}): Promise<Note | void> {
|
||||||
if (text == null) return;
|
if (text == null) return;
|
||||||
|
|
||||||
this.ai.log(`>>> Sending reply to ${chalk.underline(this.id)}`);
|
this.ai.log(`>>> Sending reply to ${chalk.underline(this.id)}`);
|
||||||
|
|
|
@ -6,6 +6,27 @@ import { renderChart } from './render-chart.js';
|
||||||
import { items } from '@/vocabulary.js';
|
import { items } from '@/vocabulary.js';
|
||||||
import config from '@/config.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 {
|
export default class extends Module {
|
||||||
public readonly name = 'chart';
|
public readonly name = 'chart';
|
||||||
|
|
||||||
|
@ -48,7 +69,7 @@ export default class extends Module {
|
||||||
let chart;
|
let chart;
|
||||||
|
|
||||||
if (type === 'userNotes') {
|
if (type === 'userNotes') {
|
||||||
const data = await this.ai.api('charts/user/notes', {
|
const data = await this.ai.api<UserNotes>('charts/user/notes', {
|
||||||
span: 'day',
|
span: 'day',
|
||||||
limit: 30,
|
limit: 30,
|
||||||
userId: params.user.id
|
userId: params.user.id
|
||||||
|
@ -65,7 +86,7 @@ export default class extends Module {
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
} else if (type === 'followers') {
|
} 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',
|
span: 'day',
|
||||||
limit: 30,
|
limit: 30,
|
||||||
userId: params.user.id
|
userId: params.user.id
|
||||||
|
@ -80,7 +101,7 @@ export default class extends Module {
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
} else if (type === 'notes') {
|
} else if (type === 'notes') {
|
||||||
const data = await this.ai.api('charts/notes', {
|
const data = await this.ai.api<Notes>('charts/notes', {
|
||||||
span: 'day',
|
span: 'day',
|
||||||
limit: 30,
|
limit: 30,
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,12 @@ import config from '@/config.js';
|
||||||
import serifs from '@/serifs.js';
|
import serifs from '@/serifs.js';
|
||||||
import { mecab } from './mecab.js';
|
import { mecab } from './mecab.js';
|
||||||
|
|
||||||
|
type LocalTimeline = {
|
||||||
|
userId: string;
|
||||||
|
text: string | null;
|
||||||
|
cw: string | null;
|
||||||
|
}[];
|
||||||
|
|
||||||
function kanaToHira(str: string) {
|
function kanaToHira(str: string) {
|
||||||
return str.replace(/[\u30a1-\u30f6]/g, match => {
|
return str.replace(/[\u30a1-\u30f6]/g, match => {
|
||||||
const chr = match.charCodeAt(0) - 0x60;
|
const chr = match.charCodeAt(0) - 0x60;
|
||||||
|
@ -35,7 +41,7 @@ export default class extends Module {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async learn() {
|
private async learn() {
|
||||||
const tl = await this.ai.api('notes/local-timeline', {
|
const tl = await this.ai.api<LocalTimeline>('notes/local-timeline', {
|
||||||
limit: 30
|
limit: 30
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -47,7 +53,8 @@ export default class extends Module {
|
||||||
let keywords: string[][] = [];
|
let keywords: string[][] = [];
|
||||||
|
|
||||||
for (const note of interestedNotes) {
|
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);
|
const keywordsInThisNote = tokens.filter(token => token[2] == '固有名詞' && token[8] != null);
|
||||||
keywords = keywords.concat(keywordsInThisNote);
|
keywords = keywords.concat(keywordsInThisNote);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue