diff --git a/app/controllers/PlayApi.scala b/app/controllers/PlayApi.scala index 413ec98edd53d..9d81dedeffe9b 100644 --- a/app/controllers/PlayApi.scala +++ b/app/controllers/PlayApi.scala @@ -3,7 +3,7 @@ package controllers import play.api.i18n.Lang import play.api.mvc.* -import lila.app.* +import lila.app.{ *, given } import lila.core.id.GameAnyId import lila.core.perf.UserWithPerfs @@ -131,9 +131,15 @@ final class PlayApi(env: Env)(using akka.stream.Materializer) extends LilaContro BadRequest: jsonError: "This endpoint can only be used with a Bot account. See https://lichess.org/api#operation/botAccountUpgrade" - else if !lila.game.Game.isBotCompatible(pov.game) then - BadRequest(jsonError("This game cannot be played with the Bot API.")) - else f(pov) + else + isReallyBotCompatible(pov.game).flatMap: + if _ then f(pov) + else BadRequest(jsonError("This game cannot be played with the Bot API.")) + + private def isReallyBotCompatible(game: lila.core.game.Game): Fu[Boolean] = + lila.game.Game.isBotCompatible(game) match + case Some(known) => fuccess(known) + case None => game.tournamentId.so(env.tournament.api.isForBots) private def WithPovAsBoard(id: GameId)(f: Pov => Fu[Result])(using ctx: Context)(using Me) = WithPov(id): pov => diff --git a/modules/api/src/main/EventStream.scala b/modules/api/src/main/EventStream.scala index 014be56a413fc..d7a6bba67d59b 100644 --- a/modules/api/src/main/EventStream.scala +++ b/modules/api/src/main/EventStream.scala @@ -141,7 +141,7 @@ final class EventStream( gameJsonView .ownerPreview(pov)(using lightUserApi.sync) .add("source" -> game.source.map(_.name)) ++ compatJson( - bot = me.isBot && lila.game.Game.isBotCompatible(game), + bot = me.isBot && lila.game.Game.isBotCompatible(game).|(true), board = lila.game.Game.isBoardCompatible(game) ) ++ Json.obj( "id" -> game.id // API BC diff --git a/modules/game/src/main/Game.scala b/modules/game/src/main/Game.scala index b4e57f5dd503b..2613bcb7d90e4 100644 --- a/modules/game/src/main/Game.scala +++ b/modules/game/src/main/Game.scala @@ -256,13 +256,16 @@ object Game: chess.Speed(c.config) >= Speed.Blitz } - def isBotCompatible(game: Game): Boolean = { - game.hasAi || game.sourceIs(_.Friend) || game.sourceIs(_.Api) - } && isBotCompatible(game.speed) + // if source is Arena, we will also need to check if the arena accepts bots! + def isBotCompatible(game: Game): Option[Boolean] = + if !isBotCompatible(game.speed) then false.some + else if game.hasAi || game.sourceIs(_.Friend) || game.sourceIs(_.Api) then true.some + else if game.sourceIs(_.Arena) then none + else false.some def isBotCompatible(speed: Speed): Boolean = speed >= Speed.Bullet - def isBoardOrBotCompatible(game: Game) = isBoardCompatible(game) || isBotCompatible(game) + def mightBeBoardOrBotCompatible(game: Game) = isBoardCompatible(game) || isBotCompatible(game).|(true) object BSONFields: export lila.core.game.BSONFields.* diff --git a/modules/round/src/main/Drawer.scala b/modules/round/src/main/Drawer.scala index 6c51d6d1a036e..acb20ca50254b 100644 --- a/modules/round/src/main/Drawer.scala +++ b/modules/round/src/main/Drawer.scala @@ -97,7 +97,7 @@ final private[round] class Drawer( lila.core.round.CorresDrawOfferEvent(game.id), "offerEventCorres" ) - if lila.game.Game.isBoardOrBotCompatible(game) then + if lila.game.Game.mightBeBoardOrBotCompatible(game) then Bus.publish( lila.game.actorApi.BoardDrawOffer(game), lila.game.actorApi.BoardDrawOffer.makeChan(game.id) diff --git a/modules/round/src/main/RoundAsyncActor.scala b/modules/round/src/main/RoundAsyncActor.scala index 4b2c7cb51c34b..a646d2fdd21b8 100644 --- a/modules/round/src/main/RoundAsyncActor.scala +++ b/modules/round/src/main/RoundAsyncActor.scala @@ -411,7 +411,7 @@ final private class RoundAsyncActor( publishBoardBotGone(pov, millis.some) private def publishBoardBotGone(pov: Pov, millis: Option[Long]) = - if lila.game.Game.isBoardOrBotCompatible(pov.game) then + if lila.game.Game.mightBeBoardOrBotCompatible(pov.game) then lila.common.Bus.publish( lila.game.actorApi.BoardGone(pov, millis.map(m => (m.atLeast(0) / 1000).toInt)), lila.game.actorApi.BoardGone.makeChan(gameId) diff --git a/modules/round/src/main/Takebacker.scala b/modules/round/src/main/Takebacker.scala index b007a042b6e54..5141ff66dcd04 100644 --- a/modules/round/src/main/Takebacker.scala +++ b/modules/round/src/main/Takebacker.scala @@ -133,14 +133,14 @@ final private class Takebacker( proxy.save(p2).inject(p2.events) private def publishTakebackOffer(game: Game): Unit = - if lila.game.Game.isBoardOrBotCompatible(game) then + if lila.game.Game.mightBeBoardOrBotCompatible(game) then Bus.publish( lila.game.actorApi.BoardTakebackOffer(game), lila.game.actorApi.BoardTakebackOffer.makeChan(game.id) ) private def publishTakeback(prevPov: Pov)(using proxy: GameProxy): Unit = - if lila.game.Game.isBoardOrBotCompatible(prevPov.game) then + if lila.game.Game.mightBeBoardOrBotCompatible(prevPov.game) then proxy.withPov(prevPov.color): p => fuccess: Bus.publish( diff --git a/modules/tournament/src/main/TournamentApi.scala b/modules/tournament/src/main/TournamentApi.scala index 270ef6b956397..fa2e147ad9e15 100644 --- a/modules/tournament/src/main/TournamentApi.scala +++ b/modules/tournament/src/main/TournamentApi.scala @@ -453,6 +453,10 @@ final class TournamentApi( _.sequentiallyVoid(playerRepo.withdraw(_, userId)) } + def isForBots(tourId: TourId): Fu[Boolean] = + for tour <- cached.tourCache.byId(tourId) + yield tour.exists(_.conditions.bots.so(_.allowed)) + private[tournament] def kickFromTeam(teamId: TeamId, userId: UserId): Funit = tournamentRepo.withdrawableIds(userId, teamId = teamId.some, reason = "kickFromTeam").flatMap { _.sequentiallyVoid: tourId =>