feat(tournament): auto-join external tournaments and publish created ones (#77)
Build & Test (NowChessSystems) TeamCity build finished

Official bots now poll the external tournament server and auto-join every
created tournament with the hardest bot (expert). Tournaments created in
NowChessSystems are forwarded to the native tournament server so the bots
can see and join them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Reviewed-on: #77
This commit was merged in pull request #77.
This commit is contained in:
2026-06-23 10:01:35 +02:00
parent 9a9784673f
commit 9978b7ea78
4 changed files with 131 additions and 1 deletions
@@ -30,6 +30,8 @@ nowchess:
tournament:
self-url: ""
external-servers: ""
native-server-url: ${TOURNAMENT_NATIVE_SERVER_URL:http://141.37.123.132:8086}
director-name: ${TOURNAMENT_DIRECTOR_NAME:NowChess System}
mp:
jwt:
@@ -53,6 +55,8 @@ mp:
tournament:
self-url: ${TOURNAMENT_SELF_URL:}
external-servers: ${TOURNAMENT_EXTERNAL_SERVERS:}
native-server-url: ${TOURNAMENT_NATIVE_SERVER_URL:http://141.37.123.132:8086}
director-name: ${TOURNAMENT_DIRECTOR_NAME:NowChess System}
"%test":
quarkus:
@@ -40,6 +40,12 @@ class TournamentResource:
@ConfigProperty(name = "nowchess.tournament.self-url")
var selfUrl: Optional[String] = uninitialized
@ConfigProperty(name = "nowchess.tournament.native-server-url", defaultValue = "http://141.37.123.132:8086")
var nativeServerUrl: String = uninitialized
@ConfigProperty(name = "nowchess.tournament.director-name", defaultValue = "NowChess System")
var directorName: String = uninitialized
// scalafix:on
@GET
@@ -95,8 +101,31 @@ class TournamentResource:
log.warnf("Failed to replicate tournament %s to %s", t.id, remoteUrl)
}
}
publishToNativeServer(t)
Response.status(Response.Status.CREATED).entity(tournamentService.toDto(t)).build()
private def publishToNativeServer(t: de.nowchess.tournament.domain.Tournament): Unit =
if nativeServerUrl.nonEmpty then
val form = encodeForm(
Map(
"name" -> t.fullName,
"nbRounds" -> t.nbRounds.toString,
"clockLimit" -> t.clockLimit.toString,
"clockIncrement" -> t.clockIncrement.toString,
"rated" -> t.rated.toString,
),
)
if !externalClient.publishNative(nativeServerUrl, directorName, form) then
log.warnf("Failed to publish tournament %s to native server %s", t.id, nativeServerUrl)
private def encodeForm(params: Map[String, String]): String =
params
.map((k, v) => s"${enc(k)}=${enc(v)}")
.mkString("&")
private def enc(s: String): String =
java.net.URLEncoder.encode(s, "UTF-8")
@GET
@Path("/{id}")
@PermitAll
@@ -14,10 +14,56 @@ class ExternalTournamentClient:
// scalafix:off DisableSyntax.var
@Inject var objectMapper: ObjectMapper = uninitialized
@volatile private var directorToken: Option[String] = None
// scalafix:on
private def buildClient(): Client = ClientBuilder.newClient()
def publishNative(serverUrl: String, directorName: String, form: String): Boolean =
val token = directorToken.orElse {
val fresh = registerDirector(serverUrl, directorName)
directorToken = fresh
fresh
}
token.exists { tok =>
if createNative(serverUrl, tok, form) then true
else
val refreshed = registerDirector(serverUrl, directorName)
directorToken = refreshed
refreshed.exists(createNative(serverUrl, _, form))
}
private def registerDirector(serverUrl: String, name: String): Option[String] =
Try {
val client = buildClient()
val body = s"""{"name":"${name.replace("\"", "\\\"")}","isBot":false}"""
val response = client
.target(s"$serverUrl/api/auth/register")
.request(MediaType.APPLICATION_JSON)
.post(Entity.entity(body, MediaType.APPLICATION_JSON))
try
if response.getStatus / 100 == 2 then
Option(objectMapper.readTree(response.readEntity(classOf[String])).path("token").asText()).filter(_.nonEmpty)
else None
finally
response.close()
client.close()
}.getOrElse(None)
private def createNative(serverUrl: String, token: String, form: String): Boolean =
Try {
val client = buildClient()
val response = client
.target(s"$serverUrl/api/tournament")
.request(MediaType.APPLICATION_JSON)
.header("Authorization", s"Bearer $token")
.post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED))
try response.getStatus / 100 == 2
finally
response.close()
client.close()
}.getOrElse(false)
def fetchList(serverUrl: String): Option[JsonNode] =
Try {
val client = buildClient()