nanka iroiro

This commit is contained in:
syuilo 2018-08-28 09:12:59 +09:00
parent 736693db5f
commit cc335a6fde
13 changed files with 193 additions and 277 deletions

View file

@ -5,7 +5,6 @@ type Config = {
apiUrl: string; apiUrl: string;
keywordEnabled: boolean; keywordEnabled: boolean;
reversiEnabled: boolean; reversiEnabled: boolean;
serverMonitoring: boolean;
mecab?: string; mecab?: string;
}; };

View file

@ -1,9 +1,11 @@
import from './ai'; import from './ai';
import IModule from './module'; import IModule from './module';
import getDate from './utils/get-date';
import { User } from './misskey/user';
export type FriendDoc = { export type FriendDoc = {
userId: string; userId: string;
user: any; user: User;
name?: string; name?: string;
love?: number; love?: number;
lastLoveIncrementedAt?: string; lastLoveIncrementedAt?: string;
@ -50,7 +52,7 @@ export default class Friend {
} }
} }
public updateUser = (user: any) => { public updateUser = (user: User) => {
this.doc.user = user; this.doc.user = user;
this.save(); this.save();
} }
@ -79,11 +81,7 @@ export default class Friend {
} }
public incLove = () => { public incLove = () => {
const now = new Date(); const today = getDate();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const today = `${y}/${m + 1}/${d}`;
if (this.doc.lastLoveIncrementedAt != today) { if (this.doc.lastLoveIncrementedAt != today) {
this.doc.todayLoveIncrements = 0; this.doc.todayLoveIncrements = 0;

View file

@ -3,7 +3,6 @@ import config from './config';
import CoreModule from './modules/core'; import CoreModule from './modules/core';
import ReversiModule from './modules/reversi'; import ReversiModule from './modules/reversi';
import ServerModule from './modules/server';
import PingModule from './modules/ping'; import PingModule from './modules/ping';
import EmojiModule from './modules/emoji'; import EmojiModule from './modules/emoji';
import FortuneModule from './modules/fortune'; import FortuneModule from './modules/fortune';
@ -36,7 +35,6 @@ promiseRetry(retry => {
ai.install(new GuessingGameModule()); ai.install(new GuessingGameModule());
ai.install(new ReversiModule()); ai.install(new ReversiModule());
ai.install(new TimerModule()); ai.install(new TimerModule());
if (config.serverMonitoring) ai.install(new ServerModule());
if (config.keywordEnabled) ai.install(new KeywordModule()); if (config.keywordEnabled) ai.install(new KeywordModule());
console.log('--- ai started! ---'); console.log('--- ai started! ---');

5
src/misskey/user.ts Normal file
View file

@ -0,0 +1,5 @@
export type User = {
id: string;
name: string;
username: string;
};

View file

@ -4,6 +4,7 @@ import IModule from '../../module';
import MessageLike from '../../message-like'; import MessageLike from '../../message-like';
import serifs from '../../serifs'; import serifs from '../../serifs';
import Friend from '../../friend'; import Friend from '../../friend';
import getDate from '../../utils/get-date';
function zeroPadding(num: number, length: number): string { function zeroPadding(num: number, length: number): string {
return ('0000000000' + num).slice(-length); return ('0000000000' + num).slice(-length);
@ -76,12 +77,9 @@ export default class CoreModule implements IModule {
return true; return true;
} }
const withSan = const titles = ['さん', 'くん', '君', 'ちゃん', '様', '先生'];
name.endsWith('さん') ||
name.endsWith('くん') || const withSan = titles.some(t => name.endsWith(t));
name.endsWith('君') ||
name.endsWith('ちゃん') ||
name.endsWith('様');
if (withSan) { if (withSan) {
msg.friend.updateName(name); msg.friend.updateName(name);
@ -101,11 +99,7 @@ export default class CoreModule implements IModule {
if (!msg.text) return false; if (!msg.text) return false;
const incLove = () => { const incLove = () => {
const now = new Date(); const today = getDate();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const today = `${y}/${m + 1}/${d}`;
const data = msg.friend.getPerModulesData(this); const data = msg.friend.getPerModulesData(this);
@ -146,6 +140,7 @@ export default class CoreModule implements IModule {
if (!msg.text) return false; if (!msg.text) return false;
if (!msg.text.includes('なでなで')) return false; if (!msg.text.includes('なでなで')) return false;
//#region 1日に1回だけ親愛度を上げる
const now = new Date(); const now = new Date();
const y = now.getFullYear(); const y = now.getFullYear();
const m = now.getMonth(); const m = now.getMonth();
@ -160,6 +155,7 @@ export default class CoreModule implements IModule {
msg.friend.incLove(); msg.friend.incLove();
} }
//#endregion
msg.reply( msg.reply(
msg.friend.love >= 5 ? serifs.core.nadenade2 : msg.friend.love >= 5 ? serifs.core.nadenade2 :

View file

@ -128,7 +128,7 @@ export default class EmojiModule implements IModule {
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)];
const emoji = Array.isArray(hand) ? hand[0] + face + hand[1] : hand + face + hand; const emoji = Array.isArray(hand) ? hand[0] + face + hand[1] : hand + face + hand;
msg.reply(serifs.EMOJI_SUGGEST.replace('$', emoji)); msg.reply(serifs.emoji.suggest.replace('$', emoji));
return true; return true;
} else { } else {
return false; return false;

View file

@ -36,7 +36,7 @@ export default class FortuneModule implements IModule {
const rng = seedrandom(seed); const rng = seedrandom(seed);
const omikuji = omikujis[Math.floor(rng() * omikujis.length)]; const omikuji = omikujis[Math.floor(rng() * omikujis.length)];
const item = items[Math.floor(rng() * items.length)]; const item = items[Math.floor(rng() * items.length)];
msg.reply(`**${omikuji}🎉**\nラッキーアイテム: ${item}`, serifs.FORTUNE_CW); msg.reply(`**${omikuji}🎉**\nラッキーアイテム: ${item}`, serifs.fortune.cw);
return true; return true;
} else { } else {
return false; return false;

View file

@ -38,9 +38,9 @@ export default class GuessingGameModule implements IModule {
if (!msg.isMessage) { if (!msg.isMessage) {
if (exist != null) { if (exist != null) {
msg.reply(serifs.GUESSINGGAME_ARLEADY_STARTED); msg.reply(serifs.guessingGame.arleadyStarted);
} else { } else {
msg.reply(serifs.GUESSINGGAME_PLZ_DM); msg.reply(serifs.guessingGame.plzDm);
} }
return true; return true;
@ -57,7 +57,7 @@ export default class GuessingGameModule implements IModule {
endedAt: null endedAt: null
}); });
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.ai.subscribeReply(this, msg.userId, msg.isMessage, msg.isMessage ? msg.userId : reply.id);
}); });
@ -76,7 +76,7 @@ export default class GuessingGameModule implements IModule {
}); });
if (msg.text.includes('やめ')) { if (msg.text.includes('やめ')) {
msg.reply(serifs.GUESSINGGAME_CANCEL); msg.reply(serifs.guessingGame.cancel);
exist.isEnded = true; exist.isEnded = true;
exist.endedAt = Date.now(); exist.endedAt = Date.now();
this.guesses.update(exist); this.guesses.update(exist);
@ -87,7 +87,7 @@ export default class GuessingGameModule implements IModule {
const guess = msg.text.toLowerCase().replace(this.ai.account.username.toLowerCase(), '').match(/[0-9]+/); const guess = msg.text.toLowerCase().replace(this.ai.account.username.toLowerCase(), '').match(/[0-9]+/);
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.ai.subscribeReply(this, msg.userId, msg.isMessage, reply.id);
}); });
} else { } else {
@ -104,15 +104,15 @@ export default class GuessingGameModule implements IModule {
if (exist.secret < g) { if (exist.secret < g) {
text = firsttime text = firsttime
? serifs.GUESSINGGAME_LESS.replace('$', g.toString()) ? serifs.guessingGame.less.replace('$', g.toString())
: serifs.GUESSINGGAME_LESS_AGAIN.replace('$', g.toString()); : serifs.guessingGame.lessAgain.replace('$', g.toString());
} else if (exist.secret > g) { } else if (exist.secret > g) {
text = firsttime text = firsttime
? serifs.GUESSINGGAME_GRATER.replace('$', g.toString()) ? serifs.guessingGame.grater.replace('$', g.toString())
: serifs.GUESSINGGAME_GRATER_AGAIN.replace('$', g.toString()); : serifs.guessingGame.graterAgain.replace('$', g.toString());
} else { } else {
end = true; end = true;
text = serifs.GUESSINGGAME_CONGRATS.replace('{tries}', exist.tries.length.toString()); text = serifs.guessingGame.congrats.replace('{tries}', exist.tries.length.toString());
} }
if (end) { if (end) {

View file

@ -9,6 +9,7 @@
import * as request from 'request-promise-native'; import * as request from 'request-promise-native';
import Reversi, { Color } from 'misskey-reversi'; import Reversi, { Color } from 'misskey-reversi';
import config from '../../config'; import config from '../../config';
import serifs from '../../serifs';
const db = {}; const db = {};
@ -208,36 +209,36 @@ class Session {
private onEnded = async (msg: any) => { private onEnded = async (msg: any) => {
// ストリームから切断 // ストリームから切断
process.send({ process.send({
type: 'close' type: 'ended'
}); });
let text: string; let text: string;
if (msg.body.game.surrendered) { if (msg.body.game.surrendered) {
if (this.isSettai) { if (this.isSettai) {
text = `(${this.userName}を接待していたら投了されちゃいました... ごめんなさい)`; text = serifs.reversi.settaiButYouSurrendered.replace('{name}', this.userName);
} else { } else {
text = `${this.userName}が投了しちゃいました`; text = serifs.reversi.youSurrendered.replace('{name}', this.userName);
} }
} else if (msg.body.winnerId) { } else if (msg.body.winnerId) {
if (msg.body.winnerId == this.account.id) { if (msg.body.winnerId == this.account.id) {
if (this.isSettai) { if (this.isSettai) {
text = `${this.userName}に接待で勝ってしまいました...`; text = serifs.reversi.iWonButSettai.replace('{name}', this.userName);
} else { } else {
text = `${this.userName}に勝ちました♪`; text = serifs.reversi.iWon.replace('{name}', this.userName);
} }
} else { } else {
if (this.isSettai) { if (this.isSettai) {
text = `(${this.userName}に接待で負けてあげました...♪)`; text = serifs.reversi.iLoseButSettai.replace('{name}', this.userName);
} else { } else {
text = `${this.userName}に負けました...`; text = serifs.reversi.iLose.replace('{name}', this.userName);
} }
} }
} else { } else {
if (this.isSettai) { if (this.isSettai) {
text = `(${this.userName}に接待で引き分けました...)`; text = serifs.reversi.drawnSettai.replace('{name}', this.userName);
} else { } else {
text = `${this.userName}と引き分けました~`; text = serifs.reversi.drawn.replace('{name}', this.userName);
} }
} }
@ -416,8 +417,8 @@ class Session {
*/ */
private postGameStarted = async () => { private postGameStarted = async () => {
const text = this.isSettai const text = this.isSettai
? `${this.userName}の接待を始めました!` ? serifs.reversi.startedSettai.replace('{name}', this.userName)
: `対局を${this.userName}と始めました! (強さ${this.strength})`; : serifs.reversi.started.replace('{name}', this.userName).replace('{strength}', this.strength.toString());
return await this.post(`${text}\n→[観戦する](${this.url})`); return await this.post(`${text}\n→[観戦する](${this.url})`);
} }

View file

@ -6,6 +6,9 @@ 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 * as WebSocket from 'ws';
import Friend from '../../friend';
import getDate from '../../utils/get-date';
import { User } from '../../misskey/user';
export default class ReversiModule implements IModule { export default class ReversiModule implements IModule {
public name = 'reversi'; public name = 'reversi';
@ -44,13 +47,13 @@ export default class ReversiModule implements IModule {
public onMention = (msg: MessageLike) => { public onMention = (msg: MessageLike) => {
if (msg.text && (msg.text.includes('リバーシ') || msg.text.includes('りばーし') || msg.text.includes('オセロ') || msg.text.includes('おせろ') || msg.text.toLowerCase().includes('reversi'))) { if (msg.text && (msg.text.includes('リバーシ') || msg.text.includes('りばーし') || msg.text.includes('オセロ') || msg.text.includes('おせろ') || msg.text.toLowerCase().includes('reversi'))) {
if (config.reversiEnabled) { if (config.reversiEnabled) {
msg.reply(serifs.REVERSI_OK); msg.reply(serifs.reversi.ok);
this.ai.api('games/reversi/match', { this.ai.api('games/reversi/match', {
userId: msg.userId userId: msg.userId
}); });
} else { } else {
msg.reply(serifs.REVERSI_DECLINE); msg.reply(serifs.reversi.decline);
} }
return true; return true;
@ -157,8 +160,10 @@ export default class ReversiModule implements IModule {
type: 'set', type: 'set',
pos: msg.pos pos: msg.pos
}); });
} else if (msg.type == 'close') { } else if (msg.type == 'ended') {
gw.close(); gw.close();
this.onGameEnded(game);
} }
}); });
@ -189,4 +194,23 @@ export default class ReversiModule implements IModule {
console.log('reversi game stream closed'); console.log('reversi game stream closed');
}); });
} }
private onGameEnded(game: any) {
const user = game.user1Id == this.ai.account.id ? game.user2 : game.user1;
//#region 1日に1回だけ親愛度を上げる
const today = getDate();
const friend = new Friend(this.ai, { user: user });
const data = friend.getPerModulesData(this);
if (data.lastPlayedAt != today) {
data.lastPlayedAt = today;
friend.setPerModulesData(this, data);
friend.incLove();
}
//#endregion
}
} }

View file

@ -1,162 +0,0 @@
import * as childProcess from 'child_process';
import * as WebSocket from 'ws';
import from '../../ai';
import IModule from '../../module';
import serifs from '../../serifs';
import config from '../../config';
import MessageLike from '../../message-like';
const ReconnectingWebSocket = require('../../../node_modules/reconnecting-websocket/dist/reconnecting-websocket-cjs.js');
export default class ServerModule implements IModule {
public name = 'server';
private ai: ;
private connection?: any;
private preventScheduleReboot = false;
private rebootTimer: NodeJS.Timer;
private rebootTimerSub: NodeJS.Timer;
private recentStat: any;
/**
* 11
*/
private statsLogs: any[] = [];
public install = (ai: ) => {
this.ai = ai;
this.connection = new ReconnectingWebSocket(`${config.wsUrl}/server-stats`, [], {
WebSocket: WebSocket
});
this.connection.addEventListener('open', () => {
console.log('server-stats stream opened');
});
this.connection.addEventListener('close', () => {
console.log('server-stats stream closed');
});
this.connection.addEventListener('message', message => {
const msg = JSON.parse(message.data);
this.onConnectionMessage(msg);
});
setInterval(() => {
this.statsLogs.unshift(this.recentStat);
if (this.statsLogs.length > 60) this.statsLogs.pop();
}, 1000);
setInterval(() => {
this.check();
}, 3000);
}
private check = () => {
const average = (arr) => arr.reduce((a, b) => a + b) / arr.length;
const memPercentages = this.statsLogs.map(s => (s.mem.used / s.mem.total) * 100);
const memPercentage = average(memPercentages);
if (memPercentage >= 90) {
this.scheduleReboot('mem');
}
const cpuPercentages = this.statsLogs.map(s => s.cpu_usage * 100);
const cpuPercentage = average(cpuPercentages);
if (cpuPercentage >= 70) {
this.scheduleReboot('cpu');
}
console.log(`CPU: ${cpuPercentage}% | MEM: ${memPercentage}%`);
}
private onConnectionMessage = (msg: any) => {
switch (msg.type) {
case 'stats': {
this.onStats(msg.body);
break;
}
default:
break;
}
}
private onStats = async (stats: any) => {
this.recentStat = stats;
}
private scheduleReboot = (reason: string) => {
if (this.preventScheduleReboot) return;
this.preventScheduleReboot = true;
this.ai.post({
text: reason == 'cpu' ? serifs.REBOOT_SCHEDULED_CPU : serifs.REBOOT_SCHEDULED_MEM
}).then(post => {
this.ai.subscribeReply(this, 'reboot', false, post.id);
});
this.rebootTimer = setTimeout(() => {
childProcess.exec('forever restartall');
}, 1000 * 60);
this.rebootTimerSub = setTimeout(() => {
this.ai.post({
cw: serifs.REBOOT,
text: serifs.REBOOT_DETAIL
});
}, 1000 * 50);
}
public onReplyThisModule = (msg: MessageLike) => {
if (msg.text == null) return;
if (msg.text.includes('やめ') || msg.text.includes('まって')) {
if (msg.user.isAdmin) {
msg.reply(serifs.REBOOT_CANCEL_REQUESTED_ACCEPT);
this.ai.post({
text: serifs.REBOOT_CANCELED
});
this.cancelReboot();
} else {
msg.reply(serifs.REBOOT_CANCEL_REQUESTED_REJECT);
}
}
this.ai.unsubscribeReply(this, 'reboot');
}
public onMention = (msg: MessageLike) => {
if (msg.text && msg.text.includes('再起動しないで')) {
if (msg.user.isAdmin) {
msg.reply(serifs.REBOOT_CANCEL_REQUESTED_ACCEPT);
this.ai.post({
text: serifs.REBOOT_CANCELED
});
this.cancelReboot();
} else {
msg.reply(serifs.REBOOT_CANCEL_REQUESTED_REJECT);
}
return true;
} else {
return false;
}
}
private cancelReboot = () => {
clearTimeout(this.rebootTimer);
clearTimeout(this.rebootTimerSub);
// 10分間延期
setTimeout(() => {
this.preventScheduleReboot = false;
}, 1000 * 60 * 10);
}
}

View file

@ -22,94 +22,143 @@ export default {
remembered: '{reading}' remembered: '{reading}'
}, },
/**
*
*/
reversi: {
/** /**
* *
*/ */
REVERSI_OK: '良いですよ~', ok: '良いですよ~',
/** /**
* *
*/ */
REVERSI_DECLINE: 'ごめんなさい、今リバーシはするなと言われてます...', decline: 'ごめんなさい、今リバーシはするなと言われてます...',
/** /**
* () *
*/ */
REBOOT_SCHEDULED_MEM: 'サーバーの空きメモリが少なくなってきたので、1分後にサーバー再起動しますね', started: '対局を{name}と始めました! (強さ{strength})',
/** /**
* (CPU使用率が高いので) *
*/ */
REBOOT_SCHEDULED_CPU: 'サーバーの負荷が高いので、1分後にサーバー再起動しますね', startedSettai: '({name}の接待を始めました)',
/** /**
* *
*/ */
REBOOT: 'では、まもなくサーバーを再起動します!', iWon: '{name}に勝ちました♪',
REBOOT_DETAIL: '(私も再起動に巻き込まれちゃうので、サーバーの再起動が完了したことのお知らせはできません...)',
REBOOT_CANCEL_REQUESTED_ACCEPT: 'わかりました。再起動の予定を取り消しました!', /**
REBOOT_CANCEL_REQUESTED_REJECT: 'ごめんなさい、再起動の取り消しは管理者のみが行えます...', *
*/
iWonButSettai: '({name}に接待で勝ってしまいました...)',
REBOOT_CANCELED: '再起動が取り消されました。お騒がせしました', /**
*
*/
iLose: '{name}に負けました...',
/**
*
*/
iLoseButSettai: '({name}に接待で負けてあげました...♪)',
/**
*
*/
drawn: '{name}と引き分けました~',
/**
*
*/
drawnSettai: '({name}に接待で引き分けました...)',
/**
*
*/
youSurrendered: '{name}が投了しちゃいました',
/**
*
*/
settaiButYouSurrendered: '({name}を接待していたら投了されちゃいました... ごめんなさい)',
},
/**
*
*/
guessingGame: {
/**
*
*/
arleadyStarted: 'え、ゲームは既に始まってますよ!',
/**
*
*/
plzDm: 'メッセージでやりましょう!',
/**
*
*/
started: '0~100の秘密の数を当ててみてください♪',
/**
*
*/
nan: '数字でお願いします!「やめる」と言ってゲームをやめることもできますよ!',
/**
*
*/
cancel: 'わかりました~。ありがとうございました♪',
/**
*
*/
grater: '$より大きいですね',
/**
* (2)
*/
graterAgain: 'もう一度言いますが$より大きいですよ!',
/**
*
*/
less: '$より小さいですね',
/**
* (2)
*/
lessAgain: 'もう一度言いますが$より小さいですよ!',
/**
*
*/
congrats: '正解です🎉 ({tries}回目で当てました)',
},
/** /**
* *
*/ */
EMOJI_SUGGEST: 'こんなのはどうですか?→$', emoji: {
suggest: 'こんなのはどうですか?→$',
FORTUNE_CW: '私が今日のあなたの運勢を占いました...', },
/** /**
* *
*/ */
GUESSINGGAME_ARLEADY_STARTED: 'え、ゲームは既に始まってますよ!', fortune: {
cw: '私が今日のあなたの運勢を占いました...',
},
/** /**
* *
*/ */
GUESSINGGAME_PLZ_DM: 'メッセージでやりましょう!',
/**
*
*/
GUESSINGGAME_STARTED: '0~100の秘密の数を当ててみてください♪',
/**
*
*/
GUESSINGGAME_NAN: '数字でお願いします!「やめる」と言ってゲームをやめることもできますよ!',
/**
*
*/
GUESSINGGAME_CANCEL: 'わかりました~。ありがとうございました♪',
/**
*
*/
GUESSINGGAME_GRATER: '$より大きいですね',
/**
* (2)
*/
GUESSINGGAME_GRATER_AGAIN: 'もう一度言いますが$より大きいですよ!',
/**
*
*/
GUESSINGGAME_LESS: '$より小さいですね',
/**
* (2)
*/
GUESSINGGAME_LESS_AGAIN: 'もう一度言いますが$より小さいですよ!',
/**
*
*/
GUESSINGGAME_CONGRATS: '正解です🎉 ({tries}回目で当てました)',
timer: { timer: {
set: 'わかりました!', set: 'わかりました!',
invalid: 'うーん...', invalid: 'うーん...',

8
src/utils/get-date.ts Normal file
View file

@ -0,0 +1,8 @@
export default function (): string {
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const today = `${y}/${m + 1}/${d}`;
return today;
}