Parse EGF and FFG ratings

This commit is contained in:
Claude Brisson
2023-12-04 21:12:54 +01:00
parent 8510bb69ec
commit 519eca8af3
12 changed files with 274 additions and 53 deletions

View File

@@ -9,7 +9,7 @@ private fun createStoreImplementation(): StoreImplementation {
return when (val storeProperty = WebappManager.getProperty("store") ?: "memory") {
"memory" -> MemoryStore()
"file" -> {
val filePath = WebappManager.getMandatoryProperty("store.file.path") ?: "."
val filePath = WebappManager.getProperty("store.file.path") ?: "."
FileStore(filePath)
}
else -> throw Error("unknown store: $storeProperty")

View File

@@ -0,0 +1,14 @@
package org.jeudego.pairgoth.ratings
import com.republicate.kson.Json
import java.net.URL
object AGARatingsHandler: RatingsHandler(RatingsManager.Ratings.AGA) {
override val defaultURL: URL by lazy {
throw Error("No URL for AGA...")
}
override val active = false
override fun parsePayload(payload: String): Json.Array {
return Json.Array()
}
}

View File

@@ -0,0 +1,28 @@
package org.jeudego.pairgoth.ratings
import com.republicate.kson.Json
import java.net.URL
object EGFRatingsHandler: RatingsHandler(RatingsManager.Ratings.EGF) {
override val defaultURL = URL("https://www.europeangodatabase.eu/EGD/EGD_2_0/downloads/allworld_lp.html")
override fun parsePayload(payload: String): Json.Array {
return payload.lines().filter {
it.matches(Regex("\\s+\\d+(?!.*\\(undefined\\)|Anonymous).*"))
}.mapNotNullTo(Json.MutableArray()) {
val match = linePattern.matchEntire(it)
if (match == null) {
logger.error("could not parse line: $it")
null
} else {
val pairs = groups.map {
Pair(it, match.groups[it]?.value)
}.toTypedArray()
Json.Object(*pairs)
}
}
}
// 19574643 Abad Jahin FR 38GJ 20k -- 15 2 T200202B
var linePattern =
Regex("\\s+(?<egf>\\d{8})\\s+(?<name>$atom+)\\s(?<firstname>$atom+)?,?\\s+(?<country>[A-Z]{2})\\s+(?<club>\\S{1,4})\\s+(?<grade>[1-9][0-9]?[kdp])\\s+(?<promotion>[1-9][0-9]?[kdp]|--)\\s+(?<rating>-?[0-9]+)\\s+(?<nt>[0-9]+)\\s+(?<last>\\S+)\\s*")
val groups = arrayOf("egf", "name", "firstname", "country", "club", "grade", "rating")
}

View File

@@ -0,0 +1,33 @@
package org.jeudego.pairgoth.ratings
import com.republicate.kson.Json
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.URL
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
object FFGRatingsHandler: RatingsHandler(RatingsManager.Ratings.FFG) {
override val defaultURL = URL("https://ffg.jeudego.org/echelle/echtxt/ech_ffg_V3.txt")
override fun parsePayload(payload: String): Json.Array {
return payload.lines().mapNotNullTo(Json.MutableArray()) {
val match = linePattern.matchEntire(it)
if (match == null) {
logger.error("could not parse line: $it")
null
} else {
val pairs = groups.map {
Pair(it, match.groups[it]?.value)
}.toTypedArray()
Json.Object(*pairs)
}
}
}
override fun defaultCharset() = StandardCharsets.ISO_8859_1
var linePattern =
Regex("(?<name>$atom+(?:\\((?:$atom|[0-9])+\\))?)\\s(?<firstname>$atom+(?:\\((?:$atom|[0-9])+\\))?)\\s+(?<rating>-?[0-9]+)\\s(?<license>[-eCLX])\\s(?<ffg>(?:\\d|[A-Z]){7}|-------)\\s(?<club>xxxx|XXXX|\\d{2}[a-zA-Z0-9]{2})\\s(?<country>[A-Z]{2})")
val groups = arrayOf("name", "firstname", "rating", "license", "club", "country")
}

View File

@@ -0,0 +1,62 @@
package org.jeudego.pairgoth.ratings
import com.republicate.kson.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jeudego.pairgoth.web.WebappManager
import org.slf4j.LoggerFactory
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.concurrent.TimeUnit
abstract class RatingsHandler(val origin: RatingsManager.Ratings) {
private val delay = TimeUnit.HOURS.toMillis(1L)
private val client = OkHttpClient()
abstract val defaultURL: URL
open val active = true
val cacheFile = RatingsManager.path.resolve("${origin.name}.json").toFile()
lateinit var players: Json.Array
val url: URL by lazy {
WebappManager.getProperty("ratings.${origin.name.lowercase(Locale.ROOT)}")?.let { URL(it) } ?: defaultURL
}
fun updateIfNeeded() {
if (Date().time - cacheFile.lastModified() > delay) {
RatingsManager.logger.info("Updating $origin cache from $url")
val payload = fetchPayload()
players = parsePayload(payload).also {
val cachePayload = it.toString()
cacheFile.printWriter().use { out ->
out.println(cachePayload)
}
}
} else if (!this::players.isInitialized) {
players = Json.parse(cacheFile.readText())?.asArray() ?: Json.Array()
}
}
fun fetchPlayers(): Json.Array {
updateIfNeeded()
return players
}
protected fun fetchPayload(): String {
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw Error("Could not fetch $origin ratings: unexpected code $response")
val contentType = response.headers["Content-Type"]?.toMediaType()
return response.body!!.source().readString(contentType?.charset() ?: defaultCharset())
}
}
open fun defaultCharset() = StandardCharsets.UTF_8
abstract fun parsePayload(payload: String): Json.Array
val logger = LoggerFactory.getLogger(origin.name)
val atom = "[-._`'a-zA-ZÀ-ÿ]"
}

View File

@@ -0,0 +1,43 @@
package org.jeudego.pairgoth.ratings
import com.republicate.kson.Json
import org.jeudego.pairgoth.web.WebappManager
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.util.*
object RatingsManager: Runnable {
enum class Ratings {
AGA,
EGF,
FFG
}
val ratingsHandlers by lazy {
mapOf(
Pair(Ratings.AGA, AGARatingsHandler),
Pair(Ratings.EGF, EGFRatingsHandler),
Pair(Ratings.FFG, FFGRatingsHandler)
);
}
val timer = Timer()
lateinit var players: Json.MutableArray
override fun run() {
logger.info("launching ratings manager")
timer.scheduleAtFixedRate(Task, 0L, 3600000L)
}
object Task: TimerTask() {
override fun run() {
players = ratingsHandlers.values.filter { it.active }.flatMapTo(Json.MutableArray()) { ratings ->
ratings.fetchPlayers()
}
}
}
val logger = LoggerFactory.getLogger("ratings")
val path = Path.of(WebappManager.getProperty("ratings.path") ?: "ratings").also {
val file = it.toFile()
if (!file.mkdirs() && !file.isDirectory) throw Error("Property pairgoth.ratings.path must be a directory")
}
}

View File

@@ -1,15 +1,13 @@
package org.jeudego.pairgoth.web
import com.republicate.mailer.SmtpLoop
import org.apache.commons.lang3.tuple.Pair
import org.jeudego.pairgoth.ratings.RatingsManager
import org.jeudego.pairgoth.util.Translator
import org.slf4j.LoggerFactory
import java.io.IOException
import java.lang.IllegalAccessError
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.*
import java.util.IllegalFormatCodePointException
import javax.net.ssl.*
import javax.servlet.*
import javax.servlet.annotation.WebListener
@@ -53,6 +51,7 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
override fun contextInitialized(sce: ServletContextEvent) {
context = sce.servletContext
logger.info("---------- Starting $WEBAPP_NAME ----------")
logger.info("info level is active")
logger.debug("debug level is active")
logger.trace("trace level is active")
webappRoot = context.getRealPath("/")
@@ -78,6 +77,9 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
// fail to correctly implement SSL...
disableSSLCertificateChecks()
registerService("ratings", RatingsManager)
startService("ratings")
} catch (ioe: IOException) {
logger.error("webapp initialization error", ioe)
}

View File

@@ -254,6 +254,7 @@
left: -50%;
border-radius: 10px;
padding: 0.5em 1em;
z-index: 100;
}
#success {

