More work on getting a basic agent implementation to work
This commit is contained in:
parent
95650c5ca6
commit
0067642359
@ -1,96 +1,220 @@
|
|||||||
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() {
|
||||||
|
|
||||||
// Select sus scores
|
|
||||||
val susUsers = dslContext.select(
|
|
||||||
SCORES.USER_ID,
|
|
||||||
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(10.0))
|
|
||||||
.and(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED.lessOrEqual(8.0))
|
|
||||||
.and(SCORES.IS_BANNED.isFalse)
|
|
||||||
.and(SCORES.VERSION.greaterOrEqual(4))
|
|
||||||
.fetchInto(ScoresRecord::class.java)
|
|
||||||
|
|
||||||
val reports = mutableListOf<UserReport>()
|
val reports = mutableListOf<UserReport>()
|
||||||
|
|
||||||
for(user in susUsers.groupBy { it.userId }) {
|
val userIds = dslContext.select(SCORES.USER_ID)
|
||||||
val userId = user.key!!
|
.from(SCORES)
|
||||||
val username = dslContext.select(USERS.USERNAME)
|
.where(SCORES.ADJUSTED_UR.lessOrEqual(256.0))
|
||||||
.from(USERS)
|
.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))
|
.where(USERS.USER_ID.eq(userId))
|
||||||
.fetchOne(USERS.USERNAME) ?: continue
|
.fetchOneInto(UsersRecord::class.java) ?: continue
|
||||||
|
|
||||||
val userScores = user.value
|
val username = userDetails.username!!
|
||||||
val susScore = calculateSuspiciousnessScore(userScores)
|
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)
|
||||||
|
|
||||||
reports.add(UserReport(userId = userId, username = username, userScores = userScores, susScore = susScore))
|
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.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
|
if (score.errorKurtosis <= 0.0) {
|
||||||
|
suspiciousness += (6.0 - score.errorKurtosis) * errorKurtosisWeight
|
||||||
|
}
|
||||||
|
|
||||||
suspiciousnessScore
|
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