From 7f50bf86cc2b32f49a58b6e666edb4048d0d7db8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 03:42:46 +0000 Subject: [PATCH 1/4] Initial plan From 2583b4d0b2d79e083e1a1c0b1228c58b75fe9732 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 03:50:40 +0000 Subject: [PATCH 2/4] Implement ChannelLimitedBot for bungo-quiz bot Co-authored-by: hakatashi <3126484+hakatashi@users.noreply.github.com> --- bungo-quiz/index.ts | 88 +++++++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/bungo-quiz/index.ts b/bungo-quiz/index.ts index 436a0c4e..f4c20bef 100644 --- a/bungo-quiz/index.ts +++ b/bungo-quiz/index.ts @@ -3,14 +3,15 @@ import axios from 'axios'; import { load as cheerioLoad } from 'cheerio'; import { sample, random } from 'lodash'; import type { SlackInterface } from '../lib/slack'; +import type { GenericMessageEvent } from '@slack/web-api'; import { AteQuizProblem, AteQuiz, typicalMessageTextsGenerator } from '../atequiz'; import { isCorrectAnswer } from '../hayaoshi'; import { unlock, increment } from '../achievements'; +import { ChannelLimitedBot } from '../lib/channelLimitedBot'; +import { Deferred } from '../lib/utils'; const mutex = new Mutex(); const decoder = new TextDecoder('shift-jis'); -const commonOption = { username: "bungo", icon_emoji: ':black_nib:' }; -const channel = process.env.CHANNEL_SANDBOX; const initialHintTextLength = 20; const normalHintTextLength = 50; const normalHintTimes = 3; @@ -75,16 +76,20 @@ const fetchCorpus = async (cardURL: string) => { }; -export default ({ eventClient, webClient: slack }: SlackInterface) => { - eventClient.on('message', (message) => { - if (message.channel !== process.env.CHANNEL_SANDBOX) { - return; - } +class BungoQuizBot extends ChannelLimitedBot { + protected override readonly wakeWordRegex = /^(?:文豪クイズ|文豪当てクイズ)$/; + + protected override readonly username = 'bungo'; + + protected override readonly iconEmoji = ':black_nib:'; + + protected override onWakeWord(message: GenericMessageEvent, channel: string): Promise { + const quizMessageDeferred = new Deferred(); mutex.runExclusive(async () => { const debugInfo = []; try { - if (message.text && (message.text === '文豪クイズ')) { + if (message.text === '文豪クイズ') { const { cards, year, month } = await fetchCards(); debugInfo.push(`ranking: ${year}/${month}`); const cardURL = sample(cards); @@ -115,11 +120,22 @@ export default ({ eventClient, webClient: slack }: SlackInterface) => { }; const quiz = new AteQuiz( - { eventClient, webClient: slack } as SlackInterface, + this.slackClients, problem, - commonOption, + { + username: this.username, + icon_emoji: this.iconEmoji, + }, ); - const result = await quiz.start(); + const result = await quiz.start({ + mode: 'normal', + onStarted(startMessage) { + quizMessageDeferred.resolve(startMessage.ts!); + }, + }); + + await this.deleteProgressMessage(await quizMessageDeferred.promise); + if (result.state === 'solved') { await increment(result.correctAnswerer, 'bungo-answer'); if (result.hintIndex === 0) { @@ -128,7 +144,7 @@ export default ({ eventClient, webClient: slack }: SlackInterface) => { } } - if (message.text && (message.text === '文豪当てクイズ')) { + if (message.text === '文豪当てクイズ') { const { cards, year, month } = await fetchCards(); debugInfo.push(`ranking: ${year}/${month}`); const cardURL = sample(cards); @@ -137,7 +153,7 @@ export default ({ eventClient, webClient: slack }: SlackInterface) => { const problem: AteQuizProblem = { problemMessage: { channel, text: `この作品の作者は誰でしょう?\n> ${hints[0]}` }, hintMessages: [ - ...hints.slice(1, -1).map((text, index, arr) => { + ...hints.slice(1, -1).map((text) => { return { channel, text: `次のヒントです!\n> ${text}` } }), { channel, text: `最後のヒントです!作品名は${title}ですよ~\n> ${hints[hints.length - 1]}` }, @@ -156,16 +172,27 @@ export default ({ eventClient, webClient: slack }: SlackInterface) => { }; const quiz = new AteQuiz( - { eventClient, webClient: slack } as SlackInterface, + this.slackClients, problem, - commonOption, + { + username: this.username, + icon_emoji: this.iconEmoji, + }, ); quiz.judge = (answer: string) => { return quiz.problem.correctAnswers.some( correctAnswer => isCorrectAnswer(correctAnswer, answer) ); }; - const result = await quiz.start(); + const result = await quiz.start({ + mode: 'normal', + onStarted(startMessage) { + quizMessageDeferred.resolve(startMessage.ts!); + }, + }); + + await this.deleteProgressMessage(await quizMessageDeferred.promise); + if (result.state === 'solved') { await increment(result.correctAnswerer, 'bungo-answer'); if (result.hintIndex === 0) { @@ -174,12 +201,29 @@ export default ({ eventClient, webClient: slack }: SlackInterface) => { } } } catch (error) { - await slack.chat.postMessage({ - channel: process.env.CHANNEL_SANDBOX, - text: `エラー😢\n${error.toString()}\n--debugInfo--\n${debugInfo.join('\n')}`, - ...commonOption, + this.log.error('Failed to start bungo quiz', error); + const errorText = + error instanceof Error && error.stack !== undefined + ? error.stack : String(error); + await this.postMessage({ + channel, + text: `エラー😢\n\`${errorText}\`\n--debugInfo--\n${debugInfo.join('\n')}`, }); + quizMessageDeferred.resolve(null); } }); - }); -}; + + return quizMessageDeferred.promise; + } + + constructor( + protected readonly slackClients: SlackInterface, + ) { + super(slackClients); + } +} + +// eslint-disable-next-line require-jsdoc +export default function bungoQuiz(slackClients: SlackInterface) { + return new BungoQuizBot(slackClients); +} From fe73c2b7b503729a22666aa60b9fdca52a932b46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 03:53:52 +0000 Subject: [PATCH 3/4] Add missing semicolons in return statements Co-authored-by: hakatashi <3126484+hakatashi@users.noreply.github.com> --- bungo-quiz/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bungo-quiz/index.ts b/bungo-quiz/index.ts index f4c20bef..a29af602 100644 --- a/bungo-quiz/index.ts +++ b/bungo-quiz/index.ts @@ -100,9 +100,9 @@ class BungoQuizBot extends ChannelLimitedBot { hintMessages: [ ...hints.slice(1, -1).map((text, index, arr) => { if (index < arr.length - 1) - return { channel, text: `次のヒントです!\n> ${text}` } + return { channel, text: `次のヒントです!\n> ${text}` }; else - return { channel, text: `次のヒントです!作者は${author}ですよ~\n> ${text}` } + return { channel, text: `次のヒントです!作者は${author}ですよ~\n> ${text}` }; }), { channel, text: `最後のヒントです!\n> ${hints[hints.length - 1]}` }, ], @@ -154,7 +154,7 @@ class BungoQuizBot extends ChannelLimitedBot { problemMessage: { channel, text: `この作品の作者は誰でしょう?\n> ${hints[0]}` }, hintMessages: [ ...hints.slice(1, -1).map((text) => { - return { channel, text: `次のヒントです!\n> ${text}` } + return { channel, text: `次のヒントです!\n> ${text}` }; }), { channel, text: `最後のヒントです!作品名は${title}ですよ~\n> ${hints[hints.length - 1]}` }, ], From 73b59f6f381fef98fa469fbd8e9d29473d478ea3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 04:48:55 +0000 Subject: [PATCH 4/4] Remove unnecessary constructor and add isResolved check Co-authored-by: hakatashi <3126484+hakatashi@users.noreply.github.com> --- bungo-quiz/index.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/bungo-quiz/index.ts b/bungo-quiz/index.ts index a29af602..86ba3413 100644 --- a/bungo-quiz/index.ts +++ b/bungo-quiz/index.ts @@ -209,18 +209,14 @@ class BungoQuizBot extends ChannelLimitedBot { channel, text: `エラー😢\n\`${errorText}\`\n--debugInfo--\n${debugInfo.join('\n')}`, }); - quizMessageDeferred.resolve(null); + if (!quizMessageDeferred.isResolved) { + quizMessageDeferred.resolve(null); + } } }); return quizMessageDeferred.promise; } - - constructor( - protected readonly slackClients: SlackInterface, - ) { - super(slackClients); - } } // eslint-disable-next-line require-jsdoc