fix: enable official bots to connect to external tournament server (#71)
Build & Test (NowChessSystems) TeamCity build finished

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 <lkhermann@web.de>
Reviewed-on: #71
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
This commit was merged in pull request #71.
This commit is contained in:
2026-06-17 09:10:13 +02:00
committed by Janis
parent 98c64fc0d5
commit 688d30e2b1
4 changed files with 13 additions and 26 deletions
@@ -239,6 +239,7 @@ class AccountService:
.subject(botId.toString) .subject(botId.toString)
.expiresAt(Long.MaxValue) .expiresAt(Long.MaxValue)
.claim("type", "bot") .claim("type", "bot")
.claim("isBot", true)
.claim("name", botName) .claim("name", botName)
.sign() .sign()
@@ -2,6 +2,7 @@ package de.nowchess.bot.resource
case class JoinTournamentRequest( case class JoinTournamentRequest(
tournamentId: String, tournamentId: String,
botToken: String,
difficulty: String, difficulty: String,
serverUrl: Option[String], serverUrl: Option[String],
) )
@@ -33,7 +33,7 @@ class TournamentJoinResource:
difficulty, difficulty,
serverUrl, serverUrl,
) )
player.joinTournament(req.tournamentId, difficulty, serverUrl) match player.joinTournament(req.tournamentId, req.botToken, difficulty, serverUrl) match
case Right(botId) => case Right(botId) =>
val resp = JoinTournamentResponse(botId, difficulty, "joining") val resp = JoinTournamentResponse(botId, difficulty, "joining")
Response.ok(resp).build() Response.ok(resp).build()
@@ -50,11 +50,16 @@ class TournamentBotGamePlayer:
log.infof("Tournament bot enabled — server=%s tournament=%s bot=%s", cfg.serverUrl, cfg.tournamentId, cfg.botId) log.infof("Tournament bot enabled — server=%s tournament=%s bot=%s", cfg.serverUrl, cfg.tournamentId, cfg.botId)
startAsync(cfg) startAsync(cfg)
def joinTournament(tournamentId: String, difficulty: String, serverUrl: String): Either[String, String] = def joinTournament(
registerBot(serverUrl, difficulty) match tournamentId: String,
case None => Left("Failed to register bot with tournament server") botToken: String,
case Some((botId, token)) => difficulty: String,
val cfg = TournamentBotConfig(serverUrl, tournamentId, token, botId, difficulty) 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 if join(cfg) then
startAsync(cfg) startAsync(cfg)
Right(botId) Right(botId)
@@ -65,26 +70,6 @@ class TournamentBotGamePlayer:
thread.setDaemon(true) thread.setDaemon(true)
thread.start() 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 @PreDestroy
def cleanup(): Unit = def cleanup(): Unit =
running = false running = false