mirror of https://github.com/hykilpikonna/AquaDX
[+] Counter measure
parent
501bf06ada
commit
355c9e2a3d
|
@ -184,7 +184,7 @@ class UserRegistrar(
|
||||||
@API("/keychip")
|
@API("/keychip")
|
||||||
@Doc("Get a Keychip ID so that the user can connect to the server.", "Success message")
|
@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 ->
|
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}")
|
log.info("Net: /user/keychip setup: ${u.auId} for ${u.username}")
|
||||||
|
|
||||||
// Generate a keychip id with 10 digits (e.g. A1234567890)
|
// Generate a keychip id with 10 digits (e.g. A1234567890)
|
||||||
|
|
|
@ -73,5 +73,5 @@ class JWT(
|
||||||
|
|
||||||
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")
|
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")
|
||||||
|
|
||||||
final inline fun auth(token: Str, block: (AquaNetUser) -> Any) = block(auth(token))
|
final inline fun <T> auth(token: Str, block: (AquaNetUser) -> T) = block(auth(token))
|
||||||
}
|
}
|
|
@ -76,7 +76,7 @@ class AquaNetUser(
|
||||||
|
|
||||||
@OneToOne(cascade = [CascadeType.ALL])
|
@OneToOne(cascade = [CascadeType.ALL])
|
||||||
@JoinColumn(name = "gameOptions", unique = true, nullable = true)
|
@JoinColumn(name = "gameOptions", unique = true, nullable = true)
|
||||||
var gameOptions: AquaGameOptions? = null
|
var gameOptions: AquaGameOptions? = null,
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
val computedName get() = displayName.ifEmpty { username }
|
val computedName get() = displayName.ifEmpty { username }
|
||||||
|
|
||||||
|
|
|
@ -31,16 +31,26 @@ abstract class GameApiController<T : IUserData>(name: String, userDataClass: KCl
|
||||||
playlogRepo.findByUserCardExtId(card.extId)
|
playlogRepo.findByUserCardExtId(card.extId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var rankingCache: Pair<Long, List<GenericRankingPlayer>>? = null
|
private var rankingCache: MutableMap<Long, Pair<Long, List<GenericRankingPlayer>>> = mutableMapOf()
|
||||||
|
private val rankingCacheDuration = 240_000
|
||||||
@API("ranking")
|
@API("ranking")
|
||||||
fun ranking(): List<GenericRankingPlayer> {
|
fun ranking(@RP token: String?): List<GenericRankingPlayer> {
|
||||||
// Read from cache if we just computed it less than 2 minutes ago
|
val reqUser = token?.let { us.jwt.auth(it) { u ->
|
||||||
rankingCache?.let { (t, r) ->
|
// Optimization: If the user is not banned, we don't need to process user information
|
||||||
if (millis() - t < 120_000) return r
|
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
|
// 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 }
|
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 ->
|
return players.filter { it.card != null }.mapIndexed { i, user ->
|
||||||
val plays = playlogRepo.findByUserCardExtId(user.card!!.extId)
|
val plays = playlogRepo.findByUserCardExtId(user.card!!.extId)
|
||||||
|
|
||||||
|
@ -54,7 +64,7 @@ abstract class GameApiController<T : IUserData>(name: String, userDataClass: KCl
|
||||||
lastSeen = user.lastPlayDate.toString(),
|
lastSeen = user.lastPlayDate.toString(),
|
||||||
username = user.card!!.aquaUser?.username ?: "user${user.card!!.id}"
|
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")
|
@API("playlog")
|
||||||
|
|
|
@ -125,7 +125,7 @@ open class UserDataEntity : BaseEntity() {
|
||||||
interface GenericUserDataRepo<T : IUserData> : JpaRepository<T, Long> {
|
interface GenericUserDataRepo<T : IUserData> : JpaRepository<T, Long> {
|
||||||
fun findByCard(card: Card): T?
|
fun findByCard(card: Card): T?
|
||||||
fun findByCard_ExtId(extId: Long): Optional<T>
|
fun findByCard_ExtId(extId: Long): Optional<T>
|
||||||
@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
|
fun getRanking(rating: Int): Long
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,8 +39,11 @@ class Card(
|
||||||
var aquaUser: AquaNetUser? = null,
|
var aquaUser: AquaNetUser? = null,
|
||||||
|
|
||||||
// Whether the card is a ghost card
|
// Whether the card is a ghost card
|
||||||
@Column(name = "is_ghost")
|
|
||||||
var isGhost: Boolean = false,
|
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
|
@Suppress("unused") // Used by serialization
|
||||||
val isLinked get() = aquaUser != null
|
val isLinked get() = aquaUser != null
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE sega_card ADD COLUMN `ranking_banned` BIT(1) NOT NULL DEFAULT 0;
|
Loading…
Reference in New Issue