feat(security): add internal secret handling and Redis integration for bot events
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
package de.nowchess.bot.service
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
|
||||
case class MoveRequest(
|
||||
gameId: String,
|
||||
fen: String,
|
||||
turn: String,
|
||||
playingAs: String,
|
||||
difficulty: Int,
|
||||
botAccountId: String,
|
||||
)
|
||||
|
||||
object MoveRequestParser:
|
||||
def parse(json: String, mapper: ObjectMapper): Option[MoveRequest] =
|
||||
scala.util.Try {
|
||||
val node = mapper.readTree(json)
|
||||
MoveRequest(
|
||||
gameId = node.get("gameId").asText(),
|
||||
fen = node.get("fen").asText(),
|
||||
turn = node.get("turn").asText(),
|
||||
playingAs = node.get("playingAs").asText(),
|
||||
difficulty = node.get("difficulty").asInt(1400),
|
||||
botAccountId = node.get("botAccountId").asText(),
|
||||
)
|
||||
}.toOption
|
||||
+62
-25
@@ -2,7 +2,6 @@ package de.nowchess.bot.service
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.bot.Bot
|
||||
import de.nowchess.bot.BotController
|
||||
import de.nowchess.bot.BotDifficulty
|
||||
import de.nowchess.bot.config.RedisConfig
|
||||
@@ -12,6 +11,7 @@ import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.enterprise.event.Observes
|
||||
import jakarta.inject.Inject
|
||||
import org.redisson.api.RedissonClient
|
||||
import org.redisson.api.listener.MessageListener
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
@ApplicationScoped
|
||||
@@ -24,35 +24,72 @@ class OfficialBotService:
|
||||
@Inject var botController: BotController = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
private val terminalStatuses =
|
||||
Set("checkmate", "resign", "timeout", "stalemate", "insufficientMaterial", "draw")
|
||||
|
||||
def onStart(@Observes event: StartupEvent): Unit =
|
||||
Thread.ofVirtual().start(() => runWorker())
|
||||
BotController.listBots.foreach(subscribeToEventChannel)
|
||||
|
||||
private def subscribeToEventChannel(botName: String): Unit =
|
||||
val topic = redisson.getTopic(s"${redisConfig.prefix}:bot:$botName:events")
|
||||
topic.addListener(
|
||||
classOf[String],
|
||||
new MessageListener[String]:
|
||||
def onMessage(channel: CharSequence, msg: String): Unit =
|
||||
handleBotEvent(botName, msg),
|
||||
)
|
||||
()
|
||||
|
||||
private def runWorker(): Unit =
|
||||
val queue = redisson.getBlockingQueue[String]("nowchess:bot:move-queue")
|
||||
while true do
|
||||
try
|
||||
val json = queue.take()
|
||||
MoveRequestParser.parse(json, objectMapper).foreach(processRequest)
|
||||
catch case _: InterruptedException => Thread.currentThread().interrupt()
|
||||
private def handleBotEvent(botName: String, msg: String): Unit =
|
||||
try
|
||||
val node = objectMapper.readTree(msg)
|
||||
if node.path("type").asText() == "gameStart" then
|
||||
val gameId = node.path("gameId").asText()
|
||||
val playingAs = node.path("playingAs").asText()
|
||||
val difficulty = node.path("difficulty").asInt(1400)
|
||||
val botAccountId = node.path("botAccountId").asText()
|
||||
watchGame(botName, gameId, playingAs, difficulty, botAccountId)
|
||||
catch case _: Exception => ()
|
||||
|
||||
private def processRequest(req: MoveRequest): Unit =
|
||||
val difficulty = DifficultyMapper.fromElo(req.difficulty).getOrElse(BotDifficulty.Medium)
|
||||
val botName = difficulty match
|
||||
case BotDifficulty.Easy => "easy"
|
||||
case BotDifficulty.Medium => "medium"
|
||||
case BotDifficulty.Hard => "hard"
|
||||
case BotDifficulty.Expert => "expert"
|
||||
botController.getBot(botName).foreach(bot => parseAndMove(req, bot))
|
||||
private def watchGame(botName: String, gameId: String, playingAs: String, difficulty: Int, botAccountId: String): Unit =
|
||||
val topic = redisson.getTopic(s"${redisConfig.prefix}:game:$gameId:s2c")
|
||||
topic.addListener(
|
||||
classOf[String],
|
||||
new MessageListener[String]:
|
||||
def onMessage(channel: CharSequence, msg: String): Unit =
|
||||
handleGameEvent(botName, gameId, playingAs, difficulty, botAccountId, msg),
|
||||
)
|
||||
()
|
||||
|
||||
private def parseAndMove(req: MoveRequest, bot: Bot): Unit =
|
||||
FenParser.parseFen(req.fen).toOption.foreach { context =>
|
||||
bot(context).foreach { move =>
|
||||
val uci = toUci(move)
|
||||
val c2sTopic = s"${redisConfig.prefix}:game:${req.gameId}:c2s"
|
||||
val moveMsg = s"""{"type":"MOVE","uci":"$uci","playerId":"${req.botAccountId}"}"""
|
||||
redisson.getTopic(c2sTopic).publish(moveMsg)
|
||||
()
|
||||
private def handleGameEvent(
|
||||
botName: String,
|
||||
gameId: String,
|
||||
playingAs: String,
|
||||
difficulty: Int,
|
||||
botAccountId: String,
|
||||
msg: String,
|
||||
): Unit =
|
||||
try
|
||||
val node = objectMapper.readTree(msg)
|
||||
val status = node.path("state").path("status").asText("")
|
||||
if !terminalStatuses.contains(status) then
|
||||
val turn = node.path("state").path("turn").asText("")
|
||||
if turn == playingAs then
|
||||
val fen = node.path("state").path("fen").asText()
|
||||
computeAndSendMove(botName, gameId, fen, difficulty, botAccountId)
|
||||
catch case _: Exception => ()
|
||||
|
||||
private def computeAndSendMove(botName: String, gameId: String, fen: String, difficulty: Int, botAccountId: String): Unit =
|
||||
val level = DifficultyMapper.fromElo(difficulty).getOrElse(BotDifficulty.Medium)
|
||||
botController.getBot(botName).orElse(botController.getBot(level.toString.toLowerCase)).foreach { bot =>
|
||||
FenParser.parseFen(fen).toOption.foreach { context =>
|
||||
bot(context).foreach { move =>
|
||||
val uci = toUci(move)
|
||||
val c2sTopic = s"${redisConfig.prefix}:game:$gameId:c2s"
|
||||
val moveMsg = s"""{"type":"MOVE","uci":"$uci","playerId":"$botAccountId"}"""
|
||||
redisson.getTopic(c2sTopic).publish(moveMsg)
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user