From c5797ed10ce579dabed6ca401ee6c4af6e68e7be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:17:20 +0000 Subject: [PATCH 1/4] Initial plan From 616443ff5213030a4fc1fd882f347af5916ed11c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:28:20 +0000 Subject: [PATCH 2/4] feat: refactor map-guessr to use ChannelLimitedBot for #tsgbot-games support Co-authored-by: hakatashi <3126484+hakatashi@users.noreply.github.com> --- map-guessr/index.ts | 209 ++++++++++++++++++++++++-------------------- 1 file changed, 115 insertions(+), 94 deletions(-) diff --git a/map-guessr/index.ts b/map-guessr/index.ts index af6bd407..5885576a 100644 --- a/map-guessr/index.ts +++ b/map-guessr/index.ts @@ -5,16 +5,18 @@ import puppeteer from "puppeteer"; import { AteQuizProblem, AteQuizResult } from "../atequiz"; import { ChatPostMessageArguments, + GenericMessageEvent, WebClient, } from "@slack/web-api"; import { increment } from "../achievements"; +import { ChannelLimitedBot } from "../lib/channelLimitedBot"; +import { Deferred } from "../lib/utils"; import { EventEmitter } from 'events'; const { Mutex } = require("async-mutex"); const { AteQuiz } = require("../atequiz/index.ts"); const cloudinary = require("cloudinary"); const API_KEY = process.env.GOOGLE_MAPS_API_KEY; -const CHANNEL = process.env.CHANNEL_SANDBOX; const mutex = new Mutex(); const img_size = 1000; @@ -174,7 +176,6 @@ const mesHelp = { }, }, ], - channel: CHANNEL, }; function countriesListMessageGen(aliases: Record): any { @@ -212,7 +213,6 @@ function countriesListMessageGen(aliases: Record): any { return { type: "section", text: { type: "mrkdwn", text: text } }; }), ], - channel: CHANNEL, }; return mesCountries; } @@ -582,13 +582,14 @@ function problemFormat( img_url: string, latitude: number, longitude: number, - thread_ts: string + thread_ts: string, + channel: string ) { const answer = latLngFormat(latitude, longitude); const problem: CoordAteQuizProblem = { problemMessage: { - channel: CHANNEL, + channel: channel, thread_ts, text: `緯度と経度を当ててね。サイズは${distFormat(size)}四方だよ。`, blocks: [ @@ -608,13 +609,13 @@ function problemFormat( }, hintMessages: [ { - channel: CHANNEL, + channel: channel, text: `画像の中心点は${country.properties.NAME_JA}にあるよ:triangular_flag_on_post:`, }, ], - immediateMessage: { channel: CHANNEL, text: "制限時間: 300秒" }, + immediateMessage: { channel: channel, text: "制限時間: 300秒" }, solvedMessage: { - channel: CHANNEL, + channel: channel, text: ``, reply_broadcast: true, thread_ts, @@ -622,13 +623,13 @@ function problemFormat( unfurl_media: false, }, incorrectMessage: { - channel: CHANNEL, + channel: channel, text: ``, unfurl_links: false, unfurl_media: false, }, unsolvedMessage: { - channel: CHANNEL, + channel: channel, text: `もう、しっかりして!\n中心点の座標は だよ:anger:`, reply_broadcast: true, thread_ts, @@ -648,10 +649,11 @@ async function prepareProblem( message: any, aliases: Record, world: any, - thread_ts: string + thread_ts: string, + channel: string ) { await slack.chat.postEphemeral({ - channel: CHANNEL, + channel: channel, text: "問題を生成中...", user: message.user, ...postOptions, @@ -664,7 +666,7 @@ async function prepareProblem( if (errorText.length > 0) { await slack.chat.postMessage({ text: errorText, - channel: CHANNEL, + channel: channel, ...postOptions, }); return; @@ -686,106 +688,125 @@ async function prepareProblem( img_url, latitude, longitude, - thread_ts + thread_ts, + channel ); return problem; } -export default async ({ eventClient, webClient: slack }: SlackInterface) => { - const aliases = (await fs.readJson( - __dirname + "/country_names.json" - )) as Record; +class MapGuessr extends ChannelLimitedBot { + protected override readonly wakeWordRegex = /^座標[当あ]て/; - const world = await fs.readJson(__dirname + "/countries.geojson"); - eventClient.on("message", async (message) => { - if ( - message.channel !== CHANNEL || - message.thread_ts || - !( - message.text?.startsWith("座標当て") || - message.text?.startsWith("座標あて") - ) - ) { - return; - } - const messageTs = { thread_ts: message.ts }; + protected override readonly username = 'coord-quiz'; + + protected override readonly iconEmoji = ':globe_with_meridians:'; - if (message.text.includes("help")) { - await slack.chat.postMessage({ + private readonly aliases: Record; + + private readonly world: any; + + constructor(slackClients: SlackInterface, aliases: Record, world: any) { + super(slackClients); + this.aliases = aliases; + this.world = world; + } + + protected override onWakeWord(message: GenericMessageEvent, channel: string): Promise { + if (message.text.includes('help')) { + this.postMessage({ ...mesHelp, - ...postOptions, - ...messageTs, + channel, + thread_ts: message.ts, }); - return; + return Promise.resolve(null); } - if (message.text.includes("countries")) { - await slack.chat.postMessage({ - ...countriesListMessageGen(aliases), - ...postOptions, - ...messageTs, + if (message.text.includes('countries')) { + this.postMessage({ + ...countriesListMessageGen(this.aliases), + channel, + thread_ts: message.ts, }); - return; + return Promise.resolve(null); } if (mutex.isLocked()) { - slack.chat.postMessage({ - channel: CHANNEL, - text: "今クイズ中だよ:angry:", - ...messageTs, - ...postOptions, + this.postMessage({ + channel, + text: '今クイズ中だよ:angry:', + thread_ts: message.ts, }); - return; + return Promise.resolve(null); } - const [result, startTime, size] = await mutex.runExclusive(async () => { - const arr = await Promise.race([ - (async () => { - const problem: CoordAteQuizProblem = await prepareProblem( - slack, - message, - aliases, - world, - message.ts - ); - - const ateQuiz = new CoordAteQuiz(eventClient, slack, problem); - const st = Date.now(); - const res = await ateQuiz.start(); - - return [res, st, problem.size]; - })(), - (async () => { - await new Promise((resolve) => { - return setTimeout(resolve, 600 * 1000); - }); - return [null, null, null] as any[]; - })(), - ]); - return arr; + const quizMessageDeferred = new Deferred(); + + mutex.runExclusive(async () => { + try { + const problem: CoordAteQuizProblem = await prepareProblem( + this.slack, + message, + this.aliases, + this.world, + message.ts, + channel, + ); + + if (!problem) { + quizMessageDeferred.resolve(null); + return; + } + + const ateQuiz = new CoordAteQuiz(this.eventClient, this.slack, problem); + const startTime = Date.now(); + const result = await ateQuiz.start({ + mode: 'normal', + onStarted(startMessage: any) { + quizMessageDeferred.resolve(startMessage.ts!); + }, + }); + + await this.deleteProgressMessage(await quizMessageDeferred.promise); + + const endTime = Date.now(); + + if (result.state === 'solved') { + await increment(result.correctAnswerer, 'coord-quiz-easy-answer'); + if (problem.size < 20.00001) { + await increment(result.correctAnswerer, 'coord-quiz-professional-answer'); + } + if (problem.size <= 100.00001) { + await increment(result.correctAnswerer, 'coord-quiz-hard-answer'); + } + if (problem.size <= 500.00001) { + await increment(result.correctAnswerer, 'coord-quiz-medium-answer'); + } + if (endTime - startTime <= 30000) { + await increment(result.correctAnswerer, 'coord-quiz-30sec-answer'); + } + } + } catch (error: unknown) { + this.log.error('Failed to start map-guessr quiz', error); + const errorText = error instanceof Error && error.stack !== undefined + ? error.stack : String(error); + this.postMessage({ + channel, + text: `エラー😢\n\`${errorText}\``, + }); + quizMessageDeferred.resolve(null); + } }); - const endTime = Date.now(); + return quizMessageDeferred.promise; + } +} - if (!result) return; +export default async (slackClients: SlackInterface) => { + const aliases = (await fs.readJson( + __dirname + "/country_names.json" + )) as Record; - if (result.state === "solved") { - await increment(result.correctAnswerer, "coord-quiz-easy-answer"); - if (size < 20.00001) { - await increment( - result.correctAnswerer, - "coord-quiz-professional-answer" - ); - } - if (size <= 100.00001) { - await increment(result.correctAnswerer, "coord-quiz-hard-answer"); - } - if (size <= 500.00001) { - await increment(result.correctAnswerer, "coord-quiz-medium-answer"); - } - if (endTime - startTime <= 30000) { - await increment(result.correctAnswerer, "coord-quiz-30sec-answer"); - } - } - }); + const world = await fs.readJson(__dirname + "/countries.geojson"); + + return new MapGuessr(slackClients, aliases, world); }; \ No newline at end of file From 59035a6548a598754f796dc8ef73c623b7d03165 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:43:41 +0000 Subject: [PATCH 3/4] fix: use shorthand property notation and type world as FeatureCollection Co-authored-by: hakatashi <3126484+hakatashi@users.noreply.github.com> --- map-guessr/index.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/map-guessr/index.ts b/map-guessr/index.ts index 5885576a..3490420b 100644 --- a/map-guessr/index.ts +++ b/map-guessr/index.ts @@ -589,7 +589,7 @@ function problemFormat( const problem: CoordAteQuizProblem = { problemMessage: { - channel: channel, + channel, thread_ts, text: `緯度と経度を当ててね。サイズは${distFormat(size)}四方だよ。`, blocks: [ @@ -609,13 +609,13 @@ function problemFormat( }, hintMessages: [ { - channel: channel, + channel, text: `画像の中心点は${country.properties.NAME_JA}にあるよ:triangular_flag_on_post:`, }, ], - immediateMessage: { channel: channel, text: "制限時間: 300秒" }, + immediateMessage: { channel, text: "制限時間: 300秒" }, solvedMessage: { - channel: channel, + channel, text: ``, reply_broadcast: true, thread_ts, @@ -623,13 +623,13 @@ function problemFormat( unfurl_media: false, }, incorrectMessage: { - channel: channel, + channel, text: ``, unfurl_links: false, unfurl_media: false, }, unsolvedMessage: { - channel: channel, + channel, text: `もう、しっかりして!\n中心点の座標は だよ:anger:`, reply_broadcast: true, thread_ts, @@ -637,8 +637,8 @@ function problemFormat( unfurl_media: false, }, answer: [latitude, longitude], - zoom: zoom, - size: size, + zoom, + size, correctAnswers: [] as string[], }; return problem; @@ -648,12 +648,12 @@ async function prepareProblem( slack: any, message: any, aliases: Record, - world: any, + world: Turf.FeatureCollection, thread_ts: string, channel: string ) { await slack.chat.postEphemeral({ - channel: channel, + channel, text: "問題を生成中...", user: message.user, ...postOptions, @@ -666,7 +666,7 @@ async function prepareProblem( if (errorText.length > 0) { await slack.chat.postMessage({ text: errorText, - channel: channel, + channel, ...postOptions, }); return; @@ -703,9 +703,9 @@ class MapGuessr extends ChannelLimitedBot { private readonly aliases: Record; - private readonly world: any; + private readonly world: Turf.FeatureCollection; - constructor(slackClients: SlackInterface, aliases: Record, world: any) { + constructor(slackClients: SlackInterface, aliases: Record, world: Turf.FeatureCollection) { super(slackClients); this.aliases = aliases; this.world = world; From 80bc765255d6b6c1c46b6771725195ec07a18bf2 Mon Sep 17 00:00:00 2001 From: Koki Takahashi Date: Mon, 2 Mar 2026 20:01:05 +0000 Subject: [PATCH 4/4] fix: update message handling to check for bot_id instead of subtype --- AGENTS.md | 12 ++++++++++++ atequiz/index.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 7b05da9c..3824dc1a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,6 +153,18 @@ kill Note the PIDs when launching (they are printed after the `&` command) and use them to stop only the intended processes. +**After stopping, always verify that the processes are actually terminated** before reporting success. On Cygwin/Windows environments, `pkill` may silently fail to kill native Windows processes. Use the following commands to confirm: + +```bash +# Confirm no matching processes remain +ps aux | grep -E "ts-node|ngrok" | grep -v grep + +# Confirm the port is no longer listening +netstat -ano | grep ":" | grep "LISTENING" +``` + +Both commands should return empty output. If processes remain, find the PID with `ps aux` and kill them explicitly with `kill `. + ### Hot Reload with ts-node-dev `npm run dev` uses `ts-node-dev`, which **automatically detects file changes and restarts the server** without any manual intervention. When a `.ts` file is saved, the output looks like: diff --git a/atequiz/index.ts b/atequiz/index.ts index 88b7e26c..b21f663c 100644 --- a/atequiz/index.ts +++ b/atequiz/index.ts @@ -212,7 +212,7 @@ export class AteQuiz { this.eventClient.on('message', async (message: MessageEvent) => { const thread_ts = await this.threadTsDeferred.promise; if ('thread_ts' in message && message.thread_ts === thread_ts) { - if (message.subtype === 'bot_message') return; + if ('bot_id' in message && message.bot_id) return; if (_option.mode === 'solo' && message.user !== _option.player) return; this.mutex.runExclusive(async () => { if (this.state === 'solving') {