Compare commits

..

No commits in common. "2f40fd4aa0aaf9a71949d86f093349c9409ea0b8" and "1c842a4b95328e75b646b225bfa0112380bf6e4c" have entirely different histories.

11 changed files with 100 additions and 333 deletions

View file

@ -9,7 +9,7 @@ import chalk from 'chalk';
import { v4 as uuid } from 'uuid';
import config from '@/config.js';
import Module, { InstalledModule } from '@/module.js';
import Module from '@/module.js';
import Message from '@/message.js';
import Friend, { FriendDoc } from '@/friend.js';
import type { User } from '@/misskey/user.js';
@ -38,11 +38,6 @@ export type Meta = {
lastWakingAt: number;
};
export type ModuleDataDoc<Data = any> = {
module: string;
data: Data;
}
/**
*
*/
@ -51,7 +46,7 @@ export default interface 藍 extends Ai {
lastSleepedAt: number;
friends: loki.Collection<FriendDoc>;
moduleData: loki.Collection<ModuleDataDoc>;
moduleData: loki.Collection<any>;
}
/**
@ -65,7 +60,6 @@ export class Ai {
private mentionHooks: MentionHook[] = [];
private contextHooks: { [moduleName: string]: ContextHook } = {};
private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {};
public installedModules: { [moduleName: string]: InstalledModule } = {};
public db: loki;
public lastSleepedAt?: number;
@ -210,7 +204,7 @@ export class Ai {
this.modules.forEach(m => {
this.log(`Installing ${chalk.cyan.italic(m.name)}\tmodule...`);
m.init(this);
const res = m.install(this);
const res = m.install();
if (res != null) {
if (res.mentionHook) this.mentionHooks.push(res.mentionHook);
if (res.contextHook) this.contextHooks[m.name] = res.contextHook;

View file

@ -18,72 +18,15 @@ type Config = {
};
import uncheckedConfig from '../config.json' assert { type: 'json' };
import log from '@/utils/log.js';
class Type<T> {
public static readonly string = new Type<string>('string');
public static readonly boolean = new Type<boolean>('boolean');
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');
// 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;

View file

@ -1,26 +1,29 @@
import { bindThis } from '@/decorators.js';
import , { HandlerResult, InstallerResult, ModuleDataDoc } from '@/ai.js';
import Message from '@/message.js';
import , { InstallerResult } from '@/ai.js';
export default abstract class Module {
public abstract readonly name: string;
private maybeAi?: ;
/**
* @deprecated
*/
public installed?: InstalledModule;
private doc: any;
public init(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(ai: ): InstallerResult;
public abstract install(): InstallerResult;
/**
* @deprecated {@link Module#install} 使
*/
protected get ai(): {
if (this.maybeAi == null) {
throw new TypeError('This module has not been initialized');
@ -28,9 +31,6 @@ export default abstract class Module {
return this.maybeAi;
}
/**
* @deprecated {@link InstalledModule#log} 使
*/
@bindThis
protected log(msg: string) {
this.ai.log(`[${this.name}]: ${msg}`);
@ -41,7 +41,6 @@ export default abstract class Module {
* @param key
* @param id ID稿ID
* @param data
* @deprecated {@link InstalledModule#subscribeReply} 使
*/
@bindThis
protected subscribeReply(key: string | null, id: string, data?: any) {
@ -51,7 +50,6 @@ export default abstract class Module {
/**
*
* @param key
* @deprecated {@link InstalledModule#unsubscribeReply} 使
*/
@bindThis
protected unsubscribeReply(key: string | null) {
@ -63,132 +61,20 @@ export default abstract class Module {
*
* @param delay
* @param data
* @deprecated {@link InstalledModule#setTimeoutWithPersistence} 使
*/
@bindThis
public setTimeoutWithPersistence(delay: number, data?: any) {
this.ai.setTimeoutWithPersistence(this, delay, data);
}
/**
* @deprecated {@link InstalledModule#getData} 使
*/
@bindThis
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;
}
@bindThis
protected setData(data: Data) {
protected setData(data: any) {
this.doc.data = data;
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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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