This commit is contained in:
Take-John 2024-03-30 00:58:05 +09:00 committed by GitHub
commit 6f267b608a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 483 additions and 128 deletions

110
src/ai.ts
View file

@ -9,7 +9,7 @@ import chalk from 'chalk';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import config from '@/config.js'; import config from '@/config.js';
import Module from '@/module.js'; import Module, { InstalledModule } from '@/module.js';
import Message from '@/message.js'; import Message from '@/message.js';
import Friend, { FriendDoc } from '@/friend.js'; import Friend, { FriendDoc } from '@/friend.js';
import type { User } from '@/misskey/user.js'; import type { User } from '@/misskey/user.js';
@ -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>;
@ -37,23 +38,40 @@ export type Meta = {
lastWakingAt: number; lastWakingAt: number;
}; };
export type ModuleDataDoc<Data = any> = {
module: string;
data: Data;
}
/** /**
* *
*/ */
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 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 installedModules: { [moduleName: string]: InstalledModule } = {};
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 +79,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 +87,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 +157,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');
@ -187,7 +210,7 @@ export default class 藍 {
this.modules.forEach(m => { this.modules.forEach(m => {
this.log(`Installing ${chalk.cyan.italic(m.name)}\tmodule...`); this.log(`Installing ${chalk.cyan.italic(m.name)}\tmodule...`);
m.init(this); m.init(this);
const res = m.install(); const res = m.install(this);
if (res != null) { if (res != null) {
if (res.mentionHook) this.mentionHooks.push(res.mentionHook); if (res.mentionHook) this.mentionHooks.push(res.mentionHook);
if (res.contextHook) this.contextHooks[m.name] = res.contextHook; if (res.contextHook) this.contextHooks[m.name] = res.contextHook;
@ -204,12 +227,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 +272,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 +328,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 +346,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 +384,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 +418,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 +437,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 +455,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 +471,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 +487,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 +503,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 +529,11 @@ export default class 藍 {
rec[k] = v; rec[k] = v;
} }
this.meta.update(rec); this.meta!.update(rec);
} }
} }
// FIXME:
// JS にコンパイルされたコードでインターフェイスであるはずの藍がインポートされてしまうので、
// 同名のクラスを定義することで実行時エラーが出ないようにしている
export default class {}

View file

@ -17,9 +17,73 @@ type Config = {
memoryDir?: string; memoryDir?: string;
}; };
import config from '../config.json' assert { type: 'json' }; import uncheckedConfig from '../config.json' assert { type: 'json' };
import log from '@/utils/log.js';
config.wsUrl = config.host.replace('http', 'ws'); class Type<T> {
config.apiUrl = config.host + '/api'; public static readonly string = new Type<string>('string');
public static readonly boolean = new Type<boolean>('boolean');
export default config as Config; public readonly name: string;
private constructor(name: string) {
this.name = name;
}
check(value: unknown): value is T {
return typeof value == this.name;
}
}
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;
}
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;
}
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] } {
(config as { [L in K]?: Config[K] })[key] = value;
}
function validate(config: unknown): Config {
if (!(config instanceof Object)) {
log('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)
) {
setProperty(config, 'wsUrl', config.host.replace('http', 'ws'));
setProperty(config, 'apiUrl', config.host + '/api');
return config;
}
throw new TypeError('config.json has an invalid type');
}
const config = validate(uncheckedConfig);
export default config;

View file

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

View file

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

View file

@ -1,29 +1,36 @@
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import , { InstallerResult } from '@/ai.js'; import , { HandlerResult, InstallerResult, ModuleDataDoc } from '@/ai.js';
import Message from '@/message.js';
export default abstract class Module { export default abstract class Module {
public abstract readonly name: string; public abstract readonly name: string;
protected ai: ; private maybeAi?: ;
private doc: any;
/**
* @deprecated
*/
public installed?: InstalledModule;
public init(ai: ) { public init(ai: ) {
this.ai = ai; this.maybeAi = ai;
this.doc = this.ai.moduleData.findOne({
module: this.name
});
if (this.doc == null) {
this.doc = this.ai.moduleData.insertOne({
module: this.name,
data: {}
});
}
} }
public abstract install(): InstallerResult; public abstract install(ai: ): InstallerResult;
/**
* @deprecated {@link Module#install} 使
*/
protected get ai(): {
if (this.maybeAi == null) {
throw new TypeError('This module has not been initialized');
}
return this.maybeAi;
}
/**
* @deprecated {@link InstalledModule#log} 使
*/
@bindThis @bindThis
protected log(msg: string) { protected log(msg: string) {
this.ai.log(`[${this.name}]: ${msg}`); this.ai.log(`[${this.name}]: ${msg}`);
@ -34,6 +41,7 @@ export default abstract class Module {
* @param key * @param key
* @param id ID稿ID * @param id ID稿ID
* @param data * @param data
* @deprecated {@link InstalledModule#subscribeReply} 使
*/ */
@bindThis @bindThis
protected subscribeReply(key: string | null, id: string, data?: any) { protected subscribeReply(key: string | null, id: string, data?: any) {
@ -43,6 +51,7 @@ export default abstract class Module {
/** /**
* *
* @param key * @param key
* @deprecated {@link InstalledModule#unsubscribeReply} 使
*/ */
@bindThis @bindThis
protected unsubscribeReply(key: string | null) { protected unsubscribeReply(key: string | null) {
@ -54,20 +63,132 @@ export default abstract class Module {
* *
* @param delay * @param delay
* @param data * @param data
* @deprecated {@link InstalledModule#setTimeoutWithPersistence} 使
*/ */
@bindThis @bindThis
public setTimeoutWithPersistence(delay: number, data?: any) { public setTimeoutWithPersistence(delay: number, data?: any) {
this.ai.setTimeoutWithPersistence(this, delay, data); this.ai.setTimeoutWithPersistence(this, delay, data);
} }
/**
* @deprecated {@link InstalledModule#getData} 使
*/
@bindThis @bindThis
protected getData() { protected getData() {
const doc = this.ai.moduleData.findOne({
module: this.name
});
return doc?.data;
}
/**
* @deprecated {@link InstalledModule#setData} 使
*/
@bindThis
protected setData(data: any) {
const doc = this.ai.moduleData.findOne({
module: this.name
});
if (doc == null) {
return;
}
doc.data = data;
this.ai.moduleData.update(doc);
if (this.installed != null) {
this.installed.updateDoc();
}
}
}
export abstract class InstalledModule<M extends Module = Module, Data = any> implements InstallerResult {
protected readonly module: M;
protected readonly ai: ;
private doc: ModuleDataDoc<Data>;
constructor(module: M, ai: , initialData: any = {}) {
this.module = module;
this.ai = ai;
const doc = this.ai.moduleData.findOne({
module: module.name
});
if (doc == null) {
this.doc = this.ai.moduleData.insertOne({
module: module.name,
data: initialData
}) as ModuleDataDoc<Data>;
} else {
this.doc = doc;
}
ai.installedModules[module.name] = this;
}
@bindThis
protected log(msg: string) {
this.ai.log(`[${this.module.name}]: ${msg}`);
}
/**
*
* @param key
* @param id ID稿ID
* @param data
*/
@bindThis
protected subscribeReply(key: string | null, id: string, data?: any) {
this.ai.subscribeReply(this.module, key, id, data);
}
/**
*
* @param key
*/
@bindThis
protected unsubscribeReply(key: string | null) {
this.ai.unsubscribeReply(this.module, key);
}
/**
*
*
* @param delay
* @param data
*/
@bindThis
public setTimeoutWithPersistence(delay: number, data?: any) {
this.ai.setTimeoutWithPersistence(this.module, delay, data);
}
@bindThis
protected getData(): Data {
return this.doc.data; return this.doc.data;
} }
@bindThis @bindThis
protected setData(data: any) { protected setData(data: Data) {
this.doc.data = data; this.doc.data = data;
this.ai.moduleData.update(this.doc); this.ai.moduleData.update(this.doc);
} }
/**
* @deprecated
*/
public updateDoc() {
const doc = this.ai.moduleData.findOne({
module: this.module.name
});
if (doc != null) {
this.doc = doc;
}
}
mentionHook?(msg: Message): Promise<boolean | HandlerResult>;
contextHook?(key: any, msg: Message, data?: any): Promise<void | boolean | HandlerResult>;
timeoutCallback?(data?: any): void;
} }

View file

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

View file

@ -1,31 +1,35 @@
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import loki from 'lokijs'; import loki from 'lokijs';
import Module from '@/module.js'; import Module, { InstalledModule } from '@/module.js';
import serifs from '@/serifs.js'; import serifs from '@/serifs.js';
import config from '@/config.js'; import config from '@/config.js';
import Message from '@/message.js'; import Message from '@/message.js';
import from '@/ai.js';
export default class extends Module { export default class extends Module {
public readonly name = 'checkCustomEmojis'; public readonly name = 'checkCustomEmojis';
@bindThis
public install(ai: ) {
if (!config.checkEmojisEnabled) return {};
return new Installed(this, ai);
}
}
class Installed extends InstalledModule {
private lastEmoji: loki.Collection<{ private lastEmoji: loki.Collection<{
id: string; id: string;
updatedAt: number; updatedAt: number;
}>; }>;
@bindThis constructor(module: Module, ai: ) {
public install() { super(module, ai);
if (!config.checkEmojisEnabled) return {};
this.lastEmoji = this.ai.getCollection('lastEmoji', { this.lastEmoji = this.ai.getCollection('lastEmoji', {
indices: ['id'] indices: ['id']
}); });
this.timeCheck(); this.timeCheck();
setInterval(this.timeCheck, 1000 * 60 * 3); setInterval(this.timeCheck, 1000 * 60 * 3);
return {
mentionHook: this.mentionHook
};
} }
@bindThis @bindThis
@ -141,7 +145,7 @@ export default class extends Module {
} }
@bindThis @bindThis
private async mentionHook(msg: Message) { public async mentionHook(msg: Message) {
if (!msg.includes(['カスタムえもじチェック','カスタムえもじを調べて','カスタムえもじを確認'])) { if (!msg.includes(['カスタムえもじチェック','カスタムえもじを調べて','カスタムえもじを確認'])) {
return false; return false;
} else { } else {

View file

@ -3,19 +3,16 @@ import { parse } from 'twemoji-parser';
import type { Note } from '@/misskey/note.js'; import type { Note } from '@/misskey/note.js';
import Module from '@/module.js'; import Module from '@/module.js';
import Stream from '@/stream.js';
import includes from '@/utils/includes.js'; import includes from '@/utils/includes.js';
import { sleep } from '@/utils/sleep.js'; import { sleep } from '@/utils/sleep.js';
export default class extends Module { export default class extends Module {
public readonly name = 'emoji-react'; public readonly name = 'emoji-react';
private htl: ReturnType<Stream['useSharedConnection']>;
@bindThis @bindThis
public install() { public install() {
this.htl = this.ai.connection.useSharedConnection('homeTimeline'); const htl = this.ai.connection.useSharedConnection('homeTimeline');
this.htl.on('note', this.onNote); htl.on('note', this.onNote);
return {}; return {};
} }

View file

@ -1,35 +1,42 @@
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import loki from 'lokijs'; import loki from 'lokijs';
import Module from '@/module.js'; import Module, { InstalledModule } from '@/module.js';
import Message from '@/message.js'; import Message from '@/message.js';
import serifs from '@/serifs.js'; import serifs from '@/serifs.js';
import , { InstallerResult } from '@/ai.js';
type Guesses = loki.Collection<{
userId: string;
secret: number;
tries: number[];
isEnded: boolean;
startedAt: number;
endedAt: number | null;
}>
export default class extends Module { export default class extends Module {
public readonly name = 'guessingGame'; public readonly name = 'guessingGame';
private guesses: loki.Collection<{
userId: string;
secret: number;
tries: number[];
isEnded: boolean;
startedAt: number;
endedAt: number | null;
}>;
@bindThis @bindThis
public install() { public install(ai: ) {
this.guesses = this.ai.getCollection('guessingGame', { const guesses = ai.getCollection('guessingGame', {
indices: ['userId'] indices: ['userId']
}); });
return { return new Installed(this, ai, guesses);
mentionHook: this.mentionHook, }
contextHook: this.contextHook }
};
class Installed extends InstalledModule implements InstallerResult {
private guesses: Guesses;
constructor(module: Module, ai: , guesses: Guesses) {
super(module, ai);
this.guesses = guesses;
} }
@bindThis @bindThis
private async mentionHook(msg: Message) { public async mentionHook(msg: Message) {
if (!msg.includes(['数当て', '数あて'])) return false; if (!msg.includes(['数当て', '数あて'])) return false;
const exist = this.guesses.findOne({ const exist = this.guesses.findOne({
@ -56,7 +63,7 @@ export default class extends Module {
} }
@bindThis @bindThis
private async contextHook(key: any, msg: Message) { public async contextHook(key: any, msg: Message) {
if (msg.text == null) return; if (msg.text == null) return;
const exist = this.guesses.findOne({ const exist = this.guesses.findOne({
@ -114,14 +121,14 @@ export default class extends Module {
if (end) { if (end) {
exist.isEnded = true; exist.isEnded = true;
exist.endedAt = Date.now(); exist.endedAt = Date.now();
this.unsubscribeReply(key); this.ai.unsubscribeReply(this.module, key);
} }
this.guesses.update(exist); this.guesses.update(exist);
msg.reply(text).then(reply => { msg.reply(text).then(reply => {
if (!end) { if (!end) {
this.subscribeReply(msg.userId, reply.id); this.ai.subscribeReply(this.module, msg.userId, reply.id);
} }
}); });
} }

View file

@ -1,10 +1,11 @@
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import loki from 'lokijs'; import loki from 'lokijs';
import Module from '@/module.js'; import Module, { InstalledModule } from '@/module.js';
import Message from '@/message.js'; import Message from '@/message.js';
import serifs from '@/serifs.js'; import serifs from '@/serifs.js';
import type { User } from '@/misskey/user.js'; import type { User } from '@/misskey/user.js';
import { acct } from '@/utils/acct.js'; import { acct } from '@/utils/acct.js';
import , { InstallerResult } from '@/ai.js';
type Game = { type Game = {
votes: { votes: {
@ -25,23 +26,28 @@ const limitMinutes = 10;
export default class extends Module { export default class extends Module {
public readonly name = 'kazutori'; public readonly name = 'kazutori';
@bindThis
public install(ai: ) {
return new Installed(this, ai);
}
}
class Installed extends InstalledModule {
private games: loki.Collection<Game>; private games: loki.Collection<Game>;
@bindThis constructor(module: Module, ai: ) {
public install() { super(module, ai);
this.games = this.ai.getCollection('kazutori'); this.games = this.ai.getCollection('kazutori');
this.crawleGameEnd(); this.crawleGameEnd();
setInterval(this.crawleGameEnd, 1000); setInterval(this.crawleGameEnd, 1000);
return { return this;
mentionHook: this.mentionHook,
contextHook: this.contextHook
};
} }
@bindThis @bindThis
private async mentionHook(msg: Message) { public async mentionHook(msg: Message) {
if (!msg.includes(['数取り'])) return false; if (!msg.includes(['数取り'])) return false;
const games = this.games.find({}); const games = this.games.find({});
@ -83,7 +89,7 @@ export default class extends Module {
} }
@bindThis @bindThis
private async contextHook(key: any, msg: Message) { public async contextHook(key: any, msg: Message) {
if (msg.text == null) return { if (msg.text == null) return {
reaction: 'hmm' reaction: 'hmm'
}; };

View file

@ -1,9 +1,16 @@
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import loki from 'lokijs'; import loki from 'lokijs';
import Module from '@/module.js'; import Module, { InstalledModule } from '@/module.js';
import config from '@/config.js'; 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';
import from '@/ai.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 => {
@ -15,27 +22,33 @@ function kanaToHira(str: string) {
export default class extends Module { export default class extends Module {
public readonly name = 'keyword'; public readonly name = 'keyword';
@bindThis
public install(ai: ) {
if (config.keywordEnabled) {
new Installed(this, ai);
}
return {};
}
}
class Installed extends InstalledModule {
private learnedKeywords: loki.Collection<{ private learnedKeywords: loki.Collection<{
keyword: string; keyword: string;
learnedAt: number; learnedAt: number;
}>; }>;
@bindThis constructor(module: Module, ai: ) {
public install() { super(module, ai);
if (!config.keywordEnabled) return {};
this.learnedKeywords = this.ai.getCollection('_keyword_learnedKeywords', { this.learnedKeywords = this.ai.getCollection('_keyword_learnedKeywords', {
indices: ['userId'] indices: ['userId']
}); });
setInterval(this.learn, 1000 * 60 * 60); setInterval(this.learn, 1000 * 60 * 60);
return {};
} }
@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 +60,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);
} }

View file

@ -1,16 +1,24 @@
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import loki from 'lokijs'; import loki from 'lokijs';
import Module from '@/module.js'; import Module, { InstalledModule } from '@/module.js';
import Message from '@/message.js'; import Message from '@/message.js';
import serifs, { getSerif } from '@/serifs.js'; import serifs, { getSerif } from '@/serifs.js';
import { acct } from '@/utils/acct.js'; import { acct } from '@/utils/acct.js';
import config from '@/config.js'; import config from '@/config.js';
import from '@/ai.js';
const NOTIFY_INTERVAL = 1000 * 60 * 60 * 12; const NOTIFY_INTERVAL = 1000 * 60 * 60 * 12;
export default class extends Module { export default class extends Module {
public readonly name = 'reminder'; public readonly name = 'reminder';
@bindThis
public install(ai: ) {
return new Installed(this, ai);
}
}
class Installed extends InstalledModule {
private reminds: loki.Collection<{ private reminds: loki.Collection<{
userId: string; userId: string;
id: string; id: string;
@ -20,21 +28,15 @@ export default class extends Module {
createdAt: number; createdAt: number;
}>; }>;
@bindThis constructor(module: Module, ai: ) {
public install() { super(module, ai);
this.reminds = this.ai.getCollection('reminds', { this.reminds = this.ai.getCollection('reminds', {
indices: ['userId', 'id'] indices: ['userId', 'id']
}); });
return {
mentionHook: this.mentionHook,
contextHook: this.contextHook,
timeoutCallback: this.timeoutCallback,
};
} }
@bindThis @bindThis
private async mentionHook(msg: Message) { public async mentionHook(msg: Message) {
let text = msg.extractedText.toLowerCase(); let text = msg.extractedText.toLowerCase();
if (!text.startsWith('remind') && !text.startsWith('todo')) return false; if (!text.startsWith('remind') && !text.startsWith('todo')) return false;
@ -99,7 +101,7 @@ export default class extends Module {
} }
@bindThis @bindThis
private async contextHook(key: any, msg: Message, data: any) { public async contextHook(key: any, msg: Message, data: any) {
if (msg.text == null) return; if (msg.text == null) return;
const remind = this.reminds.findOne({ const remind = this.reminds.findOne({
@ -129,7 +131,7 @@ export default class extends Module {
} }
@bindThis @bindThis
private async timeoutCallback(data) { public async timeoutCallback(data) {
const remind = this.reminds.findOne({ const remind = this.reminds.findOne({
id: data.id id: data.id
}); });
@ -147,7 +149,7 @@ export default class extends Module {
renoteId: remind.thing == null && remind.quoteId ? remind.quoteId : remind.id, renoteId: remind.thing == null && remind.quoteId ? remind.quoteId : remind.id,
text: acct(friend.doc.user) + ' ' + serifs.reminder.notify(friend.name) text: acct(friend.doc.user) + ' ' + serifs.reminder.notify(friend.name)
}); });
} catch (err) { } catch (err: any) {
// renote対象が消されていたらリマインダー解除 // renote対象が消されていたらリマインダー解除
if (err.statusCode === 400) { if (err.statusCode === 400) {
this.unsubscribeReply(remind.thing == null && remind.quoteId ? remind.quoteId : remind.id); this.unsubscribeReply(remind.thing == null && remind.quoteId ? remind.quoteId : remind.id);

View file

@ -11,6 +11,7 @@ import * as Reversi from './engine.js';
import config from '@/config.js'; import config from '@/config.js';
import serifs from '@/serifs.js'; import serifs from '@/serifs.js';
import type { User } from '@/misskey/user.js'; import type { User } from '@/misskey/user.js';
import { Note } from '@/misskey/note.js';
function getUserName(user) { function getUserName(user) {
return user.name || user.username; return user.name || user.username;
@ -24,11 +25,35 @@ const titles = [
]; ];
class Session { class Session {
private account: User; private maybeAccount?: User;
private game: any; private game: any;
private form: any; private form: any;
private engine: Reversi.Game; private maybeEngine?: Reversi.Game;
private botColor: Reversi.Color; private maybeBotColor?: Reversi.Color;
private get account(): User {
const maybeAccount = this.maybeAccount;
if (maybeAccount == null) {
throw new Error('Have not received "_init_" message');
}
return maybeAccount;
}
private get engine(): Reversi.Game {
const maybeEngine = this.maybeEngine;
if (maybeEngine == null) {
throw new Error('Have not received "started" message');
}
return maybeEngine;
}
private get botColor(): Reversi.Color {
const maybeBotColor = this.maybeBotColor;
if (maybeBotColor == null) {
throw new Error('Have not received "started" message');
}
return maybeBotColor;
}
private appliedOps: string[] = []; private appliedOps: string[] = [];
@ -100,7 +125,7 @@ class Session {
private onInit = (msg: any) => { private onInit = (msg: any) => {
this.game = msg.game; this.game = msg.game;
this.form = msg.form; this.form = msg.form;
this.account = msg.account; this.maybeAccount = msg.account;
} }
/** /**
@ -121,7 +146,7 @@ class Session {
}); });
// リバーシエンジン初期化 // リバーシエンジン初期化
this.engine = new Reversi.Game(this.game.map, { this.maybeEngine = new Reversi.Game(this.game.map, {
isLlotheo: this.game.isLlotheo, isLlotheo: this.game.isLlotheo,
canPutEverywhere: this.game.canPutEverywhere, canPutEverywhere: this.game.canPutEverywhere,
loopedBoard: this.game.loopedBoard loopedBoard: this.game.loopedBoard
@ -198,7 +223,7 @@ class Session {
//#endregion //#endregion
this.botColor = this.game.user1Id == this.account.id && this.game.black == 1 || this.game.user2Id == this.account.id && this.game.black == 2; this.maybeBotColor = this.game.user1Id == this.account.id && this.game.black == 1 || this.game.user2Id == this.account.id && this.game.black == 2;
if (this.botColor) { if (this.botColor) {
this.think(); this.think();
@ -454,7 +479,7 @@ class Session {
try { try {
const res = await got.post(`${config.host}/api/notes/create`, { const res = await got.post(`${config.host}/api/notes/create`, {
json: body json: body
}).json(); }).json<{ createdNote: Note }>();
return res.createdNote; return res.createdNote;
} catch (e) { } catch (e) {

View file

@ -1,24 +1,34 @@
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import Module from '@/module.js'; import Module, { InstalledModule } from '@/module.js';
import serifs from '@/serifs.js'; import serifs from '@/serifs.js';
import config from '@/config.js'; import config from '@/config.js';
import from '@/ai.js';
export default class extends Module { export default class extends Module {
public readonly name = 'server'; public readonly name = 'server';
@bindThis
public install(ai: ) {
if (config.serverMonitoring) {
new Installed(this, ai);
}
return {};
}
}
class Installed extends InstalledModule {
private connection?: any; private connection?: any;
private recentStat: any; private recentStat: any;
private warned = false; private warned = false;
private lastWarnedAt: number; private lastWarnedAt?: number;
/** /**
* 11 * 11
*/ */
private statsLogs: any[] = []; private statsLogs: any[] = [];
@bindThis constructor(module: Module, ai: ) {
public install() { super(module, ai);
if (!config.serverMonitoring) return {};
this.connection = this.ai.connection.useSharedConnection('serverStats'); this.connection = this.ai.connection.useSharedConnection('serverStats');
this.connection.on('stats', this.onStats); this.connection.on('stats', this.onStats);
@ -31,8 +41,6 @@ export default class extends Module {
setInterval(() => { setInterval(() => {
this.check(); this.check();
}, 3000); }, 3000);
return {};
} }
@bindThis @bindThis