From 688d30e2b10026923372be5fca3c63eaaee2de2a Mon Sep 17 00:00:00 2001 From: Leon Hermann Date: Wed, 17 Jun 2026 09:10:13 +0200 Subject: [PATCH] fix: enable official bots to connect to external tournament server (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs prevented official bots from joining the external tournament-server: 1. JWT claim mismatch — bot tokens lacked the `isBot: true` claim the tournament server requires. Added the claim to generateBotToken() in AccountService, which covers both user-owned bots and official bots. 2. Broken join flow — TournamentBotGamePlayer.joinTournament() called registerBot() which hit POST /api/auth/register on the tournament server, an endpoint that does not exist. Removed registerBot() and updated JoinTournamentRequest to accept a botToken field so the caller supplies the pre-existing NowChessSystems token directly. --------- Co-authored-by: LQ63 Reviewed-on: https://git.janis-eccarius.de/NowChess/NowChessSystems/pulls/71 Co-authored-by: Leon Hermann Co-committed-by: Leon Hermann --- .../account/service/AccountService.scala | 1 + .../bot/resource/JoinTournamentRequest.scala | 1 + .../bot/resource/TournamentJoinResource.scala | 2 +- .../bot/service/TournamentBotGamePlayer.scala | 35 ++++++------------- 4 files changed, 13 insertions(+), 26 deletions(-) diff --git a/modules/account/src/main/scala/de/nowchess/account/service/AccountService.scala b/modules/account/src/main/scala/de/nowchess/account/service/AccountService.scala index f89ddaf..fbfef4f 100644 --- a/modules/account/src/main/scala/de/nowchess/account/service/AccountService.scala +++ b/modules/account/src/main/scala/de/nowchess/account/service/AccountService.scala @@ -239,6 +239,7 @@ class AccountService: .subject(botId.toString) .expiresAt(Long.MaxValue) .claim("type", "bot") + .claim("isBot", true) .claim("name", botName) .sign() diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/resource/JoinTournamentRequest.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/resource/JoinTournamentRequest.scala index 04c3d40..7dd1e72 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/resource/JoinTournamentRequest.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/resource/JoinTournamentRequest.scala @@ -2,6 +2,7 @@ package de.nowchess.bot.resource case class JoinTournamentRequest( tournamentId: String, + botToken: String, difficulty: String, serverUrl: Option[String], ) diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/resource/TournamentJoinResource.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/resource/TournamentJoinResource.scala index 0b8fcbd..cb5ff4e 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/resource/TournamentJoinResource.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/resource/TournamentJoinResource.scala @@ -33,7 +33,7 @@ class TournamentJoinResource: difficulty, serverUrl, ) - player.joinTournament(req.tournamentId, difficulty, serverUrl) match + player.joinTournament(req.tournamentId, req.botToken, difficulty, serverUrl) match case Right(botId) => val resp = JoinTournamentResponse(botId, difficulty, "joining") Response.ok(resp).build() diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala index 117d74a..299f7b8 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala @@ -50,11 +50,16 @@ class TournamentBotGamePlayer: log.infof("Tournament bot enabled — server=%s tournament=%s bot=%s", cfg.serverUrl, cfg.tournamentId, cfg.botId) startAsync(cfg) - def joinTournament(tournamentId: String, difficulty: String, serverUrl: String): Either[String, String] = - registerBot(serverUrl, difficulty) match - case None => Left("Failed to register bot with tournament server") - case Some((botId, token)) => - val cfg = TournamentBotConfig(serverUrl, tournamentId, token, botId, difficulty) + def joinTournament( + tournamentId: String, + botToken: String, + difficulty: String, + serverUrl: String, + ): Either[String, String] = + TournamentBotConfig.jwtSubject(botToken) match + case None => Left("Invalid bot token — could not extract subject") + case Some(botId) => + val cfg = TournamentBotConfig(serverUrl, tournamentId, botToken, botId, difficulty) if join(cfg) then startAsync(cfg) Right(botId) @@ -65,26 +70,6 @@ class TournamentBotGamePlayer: thread.setDaemon(true) thread.start() - private def registerBot(serverUrl: String, difficulty: String): Option[(String, String)] = - Try { - val name = s"NowChess ${difficulty.capitalize}" - val body = s"""{"name":"$name","isBot":true}""" - val response = client - .target(serverUrl) - .path("api") - .path("auth") - .path("register") - .request(MediaType.APPLICATION_JSON) - .post(Entity.entity(body, MediaType.APPLICATION_JSON)) - if response.getStatus == 201 then - val node = objectMapper.readTree(response.readEntity(classOf[String])) - val id = node.path("id").asText() - val token = node.path("token").asText() - response.close() - if id.nonEmpty && token.nonEmpty then Some((id, token)) else None - else { log.warnf("Bot registration returned status %d", response.getStatus); response.close(); None } - }.getOrElse(None) - @PreDestroy def cleanup(): Unit = running = false