This commit is contained in:
syuilo 2019-01-15 00:14:22 +09:00
parent 69bf95e6bf
commit 3d467c2629
No known key found for this signature in database
GPG key ID: BDC4C49D06AB9D69
20 changed files with 332 additions and 200 deletions

34
package-lock.json generated
View file

@ -3,6 +3,14 @@
"requires": true, "requires": true,
"lockfileVersion": 1, "lockfileVersion": 1,
"dependencies": { "dependencies": {
"@types/chalk": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-2.2.0.tgz",
"integrity": "sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw==",
"requires": {
"chalk": "*"
}
},
"@types/events": { "@types/events": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
@ -142,9 +150,9 @@
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
}, },
"chalk": { "chalk": {
"version": "2.4.1", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": { "requires": {
"ansi-styles": "^3.2.1", "ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
@ -157,17 +165,17 @@
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
}, },
"color-convert": { "color-convert": {
"version": "1.9.2", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": { "requires": {
"color-name": "1.1.1" "color-name": "1.1.3"
} }
}, },
"color-name": { "color-name": {
"version": "1.1.1", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
}, },
"combined-stream": { "combined-stream": {
"version": "1.0.6", "version": "1.0.6",
@ -561,9 +569,9 @@
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
}, },
"supports-color": { "supports-color": {
"version": "5.4.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": { "requires": {
"has-flag": "^3.0.0" "has-flag": "^3.0.0"
} }

View file

@ -6,12 +6,14 @@
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"@types/chalk": "2.2.0",
"@types/lokijs": "1.5.2", "@types/lokijs": "1.5.2",
"@types/node": "10.0.5", "@types/node": "10.0.5",
"@types/promise-retry": "1.1.2", "@types/promise-retry": "1.1.2",
"@types/seedrandom": "2.4.27", "@types/seedrandom": "2.4.27",
"@types/ws": "6.0.1", "@types/ws": "6.0.1",
"autobind-decorator": "2.1.0", "autobind-decorator": "2.1.0",
"chalk": "2.4.2",
"lokijs": "1.5.5", "lokijs": "1.5.5",
"mecab-async": "0.1.2", "mecab-async": "0.1.2",
"misskey-reversi": "0.0.5", "misskey-reversi": "0.0.5",

View file

@ -1,22 +1,38 @@
// AI CORE // AI CORE
import autobind from 'autobind-decorator';
import * as loki from 'lokijs'; import * as loki from 'lokijs';
import * as request from 'request-promise-native'; import * as request from 'request-promise-native';
import chalk from 'chalk';
import config from './config'; import config from './config';
import IModule from './module'; import Module from './module';
import MessageLike from './message-like'; import MessageLike from './message-like';
import { FriendDoc } from './friend'; import { FriendDoc } from './friend';
import { User } from './misskey/user'; import { User } from './misskey/user';
import getCollection from './utils/get-collection'; import getCollection from './utils/get-collection';
import Stream from './stream'; import Stream from './stream';
type OnMentionHandler = (msg: MessageLike) => boolean | HandlerResult;
type OnContextReplyHandler = (msg: MessageLike, data?: any) => void | HandlerResult;
export type HandlerResult = {
reaction: string;
};
export type InstallerResult = {
onMention?: OnMentionHandler;
onContextReply?: OnContextReplyHandler;
};
/** /**
* *
*/ */
export default class { export default class {
public account: User; public account: User;
public connection: Stream; public connection: Stream;
private modules: IModule[] = []; public modules: Module[] = [];
private onMentionHandlers: OnMentionHandler[] = [];
private onContextReplyHandlers: { [moduleName: string]: OnContextReplyHandler } = {};
public db: loki; public db: loki;
private contexts: loki.Collection<{ private contexts: loki.Collection<{
@ -30,19 +46,30 @@ export default class 藍 {
public friends: loki.Collection<FriendDoc>; public friends: loki.Collection<FriendDoc>;
constructor(account: User, modules: IModule[]) { constructor(account: User, ready?: Function) {
this.account = account; this.account = account;
this.modules = modules;
this.db = new loki('memory.json', { this.db = new loki('memory.json', {
autoload: true, autoload: true,
autosave: true, autosave: true,
autosaveInterval: 1000, autosaveInterval: 1000,
autoloadCallback: this.init autoloadCallback: err => {
if (err) {
this.log(chalk.red(`Failed to load DB: ${err}`));
} else {
if (ready) ready();
}
}
}); });
} }
private init = () => { @autobind
public log(msg: string) {
console.log(`[AiOS]: ${msg}`);
}
@autobind
public run() {
//#region Init DB //#region Init DB
this.contexts = getCollection(this.db, 'contexts', { this.contexts = getCollection(this.db, 'contexts', {
indices: ['key'] indices: ['key']
@ -62,7 +89,7 @@ export default class 藍 {
// メンションされたとき // メンションされたとき
mainStream.on('mention', data => { mainStream.on('mention', data => {
if (data.userId == this.account.id) return; // 自分は弾く if (data.userId == this.account.id) return; // 自分は弾く
if (data.text.startsWith('@' + this.account.username)) { if (data.text && data.text.startsWith('@' + this.account.username)) {
this.onMention(new MessageLike(this, data, false)); this.onMention(new MessageLike(this, data, false));
} }
}); });
@ -81,11 +108,21 @@ export default class 藍 {
//#endregion //#endregion
// Install modules // Install modules
this.modules.forEach(m => m.install(this)); this.modules.forEach(m => {
this.log(`Installing ${chalk.cyan.italic(m.name)}\tmodule...`);
const res = m.install();
if (res != null) {
if (res.onMention) this.onMentionHandlers.push(res.onMention);
if (res.onContextReply) this.onContextReplyHandlers[m.name] = res.onContextReply;
}
});
this.log(chalk.green.bold('Ai am now running!'));
} }
private onMention = (msg: MessageLike) => { @autobind
console.log(`mention received: ${msg.id}`); private onMention(msg: MessageLike) {
this.log(`mention received: ${msg.id}`);
const context = !msg.isMessage && msg.replyId == null ? null : this.contexts.findOne(msg.isMessage ? { const context = !msg.isMessage && msg.replyId == null ? null : this.contexts.findOne(msg.isMessage ? {
isMessage: true, isMessage: true,
@ -98,17 +135,17 @@ export default class 藍 {
let reaction = 'love'; let reaction = 'love';
if (context != null) { if (context != null) {
const module = this.modules.find(m => m.name == context.module); const handler = this.onContextReplyHandlers[context.module];
const res = module.onReplyThisModule(msg, context.data); const res = handler(msg, context.data);
if (res != null && typeof res === 'object') { if (res != null && typeof res === 'object') {
reaction = res.reaction; reaction = res.reaction;
} }
} else { } else {
let res: ReturnType<IModule['onMention']>; let res: boolean | HandlerResult;
this.modules.filter(m => m.hasOwnProperty('onMention')).some(m => { this.onMentionHandlers.some(handler => {
res = m.onMention(msg); res = handler(msg);
return res === true || typeof res === 'object'; return res === true || typeof res === 'object';
}); });
@ -135,18 +172,21 @@ export default class 藍 {
}, 1000); }, 1000);
} }
public post = async (param: any) => { @autobind
public async post(param: any) {
const res = await this.api('notes/create', param); const res = await this.api('notes/create', param);
return res.createdNote; return res.createdNote;
} }
public sendMessage = (userId: any, param: any) => { @autobind
public sendMessage(userId: any, param: any) {
return this.api('messaging/messages/create', Object.assign({ return this.api('messaging/messages/create', Object.assign({
userId: userId, userId: userId,
}, param)); }, param));
} }
public api = (endpoint: string, param?: any) => { @autobind
public api(endpoint: string, param?: any) {
return request.post(`${config.apiUrl}/${endpoint}`, { return request.post(`${config.apiUrl}/${endpoint}`, {
json: Object.assign({ json: Object.assign({
i: config.i i: config.i
@ -154,7 +194,8 @@ export default class 藍 {
}); });
}; };
public subscribeReply = (module: IModule, key: string, isMessage: boolean, id: string, data?: any) => { @autobind
public subscribeReply(module: Module, key: string, isMessage: boolean, id: string, data?: any) {
this.contexts.insertOne(isMessage ? { this.contexts.insertOne(isMessage ? {
isMessage: true, isMessage: true,
userId: id, userId: id,
@ -170,7 +211,8 @@ export default class 藍 {
}); });
} }
public unsubscribeReply = (module: IModule, key: string) => { @autobind
public unsubscribeReply(module: Module, key: string) {
this.contexts.findAndRemove({ this.contexts.findAndRemove({
key: key, key: key,
module: module.name module: module.name

View file

@ -16,42 +16,48 @@ import ServerModule from './modules/server';
import FollowModule from './modules/follow'; import FollowModule from './modules/follow';
import ValentineModule from './modules/valentine'; import ValentineModule from './modules/valentine';
import chalk from 'chalk';
import * as request from 'request-promise-native'; import * as request from 'request-promise-native';
import IModule from './module';
const promiseRetry = require('promise-retry'); const promiseRetry = require('promise-retry');
console.log('--- starting ai... ---'); function log(msg: string): void {
console.log(`[Boot]: ${msg}`);
}
log(chalk.bold('Ai v1.0'));
promiseRetry(retry => { promiseRetry(retry => {
log(`Account fetching... >>> ${config.host}`);
return request.post(`${config.apiUrl}/i`, { return request.post(`${config.apiUrl}/i`, {
json: { json: {
i: config.i i: config.i
} }
}).catch(retry); }).catch(retry);
}, {
retries: 3
}).then(account => { }).then(account => {
console.log(`account fetched: @${account.username}`); log(chalk.green(`Account fetched successfully: @${account.username}`));
const modules: IModule[] = [ log('Starting AiOS...');
new EmojiModule(),
new FortuneModule(),
new GuessingGameModule(),
new ReversiModule(),
new TimerModule(),
new DiceModule(),
new CoreModule(),
new PingModule(),
new WelcomeModule(),
new ServerModule(),
new FollowModule(),
new BirthdayModule(),
new ValentineModule(),
];
if (config.keywordEnabled) modules.push(new KeywordModule()); const ai = new (account);
new (account, modules); new EmojiModule(ai);
new FortuneModule(ai);
new GuessingGameModule(ai);
new ReversiModule(ai);
new TimerModule(ai);
new DiceModule(ai);
new CoreModule(ai);
new PingModule(ai);
new WelcomeModule(ai);
new ServerModule(ai);
new FollowModule(ai);
new BirthdayModule(ai);
new ValentineModule(ai);
if (config.keywordEnabled) new KeywordModule(ai);
console.log('--- ai started! ---'); ai.run();
}).catch(e => { }).catch(e => {
console.error('failed to fetch account', e); log(chalk.red('Failed to fetch the account'));
}); });

View file

@ -50,7 +50,7 @@ export default class MessageLike {
public reply = async (text: string, cw?: string) => { public reply = async (text: string, cw?: string) => {
if (text == null) return; if (text == null) return;
console.log(`sending reply of ${this.id} ...`); this.ai.log(`sending reply of ${this.id} ...`);
await delay(2000); await delay(2000);

View file

@ -1,13 +1,30 @@
import from './ai'; import autobind from 'autobind-decorator';
import MessageLike from './message-like'; import , { InstallerResult } from './ai';
export default interface IModule { export default abstract class Module {
name: string; public abstract name: string;
install?: (ai: ) => void;
onMention?: (msg: MessageLike) => boolean | Result; protected ai: ;
onReplyThisModule?: (msg: MessageLike, data?: any) => void | Result;
constructor(ai: ) {
this.ai = ai;
this.ai.modules.push(this);
}
public abstract install(): InstallerResult;
@autobind
protected log(msg: string) {
this.ai.log(`[module ${this.name}]: ${msg}`);
}
@autobind
public subscribeReply(key: string, isMessage: boolean, id: string, data?: any) {
this.ai.subscribeReply(this, key, isMessage, id, data);
}
@autobind
public unsubscribeReply(key: string) {
this.ai.unsubscribeReply(this, key);
}
} }
export type Result = {
reaction: string;
};

View file

@ -1,5 +1,5 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
import Friend from '../../friend'; import Friend from '../../friend';
import serifs from '../../serifs'; import serifs from '../../serifs';
@ -7,22 +7,22 @@ function zeroPadding(num: number, length: number): string {
return ('0000000000' + num).slice(-length); return ('0000000000' + num).slice(-length);
} }
export default class BirthdayModule implements IModule { export default class BirthdayModule extends Module {
public readonly name = 'birthday'; public readonly name = 'birthday';
private ai: ; @autobind
public install() {
public install = (ai: ) => {
this.ai = ai;
this.crawleBirthday(); this.crawleBirthday();
setInterval(this.crawleBirthday, 1000 * 60 * 3); setInterval(this.crawleBirthday, 1000 * 60 * 3);
return {};
} }
/** /**
* () * ()
*/ */
private crawleBirthday = () => { @autobind
private crawleBirthday() {
const now = new Date(); const now = new Date();
const m = now.getMonth(); const m = now.getMonth();
const d = now.getDate(); const d = now.getDate();

View file

@ -1,5 +1,6 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule, { Result } from '../../module'; import { HandlerResult } from '../../ai';
import Module from '../../module';
import MessageLike from '../../message-like'; import MessageLike from '../../message-like';
import serifs, { getSerif } from '../../serifs'; import serifs, { getSerif } from '../../serifs';
import getDate from '../../utils/get-date'; import getDate from '../../utils/get-date';
@ -8,15 +9,19 @@ const titles = ['さん', 'くん', '君', 'ちゃん', '様', '先生'];
const invalidChars = ['@', '#', '*', ':', '(', '[', ' ', ' ']; const invalidChars = ['@', '#', '*', ':', '(', '[', ' ', ' '];
export default class CoreModule implements IModule { export default class CoreModule extends Module {
public readonly name = 'core'; public readonly name = 'core';
private ai: ;
public install = (ai: ) => { @autobind
this.ai = ai; public install() {
return {
onMention: this.onMention,
onContextReply: this.onContextReply
};
} }
public onMention = (msg: MessageLike) => { @autobind
private onMention(msg: MessageLike) {
if (!msg.text) return false; if (!msg.text) return false;
return ( return (
@ -34,7 +39,8 @@ export default class CoreModule implements IModule {
); );
} }
private setName = (msg: MessageLike): boolean => { @autobind
private setName(msg: MessageLike): boolean {
if (!msg.text) return false; if (!msg.text) return false;
if (!msg.text.includes('って呼んで')) return false; if (!msg.text.includes('って呼んで')) return false;
if (msg.text.startsWith('って呼んで')) return false; if (msg.text.startsWith('って呼んで')) return false;
@ -66,7 +72,7 @@ export default class CoreModule implements IModule {
msg.reply(serifs.core.setNameOk(name)); msg.reply(serifs.core.setNameOk(name));
} else { } else {
msg.reply(serifs.core.san).then(reply => { msg.reply(serifs.core.san).then(reply => {
this.ai.subscribeReply(this, msg.userId, msg.isMessage, msg.isMessage ? msg.userId : reply.id, { this.subscribeReply(msg.userId, msg.isMessage, msg.isMessage ? msg.userId : reply.id, {
name: name name: name
}); });
}); });
@ -75,7 +81,8 @@ export default class CoreModule implements IModule {
return true; return true;
} }
private greet = (msg: MessageLike): boolean => { @autobind
private greet(msg: MessageLike): boolean {
if (msg.text == null) return false; if (msg.text == null) return false;
const incLove = () => { const incLove = () => {
@ -143,7 +150,8 @@ export default class CoreModule implements IModule {
return false; return false;
} }
private nadenade = (msg: MessageLike): boolean => { @autobind
private nadenade(msg: MessageLike): boolean {
if (!msg.includes(['なでなで'])) return false; if (!msg.includes(['なでなで'])) return false;
// メッセージのみ // メッセージのみ
@ -177,7 +185,8 @@ export default class CoreModule implements IModule {
return true; return true;
} }
private kawaii = (msg: MessageLike): boolean => { @autobind
private kawaii(msg: MessageLike): boolean {
if (!msg.includes(['かわいい', '可愛い'])) return false; if (!msg.includes(['かわいい', '可愛い'])) return false;
// メッセージのみ // メッセージのみ
@ -191,7 +200,8 @@ export default class CoreModule implements IModule {
return true; return true;
} }
private suki = (msg: MessageLike): boolean => { @autobind
private suki(msg: MessageLike): boolean {
if (!msg.or(['好き', 'すき'])) return false; if (!msg.or(['好き', 'すき'])) return false;
// メッセージのみ // メッセージのみ
@ -205,7 +215,8 @@ export default class CoreModule implements IModule {
return true; return true;
} }
private hug = (msg: MessageLike): boolean => { @autobind
private hug(msg: MessageLike): boolean {
if (!msg.or(['ぎゅ', 'むぎゅ', /^はぐ(し(て|よ|よう)?)?$/])) return false; if (!msg.or(['ぎゅ', 'むぎゅ', /^はぐ(し(て|よ|よう)?)?$/])) return false;
// メッセージのみ // メッセージのみ
@ -238,7 +249,8 @@ export default class CoreModule implements IModule {
return true; return true;
} }
private humu = (msg: MessageLike): boolean => { @autobind
private humu(msg: MessageLike): boolean {
if (!msg.includes(['踏んで'])) return false; if (!msg.includes(['踏んで'])) return false;
// メッセージのみ // メッセージのみ
@ -252,7 +264,8 @@ export default class CoreModule implements IModule {
return true; return true;
} }
private batou = (msg: MessageLike): boolean => { @autobind
private batou(msg: MessageLike): boolean {
if (!msg.includes(['罵倒して', '罵って'])) return false; if (!msg.includes(['罵倒して', '罵って'])) return false;
// メッセージのみ // メッセージのみ
@ -266,7 +279,8 @@ export default class CoreModule implements IModule {
return true; return true;
} }
private ponkotu = (msg: MessageLike): boolean | Result => { @autobind
private ponkotu(msg: MessageLike): boolean | HandlerResult {
if (!msg.includes(['ぽんこつ'])) return false; if (!msg.includes(['ぽんこつ'])) return false;
msg.friend.decLove(); msg.friend.decLove();
@ -276,7 +290,8 @@ export default class CoreModule implements IModule {
}; };
} }
private rmrf = (msg: MessageLike): boolean | Result => { @autobind
private rmrf(msg: MessageLike): boolean | HandlerResult {
if (!msg.includes(['rm -rf'])) return false; if (!msg.includes(['rm -rf'])) return false;
msg.friend.decLove(); msg.friend.decLove();
@ -286,7 +301,8 @@ export default class CoreModule implements IModule {
}; };
} }
private shutdown = (msg: MessageLike): boolean | Result => { @autobind
private shutdown(msg: MessageLike): boolean | HandlerResult {
if (!msg.includes(['shutdown'])) return false; if (!msg.includes(['shutdown'])) return false;
msg.reply(serifs.core.shutdown); msg.reply(serifs.core.shutdown);
@ -296,12 +312,13 @@ export default class CoreModule implements IModule {
}; };
} }
public onReplyThisModule = (msg: MessageLike, data: any) => { @autobind
private onContextReply(msg: MessageLike, data: any) {
if (msg.text == null) return; if (msg.text == null) return;
const done = () => { const done = () => {
msg.reply(serifs.core.setNameOk(msg.friend.name)); msg.reply(serifs.core.setNameOk(msg.friend.name));
this.ai.unsubscribeReply(this, msg.userId); this.unsubscribeReply(msg.userId);
}; };
if (msg.text.includes('はい')) { if (msg.text.includes('はい')) {
@ -312,7 +329,7 @@ export default class CoreModule implements IModule {
done(); done();
} else { } else {
msg.reply(serifs.core.yesOrNo).then(reply => { msg.reply(serifs.core.yesOrNo).then(reply => {
this.ai.subscribeReply(this, msg.userId, msg.isMessage, reply.id, data); this.subscribeReply(msg.userId, msg.isMessage, reply.id, data);
}); });
} }
} }

View file

@ -1,17 +1,20 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
import MessageLike from '../../message-like'; import MessageLike from '../../message-like';
import serifs from '../../serifs'; import serifs from '../../serifs';
export default class DiceModule implements IModule { export default class DiceModule extends Module {
public readonly name = 'dice'; public readonly name = 'dice';
private ai: ;
public install = (ai: ) => { @autobind
this.ai = ai; public install() {
return {
onMention: this.onMention
};
} }
public onMention = (msg: MessageLike) => { @autobind
private onMention(msg: MessageLike) {
if (msg.text == null) return false; if (msg.text == null) return false;
const query = msg.text.match(/([0-9]+)[dD]([0-9]+)/); const query = msg.text.match(/([0-9]+)[dD]([0-9]+)/);

View file

@ -1,5 +1,5 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
import MessageLike from '../../message-like'; import MessageLike from '../../message-like';
import serifs from '../../serifs'; import serifs from '../../serifs';
@ -126,12 +126,18 @@ const faces = [
'👽' '👽'
] ]
export default class EmojiModule implements IModule { export default class EmojiModule extends Module {
public readonly name = 'emoji'; public readonly name = 'emoji';
public install = (ai: ) => { } @autobind
public install() {
return {
onMention: this.onMention
};
}
public onMention = (msg: MessageLike) => { @autobind
private onMention(msg: MessageLike) {
if (msg.includes(['顔文字', '絵文字', 'emoji', '福笑い'])) { if (msg.includes(['顔文字', '絵文字', 'emoji', '福笑い'])) {
const hand = hands[Math.floor(Math.random() * hands.length)]; const hand = hands[Math.floor(Math.random() * hands.length)];
const face = faces[Math.floor(Math.random() * faces.length)]; const face = faces[Math.floor(Math.random() * faces.length)];

View file

@ -1,16 +1,19 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
import MessageLike from '../../message-like'; import MessageLike from '../../message-like';
export default class FollowModule implements IModule { export default class FollowModule extends Module {
public readonly name = 'follow'; public readonly name = 'follow';
private ai: ;
public install = (ai: ) => { @autobind
this.ai = ai; public install() {
return {
onMention: this.onMention
};
} }
public onMention = (msg: MessageLike) => { @autobind
private onMention(msg: MessageLike) {
if (msg.text && msg.includes(['フォロー', 'フォロバ', 'follow me'])) { if (msg.text && msg.includes(['フォロー', 'フォロバ', 'follow me'])) {
if (!msg.user.isFollowing) { if (!msg.user.isFollowing) {
this.ai.api('following/create', { this.ai.api('following/create', {

View file

@ -1,16 +1,22 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
import MessageLike from '../../message-like'; import MessageLike from '../../message-like';
import serifs from '../../serifs'; import serifs from '../../serifs';
import * as seedrandom from 'seedrandom'; import * as seedrandom from 'seedrandom';
import { blessing, itemPrefixes, items } from './vocabulary'; import { blessing, itemPrefixes, items } from './vocabulary';
export default class FortuneModule implements IModule { export default class FortuneModule extends Module {
public readonly name = 'fortune'; public readonly name = 'fortune';
public install = (ai: ) => { } @autobind
public install() {
return {
onMention: this.onMention
};
}
public onMention = (msg: MessageLike) => { @autobind
private onMention(msg: MessageLike) {
if (msg.includes(['占', 'うらな', '運勢', 'おみくじ'])) { if (msg.includes(['占', 'うらな', '運勢', 'おみくじ'])) {
const date = new Date(); const date = new Date();
const seed = `${date.getFullYear()}/${date.getMonth()}/${date.getDate()}@${msg.userId}`; const seed = `${date.getFullYear()}/${date.getMonth()}/${date.getDate()}@${msg.userId}`;

View file

@ -1,13 +1,12 @@
import autobind from 'autobind-decorator';
import * as loki from 'lokijs'; import * as loki from 'lokijs';
import from '../../ai'; import Module from '../../module';
import IModule from '../../module';
import MessageLike from '../../message-like'; import MessageLike from '../../message-like';
import serifs from '../../serifs'; import serifs from '../../serifs';
import getCollection from '../../utils/get-collection'; import getCollection from '../../utils/get-collection';
export default class GuessingGameModule implements IModule { export default class GuessingGameModule extends Module {
public readonly name = 'guessingGame'; public readonly name = 'guessingGame';
private ai: ;
private guesses: loki.Collection<{ private guesses: loki.Collection<{
userId: string; userId: string;
secret: number; secret: number;
@ -17,17 +16,22 @@ export default class GuessingGameModule implements IModule {
endedAt: number; endedAt: number;
}>; }>;
public install = (ai: ) => { @autobind
this.ai = ai; public install() {
//#region Init DB //#region Init DB
this.guesses = getCollection(this.ai.db, 'guessingGame', { this.guesses = getCollection(this.ai.db, 'guessingGame', {
indices: ['userId'] indices: ['userId']
}); });
//#endregion //#endregion
return {
onMention: this.onMention,
onContextReply: this.onContextReply
};
} }
public onMention = (msg: MessageLike) => { @autobind
private onMention(msg: MessageLike) {
if (msg.includes(['数当て', '数あて'])) { if (msg.includes(['数当て', '数あて'])) {
const exist = this.guesses.findOne({ const exist = this.guesses.findOne({
userId: msg.userId, userId: msg.userId,
@ -56,7 +60,7 @@ export default class GuessingGameModule implements IModule {
}); });
msg.reply(serifs.guessingGame.started).then(reply => { msg.reply(serifs.guessingGame.started).then(reply => {
this.ai.subscribeReply(this, msg.userId, msg.isMessage, msg.isMessage ? msg.userId : reply.id); this.subscribeReply(msg.userId, msg.isMessage, msg.isMessage ? msg.userId : reply.id);
}); });
return true; return true;
@ -65,7 +69,8 @@ export default class GuessingGameModule implements IModule {
} }
} }
public onReplyThisModule = (msg: MessageLike) => { @autobind
private onContextReply(msg: MessageLike) {
if (msg.text == null) return; if (msg.text == null) return;
const exist = this.guesses.findOne({ const exist = this.guesses.findOne({
@ -78,7 +83,7 @@ export default class GuessingGameModule implements IModule {
exist.isEnded = true; exist.isEnded = true;
exist.endedAt = Date.now(); exist.endedAt = Date.now();
this.guesses.update(exist); this.guesses.update(exist);
this.ai.unsubscribeReply(this, msg.userId); this.unsubscribeReply(msg.userId);
return; return;
} }
@ -86,7 +91,7 @@ export default class GuessingGameModule implements IModule {
if (guess == null) { if (guess == null) {
msg.reply(serifs.guessingGame.nan).then(reply => { msg.reply(serifs.guessingGame.nan).then(reply => {
this.ai.subscribeReply(this, msg.userId, msg.isMessage, reply.id); this.subscribeReply(msg.userId, msg.isMessage, reply.id);
}); });
} else { } else {
if (guess.length > 3) return; if (guess.length > 3) return;
@ -116,14 +121,14 @@ export default class GuessingGameModule implements IModule {
if (end) { if (end) {
exist.isEnded = true; exist.isEnded = true;
exist.endedAt = Date.now(); exist.endedAt = Date.now();
this.ai.unsubscribeReply(this, msg.userId); this.unsubscribeReply(msg.userId);
} }
this.guesses.update(exist); this.guesses.update(exist);
msg.reply(text).then(reply => { msg.reply(text).then(reply => {
if (!end) { if (!end) {
this.ai.subscribeReply(this, msg.userId, msg.isMessage, reply.id); this.subscribeReply(msg.userId, msg.isMessage, reply.id);
} }
}); });
} }

View file

@ -1,6 +1,6 @@
import autobind from 'autobind-decorator';
import * as loki from 'lokijs'; import * as loki from 'lokijs';
import from '../../ai'; import Module from '../../module';
import IModule from '../../module';
import config from '../../config'; import config from '../../config';
import serifs from '../../serifs'; import serifs from '../../serifs';
import getCollection from '../../utils/get-collection'; import getCollection from '../../utils/get-collection';
@ -13,19 +13,17 @@ function kanaToHira(str: string) {
}); });
} }
export default class KeywordModule implements IModule { export default class KeywordModule extends Module {
public readonly name = 'keyword'; public readonly name = 'keyword';
private ai: ;
private tokenizer: any; private tokenizer: any;
private learnedKeywords: loki.Collection<{ private learnedKeywords: loki.Collection<{
keyword: string; keyword: string;
learnedAt: number; learnedAt: number;
}>; }>;
public install = (ai: ) => { @autobind
this.ai = ai; public install() {
//#region Init DB //#region Init DB
this.learnedKeywords = getCollection(this.ai.db, '_keyword_learnedKeywords', { this.learnedKeywords = getCollection(this.ai.db, '_keyword_learnedKeywords', {
indices: ['userId'] indices: ['userId']
@ -36,9 +34,12 @@ export default class KeywordModule implements IModule {
this.tokenizer.command = config.mecab; this.tokenizer.command = config.mecab;
setInterval(this.say, 1000 * 60 * 60); setInterval(this.say, 1000 * 60 * 60);
return {};
} }
private say = async () => { @autobind
private async say() {
const tl = await this.ai.api('notes/local-timeline', { const tl = await this.ai.api('notes/local-timeline', {
limit: 30 limit: 30
}); });

View file

@ -1,13 +1,19 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
import MessageLike from '../../message-like'; import MessageLike from '../../message-like';
export default class PingModule implements IModule { export default class PingModule extends Module {
public readonly name = 'ping'; public readonly name = 'ping';
public install = (ai: ) => { } @autobind
public install() {
return {
onMention: this.onMention
};
}
public onMention = (msg: MessageLike) => { @autobind
private onMention(msg: MessageLike) {
if (msg.text && msg.text.includes('ping')) { if (msg.text && msg.text.includes('ping')) {
msg.reply('PONG!'); msg.reply('PONG!');
return true; return true;

View file

@ -1,27 +1,23 @@
import * as childProcess from 'child_process'; import * as childProcess from 'child_process';
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
import serifs from '../../serifs'; import serifs from '../../serifs';
import config from '../../config'; import config from '../../config';
import MessageLike from '../../message-like'; import MessageLike from '../../message-like';
import * as WebSocket from 'ws';
import Friend from '../../friend'; import Friend from '../../friend';
import getDate from '../../utils/get-date'; import getDate from '../../utils/get-date';
export default class ReversiModule implements IModule { export default class ReversiModule extends Module {
public readonly name = 'reversi'; public readonly name = 'reversi';
private ai: ;
/** /**
* *
*/ */
private reversiConnection?: any; private reversiConnection?: any;
public install = (ai: ) => { @autobind
if (!config.reversiEnabled) return; public install() {
if (!config.reversiEnabled) return {};
this.ai = ai;
this.reversiConnection = this.ai.connection.useSharedConnection('gamesReversi'); this.reversiConnection = this.ai.connection.useSharedConnection('gamesReversi');
@ -30,9 +26,14 @@ export default class ReversiModule implements IModule {
// マッチしたとき // マッチしたとき
this.reversiConnection.on('matched', msg => this.onReversiGameStart(msg)); this.reversiConnection.on('matched', msg => this.onReversiGameStart(msg));
return {
onMention: this.onMention
};
} }
public onMention = (msg: MessageLike) => { @autobind
private onMention(msg: MessageLike) {
if (msg.includes(['リバーシ', 'オセロ', 'reversi', 'othello'])) { if (msg.includes(['リバーシ', 'オセロ', 'reversi', 'othello'])) {
if (config.reversiEnabled) { if (config.reversiEnabled) {
msg.reply(serifs.reversi.ok); msg.reply(serifs.reversi.ok);
@ -50,8 +51,9 @@ export default class ReversiModule implements IModule {
} }
} }
private onReversiInviteMe = async (inviter: any) => { @autobind
console.log(`Someone invited me: @${inviter.username}`); private async onReversiInviteMe(inviter: any) {
this.log(`Someone invited me: @${inviter.username}`);
if (config.reversiEnabled) { if (config.reversiEnabled) {
// 承認 // 承認
@ -65,8 +67,9 @@ export default class ReversiModule implements IModule {
} }
} }
private onReversiGameStart = (game: any) => { @autobind
console.log('enter reversi game room'); private onReversiGameStart(game: any) {
this.log('enter reversi game room');
// ゲームストリームに接続 // ゲームストリームに接続
const gw = this.ai.connection.connectToChannel('gamesReversiGame', { const gw = this.ai.connection.connectToChannel('gamesReversiGame', {
@ -144,6 +147,7 @@ export default class ReversiModule implements IModule {
}, 2000); }, 2000);
} }
@autobind
private onGameEnded(game: any) { private onGameEnded(game: any) {
const user = game.user1Id == this.ai.account.id ? game.user2 : game.user1; const user = game.user1Id == this.ai.account.id ? game.user2 : game.user1;

View file

@ -1,12 +1,11 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
import serifs from '../../serifs'; import serifs from '../../serifs';
import config from '../../config'; import config from '../../config';
export default class ServerModule implements IModule { export default class ServerModule extends Module {
public readonly name = 'server'; public readonly name = 'server';
private ai: ;
private connection?: any; private connection?: any;
private recentStat: any; private recentStat: any;
private warned = false; private warned = false;
@ -17,10 +16,9 @@ export default class ServerModule implements IModule {
*/ */
private statsLogs: any[] = []; private statsLogs: any[] = [];
public install = (ai: ) => { @autobind
if (!config.serverMonitoring) return; public install() {
if (!config.serverMonitoring) return {};
this.ai = ai;
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);
@ -33,9 +31,12 @@ export default class ServerModule implements IModule {
setInterval(() => { setInterval(() => {
this.check(); this.check();
}, 3000); }, 3000);
return {};
} }
private check = () => { @autobind
private check() {
const average = (arr) => arr.reduce((a, b) => a + b) / arr.length; const average = (arr) => arr.reduce((a, b) => a + b) / arr.length;
const cpuPercentages = this.statsLogs.map(s => s && s.cpu_usage * 100 || 0); const cpuPercentages = this.statsLogs.map(s => s && s.cpu_usage * 100 || 0);
@ -47,11 +48,13 @@ export default class ServerModule implements IModule {
} }
} }
private onStats = async (stats: any) => { @autobind
private async onStats(stats: any) {
this.recentStat = stats.body; this.recentStat = stats.body;
} }
private warn = () => { @autobind
private warn() {
//#region 前に警告したときから一旦落ち着いた状態を経験していなければ警告しない //#region 前に警告したときから一旦落ち着いた状態を経験していなければ警告しない
// 常に負荷が高いようなサーバーで無限に警告し続けるのを防ぐため // 常に負荷が高いようなサーバーで無限に警告し続けるのを防ぐため
if (this.warned) return; if (this.warned) return;

View file

@ -1,17 +1,20 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
import MessageLike from '../../message-like'; import MessageLike from '../../message-like';
import serifs from '../../serifs'; import serifs from '../../serifs';
export default class TimerModule implements IModule { export default class TimerModule extends Module {
public readonly name = 'timer'; public readonly name = 'timer';
private ai: ;
public install = (ai: ) => { @autobind
this.ai = ai; public install() {
return {
onMention: this.onMention
};
} }
public onMention = (msg: MessageLike) => { @autobind
private onMention(msg: MessageLike) {
const secondsQuery = (msg.text || '').match(/([0-9]+)秒/); const secondsQuery = (msg.text || '').match(/([0-9]+)秒/);
const minutesQuery = (msg.text || '').match(/([0-9]+)分/); const minutesQuery = (msg.text || '').match(/([0-9]+)分/);
const hoursQuery = (msg.text || '').match(/([0-9]+)時間/); const hoursQuery = (msg.text || '').match(/([0-9]+)時間/);

View file

@ -1,24 +1,24 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
import Friend from '../../friend'; import Friend from '../../friend';
import serifs from '../../serifs'; import serifs from '../../serifs';
export default class ValentineModule implements IModule { export default class ValentineModule extends Module {
public readonly name = 'valentine'; public readonly name = 'valentine';
private ai: ; @autobind
public install() {
public install = (ai: ) => {
this.ai = ai;
this.crawleValentine(); this.crawleValentine();
setInterval(this.crawleValentine, 1000 * 60 * 3); setInterval(this.crawleValentine, 1000 * 60 * 3);
return {};
} }
/** /**
* *
*/ */
private crawleValentine = () => { @autobind
private crawleValentine() {
const now = new Date(); const now = new Date();
const isValentine = now.getMonth() == 1 && now.getDate() == 14; const isValentine = now.getMonth() == 1 && now.getDate() == 14;

View file

@ -1,20 +1,20 @@
import from '../../ai'; import autobind from 'autobind-decorator';
import IModule from '../../module'; import Module from '../../module';
export default class WelcomeModule implements IModule { export default class WelcomeModule extends Module {
public readonly name = 'welcome'; public readonly name = 'welcome';
private ai: ; @autobind
public install() {
public install = (ai: ) => {
this.ai = ai;
const tl = this.ai.connection.useSharedConnection('localTimeline'); const tl = this.ai.connection.useSharedConnection('localTimeline');
tl.on('note', this.onLocalNote); tl.on('note', this.onLocalNote);
return {};
} }
public onLocalNote = (note: any) => { @autobind
private onLocalNote(note: any) {
if (note.isFirstNote) { if (note.isFirstNote) {
setTimeout(() => { setTimeout(() => {
this.ai.api('notes/create', { this.ai.api('notes/create', {