Merge branch 'agent'
This commit is contained in:
commit
5c15066ebf
220
nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/Agent.kt
Normal file
220
nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/Agent.kt
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package com.nisemoe.nise.scheduler
|
||||||
|
|
||||||
|
import com.nisemoe.generated.tables.records.ScoresRecord
|
||||||
|
import com.nisemoe.generated.tables.records.UsersRecord
|
||||||
|
import com.nisemoe.generated.tables.references.SCORES
|
||||||
|
import com.nisemoe.generated.tables.references.USERS
|
||||||
|
import com.nisemoe.nise.osu.OsuApi
|
||||||
|
import org.jooq.DSLContext
|
||||||
|
import org.springframework.context.annotation.Profile
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
data class UserReport(
|
||||||
|
val username: String,
|
||||||
|
val susScore: Double,
|
||||||
|
val urgencyScore: Double,
|
||||||
|
val userId: Long,
|
||||||
|
val userScores: List<Agent.ScoreMetrics>
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val s = "User: $username (https://nise.moe/u/${username})- Suspiciousness score: ${susScore.roundToInt()} | Urgency score: ${urgencyScore.roundToInt()}"
|
||||||
|
val s2 = "Average PP: ${userScores.map { it.pp }.average()} | Average UR: ${userScores.map { it.adjustedUr }.average()} | Average Sliderend Release: ${userScores.map { it.sliderendReleaseStandardDeviationAdjusted }.average()}"
|
||||||
|
return s + "\n" + s2 + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Profile("agent")
|
||||||
|
class Agent(
|
||||||
|
private val dslContext: DSLContext,
|
||||||
|
private val osuApi: OsuApi
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 1200000, initialDelay = 0)
|
||||||
|
fun buildReports() {
|
||||||
|
val reports = mutableListOf<UserReport>()
|
||||||
|
|
||||||
|
val userIds = dslContext.select(SCORES.USER_ID)
|
||||||
|
.from(SCORES)
|
||||||
|
.where(SCORES.ADJUSTED_UR.lessOrEqual(256.0))
|
||||||
|
.and(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED.isNotNull)
|
||||||
|
.and(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED.lessOrEqual(16.0))
|
||||||
|
.and(SCORES.IS_BANNED.isFalse)
|
||||||
|
.and(SCORES.VERSION.greaterOrEqual(4))
|
||||||
|
.groupBy(SCORES.USER_ID)
|
||||||
|
.fetchInto(Long::class.java)
|
||||||
|
|
||||||
|
println("Found ${userIds.size} users to check.")
|
||||||
|
|
||||||
|
// We check if any of them are banned.
|
||||||
|
// val bannedIds = mutableListOf<Long>()
|
||||||
|
// for(chunk in userIds.chunked(50)) {
|
||||||
|
//
|
||||||
|
// val result = osuApi.getUsersBatch(chunk)
|
||||||
|
// if(result == null) {
|
||||||
|
// println("Failed to get users from osu! api")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Compare which userIds aren't in the result and mark them as banned.
|
||||||
|
// val bannedUsers = chunk.filter { it !in result.users.map { it.id } }
|
||||||
|
// bannedIds.addAll(bannedUsers)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if(bannedIds.isNotEmpty()) {
|
||||||
|
// // Remove all banned users from the list
|
||||||
|
// susUsers.removeIf { it.userId in bannedIds }
|
||||||
|
//
|
||||||
|
// // Mark all their scores as banned
|
||||||
|
// dslContext.update(SCORES)
|
||||||
|
// .set(SCORES.IS_BANNED, true)
|
||||||
|
// .where(SCORES.USER_ID.`in`(bannedIds))
|
||||||
|
// .execute()
|
||||||
|
//
|
||||||
|
// println("Marked ${bannedIds.size} users as banned: $bannedIds")
|
||||||
|
// }
|
||||||
|
|
||||||
|
for(userId in userIds) {
|
||||||
|
val userDetails = dslContext.selectFrom(USERS)
|
||||||
|
.where(USERS.USER_ID.eq(userId))
|
||||||
|
.fetchOneInto(UsersRecord::class.java) ?: continue
|
||||||
|
|
||||||
|
val username = userDetails.username!!
|
||||||
|
val userScores = dslContext.select(
|
||||||
|
SCORES.DATE,
|
||||||
|
SCORES.ADJUSTED_UR,
|
||||||
|
SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED,
|
||||||
|
SCORES.PP,
|
||||||
|
SCORES.EDGE_HITS,
|
||||||
|
SCORES.SNAPS,
|
||||||
|
SCORES.KEYPRESSES_MEDIAN_ADJUSTED,
|
||||||
|
SCORES.ERROR_KURTOSIS
|
||||||
|
)
|
||||||
|
.from(SCORES)
|
||||||
|
.where(SCORES.ADJUSTED_UR.lessOrEqual(256.0))
|
||||||
|
.and(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED.isNotNull)
|
||||||
|
.and(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED.lessOrEqual(16.0))
|
||||||
|
.and(SCORES.IS_BANNED.isFalse)
|
||||||
|
.and(SCORES.VERSION.greaterOrEqual(4))
|
||||||
|
.and(SCORES.USER_ID.eq(userId))
|
||||||
|
.fetchInto(ScoresRecord::class.java)
|
||||||
|
|
||||||
|
if(areMetricsBlatant(userScores.map { mapScoreToMetrics(userDetails, it) }))
|
||||||
|
continue
|
||||||
|
|
||||||
|
val susScore = calculateSuspiciousnessScore(userScores.map { mapScoreToMetrics(userDetails, it) })
|
||||||
|
|
||||||
|
val newReport = UserReport(
|
||||||
|
userId = userId,
|
||||||
|
username = username,
|
||||||
|
userScores = userScores.map { mapScoreToMetrics(userDetails, it) },
|
||||||
|
susScore = susScore.first,
|
||||||
|
urgencyScore = susScore.second
|
||||||
|
)
|
||||||
|
reports.add(newReport)
|
||||||
|
println(newReport)
|
||||||
|
}
|
||||||
|
|
||||||
|
reports.sortByDescending { it.urgencyScore }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapScoreToMetrics(user: UsersRecord, score: ScoresRecord): ScoreMetrics {
|
||||||
|
return ScoreMetrics(
|
||||||
|
date = score.date!!,
|
||||||
|
country = user.country!!,
|
||||||
|
pp = score.pp!!,
|
||||||
|
adjustedUr = score.adjustedUr!!,
|
||||||
|
sliderendReleaseStandardDeviationAdjusted = score.sliderendReleaseStandardDeviationAdjusted!!,
|
||||||
|
edgeHits = score.edgeHits!!,
|
||||||
|
snaps = score.snaps!!,
|
||||||
|
keypressesMedianAdjusted = score.keypressesMedianAdjusted!!,
|
||||||
|
errorKurtosis = score.errorKurtosis!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ScoreMetrics(
|
||||||
|
val date: LocalDateTime,
|
||||||
|
val country: String,
|
||||||
|
val pp: Double,
|
||||||
|
val adjustedUr: Double,
|
||||||
|
val sliderendReleaseStandardDeviationAdjusted: Double,
|
||||||
|
val edgeHits: Int,
|
||||||
|
val snaps: Int,
|
||||||
|
val keypressesMedianAdjusted: Double,
|
||||||
|
val errorKurtosis: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
val ppWeight = 1.0
|
||||||
|
val urWeight = 2.0
|
||||||
|
val sliderendReleaseWeight = 2.0
|
||||||
|
val edgeHitsWeight = 2.0
|
||||||
|
val snapsWeight = 2.0
|
||||||
|
val keypressesWeight = 2.0
|
||||||
|
val errorKurtosisWeight = 1.0
|
||||||
|
val recencyBoost = 10.0
|
||||||
|
val spreadBoost = 5.0
|
||||||
|
|
||||||
|
fun areMetricsBlatant(metrics: List<ScoreMetrics>): Boolean {
|
||||||
|
return metrics.map { it.adjustedUr }.average() < 24.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun calculateSuspiciousnessScore(scores: List<ScoreMetrics>): Pair<Double, Double> {
|
||||||
|
if (scores.isEmpty()) return Pair(0.0, 0.0)
|
||||||
|
|
||||||
|
val currentDate = LocalDateTime.now()
|
||||||
|
val mostRecentScoreDate = scores.maxByOrNull { it.date }?.date ?: return Pair(0.0, 0.0)
|
||||||
|
val daysSinceMostRecentScore = ChronoUnit.DAYS.between(mostRecentScoreDate, currentDate)
|
||||||
|
val recencyScore = if (daysSinceMostRecentScore <= 30) recencyBoost else 0.0
|
||||||
|
|
||||||
|
val dateSpread = scores.map { score -> ChronoUnit.MONTHS.between(score.date, currentDate) }.average()
|
||||||
|
val spreadScore = if (dateSpread > 12) spreadBoost else 0.0
|
||||||
|
|
||||||
|
val (suspiciousnessScore, ppTotal) = scores.fold(Pair(0.0, 0.0)) { acc, score ->
|
||||||
|
var suspiciousness = acc.first
|
||||||
|
var ppSum = acc.second
|
||||||
|
|
||||||
|
suspiciousness += score.pp * ppWeight
|
||||||
|
|
||||||
|
if (score.adjustedUr < 6.0) {
|
||||||
|
suspiciousness += (6.0 - score.adjustedUr) * urWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if (score.sliderendReleaseStandardDeviationAdjusted < 6.0) {
|
||||||
|
suspiciousness += (6.0 - score.sliderendReleaseStandardDeviationAdjusted) * sliderendReleaseWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
suspiciousness += when {
|
||||||
|
score.edgeHits > 4 -> (score.edgeHits - 4) * edgeHitsWeight
|
||||||
|
else -> 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
suspiciousness += when {
|
||||||
|
score.snaps > 4 -> (score.snaps - 4) * snapsWeight
|
||||||
|
else -> 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (score.keypressesMedianAdjusted < 6) {
|
||||||
|
suspiciousness += (6.0 - score.keypressesMedianAdjusted) * keypressesWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if (score.errorKurtosis <= 0.0) {
|
||||||
|
suspiciousness += (6.0 - score.errorKurtosis) * errorKurtosisWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
ppSum += score.pp
|
||||||
|
Pair(suspiciousness, ppSum)
|
||||||
|
}
|
||||||
|
|
||||||
|
val averageSuspiciousnessScore = suspiciousnessScore / scores.size
|
||||||
|
val urgencyScore = ppTotal / scores.size + recencyScore + spreadScore
|
||||||
|
|
||||||
|
return Pair(averageSuspiciousnessScore, urgencyScore)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.nisemoe.nise.scheduler
|
||||||
|
|
||||||
|
import com.nisemoe.nise.database.UserService
|
||||||
|
import com.nisemoe.nise.osu.OsuApi
|
||||||
|
import com.nisemoe.nise.osu.TokenService
|
||||||
|
import com.nisemoe.nise.service.CacheService
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import org.springframework.test.context.ActiveProfiles
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("postgres", "agent")
|
||||||
|
@MockBean(GlobalCache::class, UserService::class)
|
||||||
|
@Import(Agent::class, OsuApi::class, TokenService::class, CacheService::class)
|
||||||
|
class AgentTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var agent: Agent
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildReports() {
|
||||||
|
agent.buildReports()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user