diff --git a/src/ai.ts b/src/ai.ts index 8fb599e..b46db52 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -99,16 +99,21 @@ export default class 藍 { const mainStream = this.connection.useSharedConnection('main'); // メンションされたとき - mainStream.on('mention', data => { + mainStream.on('mention', async data => { if (data.userId == this.account.id) return; // 自分は弾く if (data.text && data.text.startsWith('@' + this.account.username)) { + // Misskeyのバグで投稿が非公開扱いになる + if (data.text == null) data = await this.api('notes/show', { noteId: data.id }); this.onReceiveMessage(new Message(this, data, false)); } }); // 返信されたとき - mainStream.on('reply', data => { + mainStream.on('reply', async data => { if (data.userId == this.account.id) return; // 自分は弾く + if (data.text && data.text.startsWith('@' + this.account.username)) return; + // Misskeyのバグで投稿が非公開扱いになる + if (data.text == null) data = await this.api('notes/show', { noteId: data.id }); this.onReceiveMessage(new Message(this, data, false)); }); diff --git a/src/index.ts b/src/index.ts index 4d116db..978aacb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import PingModule from './modules/ping'; import EmojiModule from './modules/emoji'; import FortuneModule from './modules/fortune'; import GuessingGameModule from './modules/guessing-game'; +import KazutoriModule from './modules/kazutori'; import KeywordModule from './modules/keyword'; import WelcomeModule from './modules/welcome'; import TimerModule from './modules/timer'; @@ -46,6 +47,7 @@ promiseRetry(retry => { new EmojiModule(), new FortuneModule(), new GuessingGameModule(), + new KazutoriModule(), new ReversiModule(), new TimerModule(), new DiceModule(), diff --git a/src/message.ts b/src/message.ts index e3c1c7d..c674e42 100644 --- a/src/message.ts +++ b/src/message.ts @@ -50,7 +50,7 @@ export default class Message { } @autobind - public async reply(text: string, cw?: string) { + public async reply(text: string, cw?: string, renote?: string) { if (text == null) return; this.ai.log(`>>> Sending reply to ${chalk.underline(this.id)}`); @@ -65,7 +65,8 @@ export default class Message { return await this.ai.post({ replyId: this.messageOrNote.id, text: text, - cw: cw + cw: cw, + renoteId: renote }); } } diff --git a/src/misskey/user.ts b/src/misskey/user.ts index 7206d2b..c992643 100644 --- a/src/misskey/user.ts +++ b/src/misskey/user.ts @@ -2,6 +2,7 @@ export type User = { id: string; name: string; username: string; + host: string; isFollowing: boolean; isBot: boolean; }; diff --git a/src/modules/kazutori/index.ts b/src/modules/kazutori/index.ts new file mode 100644 index 0000000..79a7426 --- /dev/null +++ b/src/modules/kazutori/index.ts @@ -0,0 +1,176 @@ +import autobind from 'autobind-decorator'; +import * as loki from 'lokijs'; +import Module from '../../module'; +import Message from '../../message'; +import serifs from '../../serifs'; +import getCollection from '../../utils/get-collection'; +import { User } from '../../misskey/user'; + +type Game = { + votes: { + user: User; + number: number; + }[]; + isEnded: boolean; + startedAt: number; + postId: string; +}; + +export default class extends Module { + public readonly name = 'kazutori'; + + private games: loki.Collection; + + @autobind + public install() { + this.games = getCollection(this.ai.db, 'kazutori'); + + this.crawleGameEnd(); + setInterval(this.crawleGameEnd, 1000); + + return { + mentionHook: this.mentionHook, + contextHook: this.contextHook + }; + } + + @autobind + private async mentionHook(msg: Message) { + if (!msg.includes(['数取り'])) return false; + + const games = this.games.find({}); + + const recentGame = games.length == 0 ? null : games[games.length - 1]; + + if (recentGame) { + // 現在アクティブなゲームがある場合 + if (!recentGame.isEnded) { + msg.reply(serifs.kazutori.alreadyStarted, null, recentGame.postId); + return true; + } + + // 直近のゲームから1時間経ってない場合 + if (Date.now() - recentGame.startedAt < 1000 * 60 * 60) { + msg.reply(serifs.kazutori.matakondo); + return true; + } + } + + const post = await this.ai.post({ + text: serifs.kazutori.intro + }); + + this.games.insertOne({ + votes: [], + isEnded: false, + startedAt: Date.now(), + postId: post.id + }); + + this.subscribeReply(null, false, post.id); + + return true; + } + + @autobind + private async contextHook(msg: Message) { + if (msg.text == null) return; + + const game = this.games.findOne({ + isEnded: false + }); + + // 既に数字を取っていたら + if (game.votes.some(x => x.user.id == msg.userId)) return; + + const match = msg.text.match(/[0-9]+/); + if (match == null) return; + + const num = parseInt(match[0], 10); + + // 整数じゃない + if (!Number.isInteger(num)) return; + + // 範囲外 + if (num < 0 || num > 100) return; + + this.log(`Voted ${num} by ${msg.user.id}`); + + game.votes.push({ + user: msg.user, + number: num + }); + + this.games.update(game); + } + + /** + * 終了すべきゲームがないかチェック + */ + @autobind + private crawleGameEnd() { + const game = this.games.findOne({ + isEnded: false + }); + + if (game == null) return; + + // ゲーム開始から3分以上経過していたら + if (Date.now() - game.startedAt >= 1000 * 60 * 3) { + this.finish(game); + } + } + + /** + * ゲームを終わらせる + */ + @autobind + private finish(game: Game) { + game.isEnded = true; + this.games.update(game); + + // お流れ + if (game.votes.length <= 1) { + this.ai.post({ + text: serifs.kazutori.onagare, + renoteId: game.postId + }); + + return; + } + + function acct(user: User): string { + return user.host ? `@${user.username}@${user.host}` : `@${user.username}`; + } + + let results: string[] = []; + + let winner: User = null; + + for (let i = 100; i >= 0; i--) { + const users = game.votes.filter(x => x.number == i).map(x => x.user); + if (users.length == 1) { + if (winner == null) { + winner = users[0]; + results.push(`${i == 100 ? '💯' : '🎉'} ${i}: ${acct(users[0])}`); + } else { + results.push(`➖ ${i}: ${acct(users[0])}`); + } + } else if (users.length > 1) { + results.push(`❌ ${i}: ${users.map(u => acct(u)).join(' ')}`); + } + } + + const text = results.join('\n') + '\n\n' + (winner + ? serifs.kazutori.finishWithWinner(acct(winner)) + : serifs.kazutori.finishWithNoWinner); + + this.ai.post({ + text: text, + cw: serifs.kazutori.finish, + renote: game.postId + }); + + this.unsubscribeReply(null); + } +} diff --git a/src/serifs.ts b/src/serifs.ts index 6e48f4f..e00c551 100644 --- a/src/serifs.ts +++ b/src/serifs.ts @@ -235,6 +235,25 @@ export default { congrats: tries => `正解です🎉 (${tries}回目で当てました)`, }, + /** + * 数取りゲーム + */ + kazutori: { + alreadyStarted: '今ちょうどやってますよ~', + + matakondo: 'また今度やりましょう!', + + intro: 'みなさん、数取りゲームしましょう!\n0~100の中で最も大きい数字を取った人が勝ちです。他の人と被ったらだめですよ~\n制限時間は3分です。数字はこの投稿にリプライで送ってくださいね!', + + finish: 'ゲームの結果発表です!', + + finishWithWinner: user => `今回は${user}さんの勝ちです!またやりましょう♪`, + + finishWithNoWinner: '今回は勝者はいませんでした... またやりましょう♪', + + onagare: '参加者が集まらなかったのでお流れになりました...' + }, + /** * 絵文字生成 */