View File

@@ -84,8 +84,8 @@ function exportCSV(filename, content) {
document.body.removeChild(link);
}
/* modals */
/* modals
NOT IN USE, see popup-related code.
NodeList.prototype.modal = function(show) {
this.item(0).modal(show);
return this;
@@ -101,11 +101,12 @@ Element.prototype.modal = function(show) {
}
return this;
}
*/
/* DOM helpers */
function formValue(name) {
let ctl = $(`[name="${name}"]`)[0];
HTMLFormElement.prototype.val = function(name) {
let ctl = this.find(`[name="${name}"]`)[0];
if (!ctl) {
console.error(`unknown input name: ${name}`)
}
@@ -124,7 +125,7 @@ function formValue(name) {
}
console.error(`unhandled input tag or type for input ${name} (tag: ${tag}, type:${type}`);
return null;
}
};
function msg(id) {
let ctl = $(`#${id}`)[0];

View File

@@ -16,12 +16,13 @@ onLoad(() => {
});
$('#validate').on('click', e => {
let form = e.target.closest('form');
let valid = true;
// validate required fields
let required = ['name', 'shortName', 'startDate', 'endDate'];
if (!$('input[name="online"]')[0].checked) required.push('location')
if (!form.find('input[name="online"]')[0].checked) required.push('location')
for (let name of required) {
let ctl = $(`input[name=${name}]`)[0];
let ctl = form.find(`input[name=${name}]`)[0];
let val = ctl.value;
if (val) {
ctl.setCustomValidity('');
@@ -32,7 +33,7 @@ onLoad(() => {
}
if (!valid) return;
// validate short_name
let shortNameCtl = $('input[name="shortName"]')[0];
let shortNameCtl = form.find('input[name="shortName"]')[0];
let shortName = shortNameCtl.value;
if (safeRegex.test(shortName)) {
shortNameCtl.setCustomValidity('');
@@ -40,7 +41,8 @@ onLoad(() => {
valid = false;
shortNameCtl.setCustomValidity(msg('invalid_character'));
}
if (!valid) return;
// if (!valid) return;
// ...
});
for(let name of ['startDate', 'endDate']) {
@@ -99,37 +101,38 @@ onLoad(() => {
$('#tournament-infos').on('submit', e => {
e.preventDefault();
let form = e.target;
let tour = {
name: formValue('name'),
shortName: formValue('shortName'),
startDate: parseDate(formValue('startDate')),
endDate: parseDate(formValue('endDate')),
type: formValue('type'),
rounds: formValue('rounds'),
country: formValue('country'),
online: formValue('online'),
location: formValue('online') ? "" : formValue('location'),
name: form.val('name'),
shortName: form.val('shortName'),
startDate: parseDate(form.val('startDate')),
endDate: parseDate(form.val('endDate')),
type: form.val('type'),
rounds: form.val('rounds'),
country: form.val('country'),
online: form.val('online'),
location: form.val('online') ? "" : form.val('location'),
pairing: {
type: formValue('pairing'),
// mmFloor: formValue('mmFloor'),
mmBar: formValue('mmBar'),
type: form.val('pairing'),
// mmFloor: form.val('mmFloor'),
mmBar: form.val('mmBar'),
main: {
firstSeed: formValue('firstSeed'),
secondSeed: formValue('secondSeed')
firstSeed: form.val('firstSeed'),
secondSeed: form.val('secondSeed')
},
handicap: {
correction: formValue('correction'),
treshold: formValue('treshold')
correction: form.val('correction'),
treshold: form.val('treshold')
}
},
timeSystem: {
type: formValue('timeSystemType'),
mainTime: fromHMS(formValue('mainTime')),
increment: fromHMS(formValue('increment')),
maxTime: fromHMS(formValue('maxTime')),
byoyomi: fromHMS(formValue('byoyomi')),
periods: formValue('periods'),
stones: formValue('stones')
type: form.val('timeSystemType'),
mainTime: fromHMS(form.val('mainTime')),
increment: fromHMS(form.val('increment')),
maxTime: fromHMS(form.val('maxTime')),
byoyomi: fromHMS(form.val('byoyomi')),
periods: form.val('periods'),
stones: form.val('stones')
}
}
console.log(tour);

View File

@@ -5,5 +5,39 @@ onLoad(() => {
min: 0,
max: 4000
});
//$('')
$('#register').on('click', e => {
let form = e.target.closest('form');
let required = ['name', 'firstname', 'country', 'club', 'rank', 'rating'];
for (let name of required) {
let ctl = form.find(`[name=${name}]`)[0];
let val = ctl.value;
if (val) {
ctl.setCustomValidity('');
} else {
valid = false;
ctl.setCustomValidity(msg('required_field'));
}
}
if (!valid) return;
});
$('#player-form').on('submit', e => {
e.preventDefault();
let form = e.target;
let player = {
name: form.val('name'),
firstname: form.val('firstname'),
rating: form.val('rating'),
rank: form.val('rank'),
country: form.val('country'),
club: form.val('club')
}
console.log(player);
api.postJson(`tour/${tour_id}/part`, player)
.then(player => {
console.log(player)
if (player !== 'error') {
window.location.reload();
}
});
});
});

View File

@@ -14,8 +14,8 @@ $parts
</div>
<div id="player" class="popup">
<div class="popup-body">
<div class="popup-content">
<form id="player-form" class="ui form edit">
<form id="player-form" class="ui form edit">
<div class="popup-content">
<div class="four stackable fields">
<div class="twelve wide field">
<div class="ui icon input">
@@ -54,7 +54,7 @@ $parts
<div class="eight wide field">
<label>Given name</label>
<span class="info"></span>
<input type="text" name="name" placeholder="first name"/>
<input type="text" name="firstname" placeholder="first name"/>
</div>
</div>
<div class="two stackable fields">
@@ -72,7 +72,7 @@ $parts
<div class="eight wide field">
<label>Club</label>
<span class="info"></span>
<input type="text" name="name" placeholder="club"/>
<input type="text" name="club" placeholder="club"/>
</div>
</div>
<div class="two stackable fields">
@@ -90,18 +90,18 @@ $parts
<input name="rating" type="text" class="numeric"/>
</div>
</div>
</form>
</div>
<div class="popup-footer">
<button class="ui gray right labeled icon floating close button">
<i class="times icon"></i>
Cancel
</button>
<button class="ui green right labeled icon floating add button">
<i class="plus icon"></i>
Register
</button>
</div>
</div>
<div class="popup-footer">
<button class="ui gray right labeled icon floating close button">
<i class="times icon"></i>
Cancel
</button>
<button id="register" class="ui green right labeled icon floating button">
<i class="plus icon"></i>
Register
</button>
</div>
</form>
</div>
</div>
<script type="text/javascript">