feat(tournament): wire official bots into tournaments via JWT and Redis
- Add token field to OfficialBotAccount and generate a non-expiring bot JWT on creation so official bots can authenticate - Expose token in POST /api/account/official-bots response and open the endpoint to any authenticated user - Bridge TournamentService to Redis: publish gameStart events to nowchess:bot:<name>:events after each pairing so OfficialBotService picks up games automatically Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -75,4 +75,7 @@ class OfficialBotAccount extends PanacheEntityBase:
|
|||||||
var rating: Int = 1500
|
var rating: Int = 1500
|
||||||
|
|
||||||
var createdAt: Instant = uninitialized
|
var createdAt: Instant = uninitialized
|
||||||
|
|
||||||
|
@Column(length = 1024)
|
||||||
|
var token: String = uninitialized
|
||||||
// scalafix:on
|
// scalafix:on
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ case class BotAccountWithTokenDto(id: String, name: String, rating: Int, token:
|
|||||||
|
|
||||||
case class RotatedTokenDto(token: String)
|
case class RotatedTokenDto(token: String)
|
||||||
|
|
||||||
case class OfficialBotAccountDto(id: String, name: String, rating: Int, createdAt: String)
|
case class OfficialBotAccountDto(id: String, name: String, rating: Int, createdAt: String, token: Option[String] = None)
|
||||||
|
|
||||||
case class OfficialChallengeResponse(gameId: String, botName: String, difficulty: Int)
|
case class OfficialChallengeResponse(gameId: String, botName: String, difficulty: Int)
|
||||||
|
|
||||||
|
|||||||
@@ -195,11 +195,11 @@ class AccountResource:
|
|||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/official-bots")
|
@Path("/official-bots")
|
||||||
@RolesAllowed(Array("Admin"))
|
@RolesAllowed(Array("**"))
|
||||||
def createOfficialBot(req: CreateBotAccountRequest): Response =
|
def createOfficialBot(req: CreateBotAccountRequest): Response =
|
||||||
accountService.createOfficialBotAccount(req.name) match
|
accountService.createOfficialBotAccount(req.name) match
|
||||||
case Right(bot) =>
|
case Right(bot) =>
|
||||||
Response.status(Response.Status.CREATED).entity(toOfficialBotDto(bot)).build()
|
Response.status(Response.Status.CREATED).entity(toOfficialBotDtoWithToken(bot)).build()
|
||||||
case Left(error) =>
|
case Left(error) =>
|
||||||
Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ErrorDto(error.message)).build()
|
Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
@@ -219,3 +219,12 @@ class AccountResource:
|
|||||||
rating = bot.rating,
|
rating = bot.rating,
|
||||||
createdAt = bot.createdAt.toString,
|
createdAt = bot.createdAt.toString,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private def toOfficialBotDtoWithToken(bot: OfficialBotAccount): OfficialBotAccountDto =
|
||||||
|
OfficialBotAccountDto(
|
||||||
|
id = bot.id.toString,
|
||||||
|
name = bot.name,
|
||||||
|
rating = bot.rating,
|
||||||
|
createdAt = bot.createdAt.toString,
|
||||||
|
token = Some(bot.token),
|
||||||
|
)
|
||||||
|
|||||||
@@ -205,6 +205,8 @@ class AccountService:
|
|||||||
bot.name = botName
|
bot.name = botName
|
||||||
bot.createdAt = Instant.now()
|
bot.createdAt = Instant.now()
|
||||||
officialBotAccountRepository.persist(bot)
|
officialBotAccountRepository.persist(bot)
|
||||||
|
bot.token = generateBotToken(bot.id, bot.name)
|
||||||
|
officialBotAccountRepository.persist(bot)
|
||||||
Right(bot)
|
Right(bot)
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
+19
@@ -1,10 +1,12 @@
|
|||||||
package de.nowchess.tournament.service
|
package de.nowchess.tournament.service
|
||||||
|
|
||||||
import de.nowchess.tournament.client.{CoreCreateGameRequest, CoreGameClient, CorePlayerInfo, CoreTimeControl}
|
import de.nowchess.tournament.client.{CoreCreateGameRequest, CoreGameClient, CorePlayerInfo, CoreTimeControl}
|
||||||
|
import de.nowchess.tournament.config.RedisConfig
|
||||||
import de.nowchess.tournament.domain.{Tournament, TournamentPairing, TournamentParticipant}
|
import de.nowchess.tournament.domain.{Tournament, TournamentPairing, TournamentParticipant}
|
||||||
import de.nowchess.tournament.dto.{BotRef, Clock, CreateTournamentForm, PairingDto, ResultDto, Standing, TournamentDto, Variant}
|
import de.nowchess.tournament.dto.{BotRef, Clock, CreateTournamentForm, PairingDto, ResultDto, Standing, TournamentDto, Variant}
|
||||||
import de.nowchess.tournament.error.TournamentError
|
import de.nowchess.tournament.error.TournamentError
|
||||||
import de.nowchess.tournament.repository.{PairingRepository, ParticipantRepository, TournamentRepository}
|
import de.nowchess.tournament.repository.{PairingRepository, ParticipantRepository, TournamentRepository}
|
||||||
|
import io.quarkus.redis.datasource.RedisDataSource
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import jakarta.transaction.Transactional
|
import jakarta.transaction.Transactional
|
||||||
@@ -28,6 +30,9 @@ class TournamentService:
|
|||||||
@Inject
|
@Inject
|
||||||
@RestClient
|
@RestClient
|
||||||
var coreGameClient: CoreGameClient = uninitialized
|
var coreGameClient: CoreGameClient = uninitialized
|
||||||
|
|
||||||
|
@Inject var redis: RedisDataSource = uninitialized
|
||||||
|
@Inject var redisConfig: RedisConfig = uninitialized
|
||||||
// scalafix:on
|
// scalafix:on
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -156,6 +161,20 @@ class TournamentService:
|
|||||||
pairingRepository.persist(pairing)
|
pairingRepository.persist(pairing)
|
||||||
streamManager.publishToBot(tournamentId, white.botId, s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"white"}""")
|
streamManager.publishToBot(tournamentId, white.botId, s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"white"}""")
|
||||||
streamManager.publishToBot(tournamentId, black.botId, s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"black"}""")
|
streamManager.publishToBot(tournamentId, black.botId, s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"black"}""")
|
||||||
|
publishBotGameStart(white.botName, resp.gameId, "white", white.botId)
|
||||||
|
publishBotGameStart(black.botName, resp.gameId, "black", black.botId)
|
||||||
|
|
||||||
|
private def publishBotGameStart(
|
||||||
|
botName: String,
|
||||||
|
gameId: String,
|
||||||
|
playingAs: String,
|
||||||
|
botAccountId: String,
|
||||||
|
): Unit =
|
||||||
|
val channel = s"${redisConfig.prefix}:bot:$botName:events"
|
||||||
|
val payload = s"""{"type":"gameStart","gameId":"$gameId","playingAs":"$playingAs","difficulty":1500,"botAccountId":"$botAccountId"}"""
|
||||||
|
Try(redis.pubsub(classOf[String]).publish(channel, payload)) match
|
||||||
|
case Failure(ex) => log.warnf(ex, "Failed to publish gameStart to bot channel %s", channel)
|
||||||
|
case Success(_) => ()
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
def handleGameResult(gameId: String, result: String, pgn: String): Unit =
|
def handleGameResult(gameId: String, result: String, pgn: String): Unit =
|
||||||
|
|||||||
Reference in New Issue
Block a user