diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index 3876919b..43c21c71 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -184,7 +184,7 @@ class UserRegistrar( @API("/keychip") @Doc("Get a Keychip ID so that the user can connect to the server.", "Success message") suspend fun setupConnection(@RP token: Str) = jwt.auth(token) { u -> - if (u.keychip != null) return mapOf("keychip" to u.keychip) + u.keychip?.let { return mapOf("keychip" to it) } log.info("Net: /user/keychip setup: ${u.auId} for ${u.username}") // Generate a keychip id with 10 digits (e.g. A1234567890) diff --git a/src/main/java/icu/samnyan/aqua/net/components/JWT.kt b/src/main/java/icu/samnyan/aqua/net/components/JWT.kt index 3ec397cb..4023e751 100644 --- a/src/main/java/icu/samnyan/aqua/net/components/JWT.kt +++ b/src/main/java/icu/samnyan/aqua/net/components/JWT.kt @@ -73,5 +73,5 @@ class JWT( fun auth(token: Str) = parse(token) ?: (400 - "Invalid token") - final inline fun auth(token: Str, block: (AquaNetUser) -> Any) = block(auth(token)) + final inline fun auth(token: Str, block: (AquaNetUser) -> T) = block(auth(token)) } \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt index e2ebde91..d1923195 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt @@ -76,7 +76,7 @@ class AquaNetUser( @OneToOne(cascade = [CascadeType.ALL]) @JoinColumn(name = "gameOptions", unique = true, nullable = true) - var gameOptions: AquaGameOptions? = null + var gameOptions: AquaGameOptions? = null, ) : Serializable { val computedName get() = displayName.ifEmpty { username } diff --git a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt index 73831eaa..fcadbcb6 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt @@ -31,16 +31,26 @@ abstract class GameApiController(name: String, userDataClass: KCl playlogRepo.findByUserCardExtId(card.extId) } - private var rankingCache: Pair>? = null + private var rankingCache: MutableMap>> = mutableMapOf() + private val rankingCacheDuration = 240_000 @API("ranking") - fun ranking(): List { - // Read from cache if we just computed it less than 2 minutes ago - rankingCache?.let { (t, r) -> - if (millis() - t < 120_000) return r + fun ranking(@RP token: String?): List { + val reqUser = token?.let { us.jwt.auth(it) { u -> + // Optimization: If the user is not banned, we don't need to process user information + if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned }) null + else u + } } + val cacheKey = reqUser?.auId ?: -1 + + // Read from cache if we just computed it less than duration ago + rankingCache[cacheKey]?.let { (t, r) -> + if (millis() - t < rankingCacheDuration) return r } // TODO: pagination + // Shadow-ban: Do not show banned cards in the ranking except for the user who owns the card val players = userDataRepo.findAll().sortedByDescending { it.playerRating } + .filter { it.card?.rankingBanned != true || it.card?.aquaUser?.let { it == reqUser } ?: false } return players.filter { it.card != null }.mapIndexed { i, user -> val plays = playlogRepo.findByUserCardExtId(user.card!!.extId) @@ -54,7 +64,7 @@ abstract class GameApiController(name: String, userDataClass: KCl lastSeen = user.lastPlayDate.toString(), username = user.card!!.aquaUser?.username ?: "user${user.card!!.id}" ) - }.also { rankingCache = millis() to it } // Update the cache + }.also { rankingCache[cacheKey] = millis() to it } // Update cache } @API("playlog") diff --git a/src/main/java/icu/samnyan/aqua/net/games/Models.kt b/src/main/java/icu/samnyan/aqua/net/games/Models.kt index 6c8f7a82..c71e9fd8 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/Models.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/Models.kt @@ -125,7 +125,7 @@ open class UserDataEntity : BaseEntity() { interface GenericUserDataRepo : JpaRepository { fun findByCard(card: Card): T? fun findByCard_ExtId(extId: Long): Optional - @Query("select count(*) from #{#entityName} where playerRating > :rating") + @Query("select count(*) from #{#entityName} e where e.playerRating > :rating and e.card.rankingBanned = false") fun getRanking(rating: Int): Long } diff --git a/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt b/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt index 740a5ea8..bc461438 100644 --- a/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt +++ b/src/main/java/icu/samnyan/aqua/sega/general/model/Card.kt @@ -39,8 +39,11 @@ class Card( var aquaUser: AquaNetUser? = null, // Whether the card is a ghost card - @Column(name = "is_ghost") var isGhost: Boolean = false, + + // Unfortunately some people decide to cheat and upload all perfect scores :( + // This will not affect gameplay behavior, but will hide the user from ranking + var rankingBanned: Boolean = false, ) { @Suppress("unused") // Used by serialization val isLinked get() = aquaUser != null diff --git a/src/main/resources/db/migration/mariadb/V1000_14__ranking_ban.sql b/src/main/resources/db/migration/mariadb/V1000_14__ranking_ban.sql new file mode 100644 index 00000000..0d0e60d4 --- /dev/null +++ b/src/main/resources/db/migration/mariadb/V1000_14__ranking_ban.sql @@ -0,0 +1 @@ +ALTER TABLE sega_card ADD COLUMN `ranking_banned` BIT(1) NOT NULL DEFAULT 0; \ No newline at end of file