This commit is contained in:
Take-John 2024-03-30 17:54:35 +09:00 committed by GitHub
commit 9ad70138d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 452 additions and 121 deletions

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,6 +38,11 @@ export type Meta = {
lastWakingAt: number; lastWakingAt: number;
}; };
export type ModuleDataDoc<Data = any> = {
module: string;
data: Data;
}
/** /**
* *
*/ */
@ -48,6 +54,7 @@ export default class 藍 {
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;
@ -77,10 +84,8 @@ export default class 藍 {
* @param account 使 * @param account 使
* @param modules * @param modules
*/ */
constructor(account: User, modules: Module[]) { @bindThis
this.account = account; public static start(account: User, modules: Module[]) {
this.modules = modules;
let memoryDir = '.'; let memoryDir = '.';
if (config.memoryDir) { if (config.memoryDir) {
memoryDir = config.memoryDir; memoryDir = config.memoryDir;
@ -89,7 +94,7 @@ export default class 藍 {
this.log(`Lodaing the memory from ${file}...`); this.log(`Lodaing the memory from ${file}...`);
this.db = new loki(file, { const db = new loki(file, {
autoload: true, autoload: true,
autosave: true, autosave: true,
autosaveInterval: 1000, autosaveInterval: 1000,
@ -98,7 +103,7 @@ export default class 藍 {
this.log(chalk.red(`Failed to load the memory: ${err}`)); this.log(chalk.red(`Failed to load the memory: ${err}`));
} else { } else {
this.log(chalk.green('The memory loaded successfully')); this.log(chalk.green('The memory loaded successfully'));
this.run(); new (account, modules, db);
} }
} }
}); });
@ -106,11 +111,19 @@ export default class 藍 {
@bindThis @bindThis
public log(msg: string) { public log(msg: string) {
log(`[${chalk.magenta('AiOS')}]: ${msg}`); .log(msg);
} }
@bindThis @bindThis
private run() { private static log(msg: string) {
log(`[${chalk.magenta('AiOS')}]: ${msg}`);
}
private constructor(account: User, modules: Module[], db: loki) {
this.account = account;
this.modules = modules;
this.db = db;
//#region Init DB //#region Init DB
this.meta = this.getCollection('meta', {}); this.meta = this.getCollection('meta', {});
@ -187,7 +200,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;
@ -361,7 +374,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 +393,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>();
}; };
/** /**

View file

@ -17,9 +17,102 @@ type Config = {
memoryDir?: string; memoryDir?: string;
}; };
import config from '../config.json' assert { type: 'json' }; import chalk from 'chalk';
import uncheckedConfig from '../config.json' assert { type: 'json' };
import { warn } from '@/utils/log.js';
config.wsUrl = config.host.replace('http', 'ws'); function warnWithPrefix(msg: string): void {
config.apiUrl = config.host + '/api'; warn(`[Config]: ${chalk.red(msg)}`);
}
export default config as Config; 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;
}
}
class OptionalProperty<K extends keyof Config> {
protected readonly key: K;
protected readonly type: Type<Config[K]>
public constructor(key: K, type: Type<Config[K]>) {
this.key = key;
this.type = type;
}
check(config: Object): config is { [J in K]?: Config[K] } {
const key = this.key;
if (!(key in config)) {
return true;
}
const result = this.type.check((config as { [J in K]?: unknown})[key]);
if (!result) {
warnWithPrefix(`config.json: The type of property '${key}' must be ${this.type.name}`);
}
return result;
}
}
class Property<K extends keyof Config> extends OptionalProperty<K> {
check(config: Object): config is { [J in K]: Config[K] } {
const result = this.key in config && this.type.check((config as { [J in K]?: unknown })[this.key]);
if (!result) {
warnWithPrefix(`config.json: Property '${this.key}': ${this.type.name} required`);
}
return result;
}
}
type Intersection<P extends unknown[]> = P extends [infer Q, ...infer R] ? Q & Intersection<R> : unknown;
function checkProperties<P extends OptionalProperty<keyof Config>[]>(config: Object, ...properties: P):
config is object & Intersection<{ [I in keyof P]: P[I] extends OptionalProperty<infer K> ? { [J in K]: Config[K] } : never }> {
// メッセージを表示するためすべてのプロパティをチェックしてから結果を返す
return properties.map(p => p.check(config)).every(c => c);
}
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)) {
warnWithPrefix('config.json: Root object required');
} else if (
checkProperties(
config,
new Property('host', Type.string),
new OptionalProperty('serverName', Type.string),
new Property('i', Type.string),
new OptionalProperty('master', Type.string),
new Property('keywordEnabled', Type.boolean),
new Property('reversiEnabled', Type.boolean),
new Property('notingEnabled', Type.boolean),
new Property('chartEnabled', Type.boolean),
new Property('serverMonitoring', Type.boolean),
new OptionalProperty('checkEmojisEnabled', Type.boolean),
new OptionalProperty('checkEmojisAtOnce', Type.boolean),
new OptionalProperty('mecab', Type.string),
new OptionalProperty('mecabDic', Type.string),
new OptionalProperty('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

@ -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, [ .start(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,6 +1,8 @@
export type Note = { export type Note = {
id: string; id: string;
text: string | null; text: string | null;
cw: string | null;
userId: string;
reply: any | null; reply: any | null;
poll?: { poll?: {
choices: { choices: {

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';
export default class extends Module { type Guesses = loki.Collection<{
public readonly name = 'guessingGame';
private guesses: loki.Collection<{
userId: string; userId: string;
secret: number; secret: number;
tries: number[]; tries: number[];
isEnded: boolean; isEnded: boolean;
startedAt: number; startedAt: number;
endedAt: number | null; endedAt: number | null;
}>; }>
export default class extends Module {
public readonly name = 'guessingGame';
@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,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 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';
import { Note } from '@/misskey/note.js';
function kanaToHira(str: string) { function kanaToHira(str: string) {
return str.replace(/[\u30a1-\u30f6]/g, match => { return str.replace(/[\u30a1-\u30f6]/g, match => {
@ -15,31 +17,37 @@ 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<Note[]>('notes/local-timeline', {
limit: 30 limit: 30
}); });
const interestedNotes = tl.filter(note => const interestedNotes = tl.filter((note): note is Note & { text: string } =>
note.userId !== this.ai.account.id && note.userId !== this.ai.account.id &&
note.text != null && note.text != null &&
note.cw == null); note.cw == null);

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

View file

@ -1,9 +1,17 @@
import chalk from 'chalk'; import chalk from 'chalk';
export default function(msg: string) { export default function(msg: string) {
console.log(createMessage(msg));
}
export function warn(msg: string) {
console.warn(createMessage(msg));
}
function createMessage(msg: string) {
const now = new Date(); const now = new Date();
const date = `${zeroPad(now.getHours())}:${zeroPad(now.getMinutes())}:${zeroPad(now.getSeconds())}`; const date = `${zeroPad(now.getHours())}:${zeroPad(now.getMinutes())}:${zeroPad(now.getSeconds())}`;
console.log(`${chalk.gray(date)} ${msg}`); return `${chalk.gray(date)} ${msg}`;
} }
function zeroPad(num: number, length: number = 2): string { function zeroPad(num: number, length: number = 2): string {