More work on getting a basic agent implementation to work

This commit is contained in:
nise.moe 2024-02-16 17:47:45 +01:00
parent 95650c5ca6
commit 0067642359
2 changed files with 200 additions and 48 deletions

View File

@ -1,33 +1,93 @@
package com.nisemoe.nise.scheduler package com.nisemoe.nise.scheduler
import com.nisemoe.generated.tables.records.ScoresRecord 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.SCORES
import com.nisemoe.generated.tables.references.USERS import com.nisemoe.generated.tables.references.USERS
import com.nisemoe.nise.osu.OsuApi
import org.jooq.DSLContext import org.jooq.DSLContext
import org.springframework.context.annotation.Profile import org.springframework.context.annotation.Profile
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
data class UserReport( data class UserReport(
val username: String, val username: String,
val susScore: Double, val susScore: Double,
val urgencyScore: Double,
val userId: Long, val userId: Long,
val userScores: List<ScoresRecord> 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 @Service
@Profile("agent") @Profile("agent")
class Agent( class Agent(
private val dslContext: DSLContext private val dslContext: DSLContext,
private val osuApi: OsuApi
) { ) {
// 20 minutes to ms = 1200000
@Scheduled(fixedDelay = 1200000, initialDelay = 0) @Scheduled(fixedDelay = 1200000, initialDelay = 0)
fun buildReports() { fun buildReports() {
val reports = mutableListOf<UserReport>()
// Select sus scores val userIds = dslContext.select(SCORES.USER_ID)
val susUsers = dslContext.select( .from(SCORES)
SCORES.USER_ID, .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.ADJUSTED_UR,
SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED,
SCORES.PP, SCORES.PP,
@ -37,60 +97,124 @@ class Agent(
SCORES.ERROR_KURTOSIS SCORES.ERROR_KURTOSIS
) )
.from(SCORES) .from(SCORES)
.where(SCORES.ADJUSTED_UR.lessOrEqual(10.0)) .where(SCORES.ADJUSTED_UR.lessOrEqual(256.0))
.and(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED.lessOrEqual(8.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.IS_BANNED.isFalse)
.and(SCORES.VERSION.greaterOrEqual(4)) .and(SCORES.VERSION.greaterOrEqual(4))
.and(SCORES.USER_ID.eq(userId))
.fetchInto(ScoresRecord::class.java) .fetchInto(ScoresRecord::class.java)
val reports = mutableListOf<UserReport>() if(areMetricsBlatant(userScores.map { mapScoreToMetrics(userDetails, it) }))
continue
for(user in susUsers.groupBy { it.userId }) { val susScore = calculateSuspiciousnessScore(userScores.map { mapScoreToMetrics(userDetails, it) })
val userId = user.key!!
val username = dslContext.select(USERS.USERNAME)
.from(USERS)
.where(USERS.USER_ID.eq(userId))
.fetchOne(USERS.USERNAME) ?: continue
val userScores = user.value val newReport = UserReport(
val susScore = calculateSuspiciousnessScore(userScores) userId = userId,
username = username,
reports.add(UserReport(userId = userId, username = username, userScores = userScores, susScore = susScore)) userScores = userScores.map { mapScoreToMetrics(userDetails, it) },
susScore = susScore.first,
urgencyScore = susScore.second
)
reports.add(newReport)
println(newReport)
} }
reports.sortByDescending { it.susScore } reports.sortByDescending { it.urgencyScore }
println(reports)
} }
fun calculateSuspiciousnessScore(scores: List<ScoresRecord>): Double { fun mapScoreToMetrics(user: UsersRecord, score: ScoresRecord): ScoreMetrics {
if (scores.isEmpty()) return 0.0 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!!
)
}
return scores.sumOf { score -> data class ScoreMetrics(
var suspiciousnessScore = 0.0 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
)
suspiciousnessScore += score.pp!! * 1.0 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
suspiciousnessScore += if (score.adjustedUr!! < 6.0) 5.0 else 0.0 fun areMetricsBlatant(metrics: List<ScoreMetrics>): Boolean {
return metrics.map { it.adjustedUr }.average() < 24.0
}
suspiciousnessScore += if (score.sliderendReleaseStandardDeviationAdjusted!! < 6) 5.0 else 0.0 fun calculateSuspiciousnessScore(scores: List<ScoreMetrics>): Pair<Double, Double> {
if (scores.isEmpty()) return Pair(0.0, 0.0)
suspiciousnessScore += when { val currentDate = LocalDateTime.now()
score.edgeHits!! > 4 -> (score.edgeHits!! - 4) * 2.0 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 else -> 0.0
} }
suspiciousnessScore += when { suspiciousness += when {
score.snaps!! > 4 -> (score.snaps!! - 4) * 2.0 score.snaps > 4 -> (score.snaps - 4) * snapsWeight
else -> 0.0 else -> 0.0
} }
suspiciousnessScore += if (score.keypressesMedianAdjusted!! < 6) 5.0 else 0.0 if (score.keypressesMedianAdjusted < 6) {
suspiciousness += (6.0 - score.keypressesMedianAdjusted) * keypressesWeight
suspiciousnessScore += if (score.errorKurtosis!! < 6) 5.0 else 0.0
suspiciousnessScore
} }
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)
} }
} }

View File

@ -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()
}
}