Compare commits

..

No commits in common. "38e997cba214a301595ffec428ee78d921582970" and "34a05015e2cf0711c54eb16dc19c6d3242293e12" have entirely different histories.

35 changed files with 279 additions and 447 deletions

View File

@ -141,7 +141,6 @@ data class ReplayData(
val beatmap_count_sliders: Int?, val beatmap_count_sliders: Int?,
val beatmap_count_spinners: Int?, val beatmap_count_spinners: Int?,
val score: Int, val score: Int,
val mods_bitwise: Int,
val mods: List<String>, val mods: List<String>,
val rank: String?, val rank: String?,
val ur: Double?, val ur: Double?,

View File

@ -1,22 +0,0 @@
package com.nisemoe.nise.config
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.jdbc.DataSourceBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.transaction.annotation.EnableTransactionManagement
import javax.sql.DataSource
@Configuration
@EnableTransactionManagement
class DataSourceConfig {
@Primary
@Bean(name = ["niseDataSource"])
@ConfigurationProperties(prefix = "spring.datasource.nise")
fun niseDataSource(): DataSource = DataSourceBuilder.create().build()
@Bean(name = ["replayCacheDataSource"])
@ConfigurationProperties(prefix = "spring.datasource.replay-cache")
fun replayCacheDataSource(): DataSource = DataSourceBuilder.create().build()
}

View File

@ -1,22 +0,0 @@
package com.nisemoe.nise.controller
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
data class HealthResponse(
val healthy: Boolean,
)
val healthResponse = HealthResponse(
healthy = true,
)
@RestController
class HealthController {
@GetMapping("/health")
fun healthCheck(): ResponseEntity<HealthResponse> {
return ResponseEntity.ok(healthResponse)
}
}

View File

@ -1,19 +0,0 @@
package com.nisemoe.nise.controller
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
data class VersionResponse(
val version: String,
)
val versionResponse = VersionResponse(
version = "v20250213",
)
@RestController
class VersionController {
@GetMapping("/version")
fun getVersion(): ResponseEntity<VersionResponse> = ResponseEntity.ok(versionResponse)
}

View File

@ -1,46 +0,0 @@
package com.nisemoe.nise.database
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import javax.sql.DataSource
data class ReplayCacheReplay(
val replayId: Long,
val mapId: Int,
val userId: Int,
val replayData: ByteArray,
val mods: Int,
)
@Service
class ReplayCacheService(
@Qualifier("replayCacheDataSource") private val dataSource: DataSource,
) {
fun getReplayById(replayId: Long): ByteArray? =
dataSource.connection.use { connection ->
val statement = connection.prepareStatement("SELECT replay_data FROM replays WHERE replay_id = ?")
statement.setLong(1, replayId)
val resultSet = statement.executeQuery()
var replayData: ByteArray? = null
while (resultSet.next()) {
replayData = resultSet.getBytes(1)
}
return replayData
}
fun insertReplay(replay: ReplayCacheReplay): Boolean =
dataSource.connection.use { connection ->
val statement = connection.prepareStatement("INSERT INTO replays VALUES (?, ?, ?, ?, ?)")
statement.setLong(1, replay.replayId)
statement.setInt(2, replay.mapId)
statement.setInt(3, replay.userId)
statement.setBytes(4, replay.replayData)
statement.setInt(5, replay.mods)
val updateCount = statement.executeUpdate()
return updateCount != 0
}
}

View File

@ -177,8 +177,6 @@ class ScoreService(
val hitDistribution = this.getHitDistribution(scoreId = result.get(SCORES.ID, Int::class.java)) val hitDistribution = this.getHitDistribution(scoreId = result.get(SCORES.ID, Int::class.java))
val charts = this.getCharts(result) val charts = this.getCharts(result)
val mods = result.get(SCORES.MODS, Int::class.java)
val replayData = ReplayData( val replayData = ReplayData(
replay_id = replayId, replay_id = replayId,
user_id = result.get(SCORES.USER_ID, Int::class.java), user_id = result.get(SCORES.USER_ID, Int::class.java),
@ -206,8 +204,7 @@ class ScoreService(
ur = result.get(SCORES.UR, Double::class.java), ur = result.get(SCORES.UR, Double::class.java),
adjusted_ur = result.get(SCORES.ADJUSTED_UR, Double::class.java), adjusted_ur = result.get(SCORES.ADJUSTED_UR, Double::class.java),
score = result.get(SCORES.SCORE, Int::class.java), score = result.get(SCORES.SCORE, Int::class.java),
mods_bitwise = mods, mods = Mod.parseModCombination(result.get(SCORES.MODS, Int::class.java)),
mods = Mod.parseModCombination(mods),
rank = result.get(SCORES.RANK, String::class.java), rank = result.get(SCORES.RANK, String::class.java),
snaps = result.get(SCORES.SNAPS, Int::class.java), snaps = result.get(SCORES.SNAPS, Int::class.java),
hits = result.get(SCORES.EDGE_HITS, Int::class.java), hits = result.get(SCORES.EDGE_HITS, Int::class.java),
@ -235,7 +232,7 @@ class ScoreService(
} }
fun getDefaultCondition(): Condition { fun getDefaultCondition(): Condition {
return SCORES.UR.lessOrEqual(35.0) return SCORES.UR.lessOrEqual(25.0)
.and(SCORES.IS_BANNED.eq(false)) .and(SCORES.IS_BANNED.eq(false))
} }

View File

@ -76,8 +76,6 @@ class UserScoreService(
val hitDistribution = this.getHitDistribution(result.get(USER_SCORES.JUDGEMENTS, ByteArray::class.java)) val hitDistribution = this.getHitDistribution(result.get(USER_SCORES.JUDGEMENTS, ByteArray::class.java))
val charts = this.scoreService.getCharts(result) val charts = this.scoreService.getCharts(result)
val mods = result.get(USER_SCORES.MODS, Int::class.java)
val replayData = ReplayData( val replayData = ReplayData(
replay_id = result.get(USER_SCORES.ONLINE_SCORE_ID, Long::class.java), replay_id = result.get(USER_SCORES.ONLINE_SCORE_ID, Long::class.java),
username = result.get(USER_SCORES.PLAYER_NAME, String::class.java), username = result.get(USER_SCORES.PLAYER_NAME, String::class.java),
@ -102,8 +100,7 @@ class UserScoreService(
ur = result.get(USER_SCORES.UR, Double::class.java), ur = result.get(USER_SCORES.UR, Double::class.java),
adjusted_ur = result.get(USER_SCORES.ADJUSTED_UR, Double::class.java), adjusted_ur = result.get(USER_SCORES.ADJUSTED_UR, Double::class.java),
score = result.get(USER_SCORES.TOTAL_SCORE, Int::class.java), score = result.get(USER_SCORES.TOTAL_SCORE, Int::class.java),
mods_bitwise = mods, mods = Mod.parseModCombination(result.get(USER_SCORES.MODS, Int::class.java)),
mods = Mod.parseModCombination(mods),
snaps = result.get(USER_SCORES.SNAPS, Int::class.java), snaps = result.get(USER_SCORES.SNAPS, Int::class.java),
hits = result.get(USER_SCORES.EDGE_HITS, Int::class.java), hits = result.get(USER_SCORES.EDGE_HITS, Int::class.java),
perfect = result.get(USER_SCORES.PERFECT, Boolean::class.java), perfect = result.get(USER_SCORES.PERFECT, Boolean::class.java),

View File

@ -70,14 +70,13 @@ class OsuApi(
.version(HttpClient.Version.HTTP_2) .version(HttpClient.Version.HTTP_2)
.build() .build()
fun doRequest(url: String, queryParams: Map<String, Any?>, authorized: Boolean = true, appendToUrl: String? = null): HttpResponse<String>? { fun doRequest(url: String, queryParams: Map<String, Any>, authorized: Boolean = true, appendToUrl: String? = null): HttpResponse<String>? {
var accessToken: TokenService.AccessTokenResponse? = null var accessToken: TokenService.AccessTokenResponse? = null
if(authorized) if(authorized)
accessToken = this.tokenService.getAccessToken() accessToken = this.tokenService.getAccessToken()
val uriBuilder = StringBuilder(url) val uriBuilder = StringBuilder(url)
queryParams.forEach { (key, value) -> queryParams.forEach { (key, value) ->
if (value != null)
uriBuilder.append("$key=$value&") uriBuilder.append("$key=$value&")
} }
@ -137,19 +136,6 @@ class OsuApi(
} }
} }
fun getBeatmapFromId(beatmapId: Int): OsuApiModels.Beatmap? {
val response = doRequest("https://osu.ppy.sh/api/v2/beatmaps/$beatmapId", emptyMap())
if (response == null) {
this.logger.info("Error loading beatmap $beatmapId")
return null
}
return when (response.statusCode()) {
200 -> serializer.decodeFromString<OsuApiModels.Beatmap>(response.body())
else -> null
}
}
/** /**
* Retrieves the replay data for a given score ID from the osu!api. * Retrieves the replay data for a given score ID from the osu!api.
* Efficiently cycles through the API keys to avoid rate limiting. * Efficiently cycles through the API keys to avoid rate limiting.
@ -227,20 +213,6 @@ class OsuApi(
} }
} }
fun getUserBeatmapScores(userId: Long, beatmapId: Int): OsuApiModels.BeatmapScores? {
val response = doRequest("https://osu.ppy.sh/api/v2/beatmaps/$beatmapId/scores/users/$userId/all", emptyMap())
if(response == null) {
this.logger.info("Error getting scores on beatmap $beatmapId for user $userId")
return null
}
return when (response.statusCode()) {
200 -> serializer.decodeFromString<OsuApiModels.BeatmapScores>(response.body())
else -> null
}
}
fun searchBeatmapsets(cursor: OsuApiModels.BeatmapsetSearchResultCursor?): OsuApiModels.BeatmapsetSearchResult? { fun searchBeatmapsets(cursor: OsuApiModels.BeatmapsetSearchResultCursor?): OsuApiModels.BeatmapsetSearchResult? {
val queryParams = mutableMapOf( val queryParams = mutableMapOf(
"s" to "ranked", // Status [only ranked] "s" to "ranked", // Status [only ranked]
@ -262,7 +234,7 @@ class OsuApi(
} }
fun checkIfUserBanned(userId: Long): Boolean? { fun checkIfUserBanned(userId: Long): Boolean? {
val response = this.doRequest("https://osu.ppy.sh/api/v2/users/$userId/osu?key=id", emptyMap()) val response = this.doRequest("https://osu.ppy.sh/api/v2/users/$userId/osu?key=id", mapOf())
if(response == null) { if(response == null) {
this.logger.info("Error loading user with userId = $userId") this.logger.info("Error loading user with userId = $userId")
return null return null
@ -321,24 +293,6 @@ class OsuApi(
} }
} }
fun getUserMostPlayed(userId: Long, limit: Int? = null, offset: Int? = null): List<OsuApiModels.BeatmapPlaycount>? {
val queryParams = mapOf(
"limit" to limit,
"offset" to offset,
)
val response = this.doRequest("https://osu.ppy.sh/api/v2/users/$userId/beatmapsets/most_played/?", queryParams)
if (response == null) {
this.logger.info("Error getting user most played ($userId)")
return null
}
return when (response.statusCode()) {
200 -> serializer.decodeFromString<List<OsuApiModels.BeatmapPlaycount>>(response.body())
else -> null
}
}
var rateLimitRemaining: Long = 0L var rateLimitRemaining: Long = 0L
var rateLimitTotal: Long = 0L var rateLimitTotal: Long = 0L

View File

@ -39,7 +39,6 @@ class OsuApiModels {
val avatar_url: String, val avatar_url: String,
val id: Long, val id: Long,
val username: String, val username: String,
val beatmap_playcounts_count: Int?,
// Documentation: https://osu.ppy.sh/docs/index.html#userextended // Documentation: https://osu.ppy.sh/docs/index.html#userextended
val join_date: String?, val join_date: String?,
@ -205,7 +204,6 @@ class OsuApiModels {
data class Beatmap( data class Beatmap(
val beatmapset_id: Int, val beatmapset_id: Int,
val difficulty_rating: Double?, val difficulty_rating: Double?,
val checksum: String?,
val id: Int, val id: Int,
val version: String?, val version: String?,
val beatmapset: BeatmapSet, val beatmapset: BeatmapSet,
@ -223,7 +221,6 @@ class OsuApiModels {
@Serializable @Serializable
data class BeatmapSet( data class BeatmapSet(
val id: Int,
val artist: String?, val artist: String?,
val creator: String?, val creator: String?,
val source: String?, val source: String?,
@ -235,10 +232,4 @@ class OsuApiModels {
val content: String val content: String
) )
@Serializable
data class BeatmapPlaycount(
val beatmap_id: Int,
val count: Int,
)
} }

View File

@ -1,28 +0,0 @@
package com.nisemoe.nise.osu
fun OsuApiModels.Beatmap.toScoreBeatmap(): OsuApiModels.ScoreBeatmap =
OsuApiModels.ScoreBeatmap(
id = this.id,
checksum = this.checksum,
difficulty_rating = this.difficulty_rating,
version = this.version,
max_combo = this.max_combo,
total_length = this.total_length,
bpm = this.bpm,
accuracy = this.accuracy,
ar = this.ar,
cs = this.cs,
drain = this.drain,
count_circles = this.count_circles,
count_sliders = this.count_sliders,
count_spinners = this.count_spinners,
)
fun OsuApiModels.BeatmapSet.toScoreBeatmapSet(): OsuApiModels.ScoreBeatmapset =
OsuApiModels.ScoreBeatmapset(
id = this.id,
title = this.title,
artist = this.artist,
creator = this.creator,
source = this.source,
)

View File

@ -3,8 +3,6 @@ package com.nisemoe.nise.scheduler
import com.nisemoe.generated.tables.records.ScoresRecord import com.nisemoe.generated.tables.records.ScoresRecord
import com.nisemoe.generated.tables.references.* import com.nisemoe.generated.tables.references.*
import com.nisemoe.nise.UserQueueDetails import com.nisemoe.nise.UserQueueDetails
import com.nisemoe.nise.database.ReplayCacheReplay
import com.nisemoe.nise.database.ReplayCacheService
import com.nisemoe.nise.database.ScoreService import com.nisemoe.nise.database.ScoreService
import com.nisemoe.nise.database.UserService import com.nisemoe.nise.database.UserService
import com.nisemoe.nise.integrations.CircleguardService import com.nisemoe.nise.integrations.CircleguardService
@ -13,7 +11,9 @@ import com.nisemoe.nise.integrations.DiscordService
import com.nisemoe.nise.konata.Replay import com.nisemoe.nise.konata.Replay
import com.nisemoe.nise.konata.ReplaySetComparison import com.nisemoe.nise.konata.ReplaySetComparison
import com.nisemoe.nise.konata.compareReplaySet import com.nisemoe.nise.konata.compareReplaySet
import com.nisemoe.nise.osu.* import com.nisemoe.nise.osu.Mod
import com.nisemoe.nise.osu.OsuApi
import com.nisemoe.nise.osu.OsuApiModels
import com.nisemoe.nise.service.CacheService import com.nisemoe.nise.service.CacheService
import com.nisemoe.nise.service.CompressReplay import com.nisemoe.nise.service.CompressReplay
import com.nisemoe.nise.service.UpdateUserQueueService import com.nisemoe.nise.service.UpdateUserQueueService
@ -36,7 +36,6 @@ import org.springframework.web.bind.annotation.RestController
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import java.util.Base64
@Service @Service
@RestController @RestController
@ -50,10 +49,8 @@ class ImportScores(
private val scoreService: ScoreService, private val scoreService: ScoreService,
private val updateUserQueueService: UpdateUserQueueService, private val updateUserQueueService: UpdateUserQueueService,
private val circleguardService: CircleguardService, private val circleguardService: CircleguardService,
private val messagingTemplate: SimpMessagingTemplate, private val messagingTemplate: SimpMessagingTemplate
private val replayCacheService: ReplayCacheService,
) : InitializingBean { ) : InitializingBean {
val replayCacheEnabled = (System.getenv("REPLAY_CACHE_ENABLED") ?: "0") != "0"
private val userToUpdateBucket = mutableListOf<Long>() private val userToUpdateBucket = mutableListOf<Long>()
@ -162,93 +159,39 @@ class ImportScores(
this.logger.info("Processing ${queue.size} users from the queue.") this.logger.info("Processing ${queue.size} users from the queue.")
} }
for(queueEntry in queue) { for(userId in queue) {
val userId = queueEntry.userId
// We should only 'full fetch' a user if they have been explicitly added by another user,
// else we will spend way too much time on random users.
val shouldFullFetch = queueEntry.addedByUserId != null
val user = this.osuApi.getUserProfile(userId.toString())
if (user == null) {
this.logger.error("Failed to fetch user from queue $userId")
this.updateUserQueueService.setUserAsProcessed(userId, failed = true)
continue;
}
var userScores = mutableListOf<OsuApiModels.Score>()
if (shouldFullFetch && user.beatmap_playcounts_count != null) {
val mapsPlayed: MutableSet<Int> = mutableSetOf()
this.logger.info("User has ${user.beatmap_playcounts_count} unique beatmap plays")
for (page in 1..(user.beatmap_playcounts_count / 50) + 1) {
val maps = this.osuApi.getUserMostPlayed(userId, 50, 50 * page)
?: break
mapsPlayed.addAll(maps.map { it.beatmap_id })
this.logger.info("Page: $page/${(user.beatmap_playcounts_count / 50) + 1}")
Thread.sleep(SLEEP_AFTER_API_CALL)
}
var scoreProcessCount = 0
for (mapId in mapsPlayed) {
val scores = this.osuApi.getUserBeatmapScores(userId, mapId)
?: continue
for (mapScore in scores.scores) {
if (mapScore.replay && mapScore.id != null) {
val beatmap = this.osuApi.getBeatmapFromId(mapId)
?: continue
userScores.add(mapScore.copy(
beatmap = beatmap.toScoreBeatmap(),
beatmapset = beatmap.beatmapset.toScoreBeatmapSet(),
))
}
}
this.logger.info(
"Getting all user scores for $userId: Processed map scores ${++scoreProcessCount}/${mapsPlayed.size}"
)
Thread.sleep(SLEEP_AFTER_API_CALL)
}
} else {
val topUserScores = this.osuApi.getTopUserScores(userId = userId) val topUserScores = this.osuApi.getTopUserScores(userId = userId)
val recentUserScores = this.osuApi.getTopUserScores(userId = userId, type = "recent") val recentUserScores = this.osuApi.getTopUserScores(userId = userId, type = "recent")
val firstPlaceUserScores = this.osuApi.getTopUserScores(userId = userId, type = "firsts") val firstPlaceUserScores = this.osuApi.getTopUserScores(userId = userId, type = "firsts")
if (topUserScores == null || recentUserScores == null || firstPlaceUserScores == null) { this.logger.info("Processing user with id = $userId")
this.logger.info("Top scores: ${topUserScores?.size}")
this.logger.info("Recent scores: ${recentUserScores?.size}")
this.logger.info("First place scores: ${firstPlaceUserScores?.size}")
Thread.sleep(SLEEP_AFTER_API_CALL)
if(topUserScores == null || recentUserScores == null || firstPlaceUserScores == null) {
this.logger.error("Failed to fetch top scores for user with id = $userId") this.logger.error("Failed to fetch top scores for user with id = $userId")
this.updateUserQueueService.setUserAsProcessed(userId, failed = true) this.updateUserQueueService.setUserAsProcessed(userId, failed = true)
continue continue
} }
userScores += (topUserScores + recentUserScores + firstPlaceUserScores) val allUserScores = (topUserScores + recentUserScores + firstPlaceUserScores)
userScores = userScores
.filter { it.beatmap != null && it.beatmapset != null } .filter { it.beatmap != null && it.beatmapset != null }
.distinctBy { it.best_id } .distinctBy { it.best_id }
.toMutableList()
}
this.logger.info("Processing user with id = $userId") this.logger.info("Unique scores: ${allUserScores.size}")
this.logger.info("User has ${userScores.size} total scores")
Thread.sleep(SLEEP_AFTER_API_CALL)
this.logger.info("Unique scores: ${userScores.size}")
val userExists = dslContext.fetchExists(USERS, USERS.USER_ID.eq(userId), USERS.SYS_LAST_UPDATE.greaterOrEqual(OffsetDateTime.now(ZoneOffset.UTC).minusDays(UPDATE_USER_EVERY_DAYS))) val userExists = dslContext.fetchExists(USERS, USERS.USER_ID.eq(userId), USERS.SYS_LAST_UPDATE.greaterOrEqual(OffsetDateTime.now(ZoneOffset.UTC).minusDays(UPDATE_USER_EVERY_DAYS)))
if (!userExists) { if(!userExists) {
this.userService.insertApiUser(user) val apiUser = this.osuApi.getUserProfile(userId = userId.toString(), mode = "osu", key = "id")
if(apiUser != null) {
this.userService.insertApiUser(apiUser)
this.statistics.usersAddedToDatabase++ this.statistics.usersAddedToDatabase++
} else {
this.logger.error("Failed to fetch user with id = $userId")
}
} }
var current = 0 var current = 0
@ -261,7 +204,7 @@ class ImportScores(
.limit(1) .limit(1)
.fetchOneInto(OffsetDateTime::class.java) .fetchOneInto(OffsetDateTime::class.java)
for(topScore in userScores) { for(topScore in allUserScores) {
val beatmapExists = dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(topScore.beatmap!!.id)) val beatmapExists = dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(topScore.beatmap!!.id))
if (!beatmapExists) { if (!beatmapExists) {
val beatmapFile = this.osuApi.getBeatmapFile(beatmapId = topScore.beatmap.id) val beatmapFile = this.osuApi.getBeatmapFile(beatmapId = topScore.beatmap.id)
@ -316,7 +259,7 @@ class ImportScores(
// Update the database // Update the database
dslContext.update(UPDATE_USER_QUEUE) dslContext.update(UPDATE_USER_QUEUE)
.set(UPDATE_USER_QUEUE.PROGRESS_CURRENT, current) .set(UPDATE_USER_QUEUE.PROGRESS_CURRENT, current)
.set(UPDATE_USER_QUEUE.PROGRESS_TOTAL, userScores.size) .set(UPDATE_USER_QUEUE.PROGRESS_TOTAL, allUserScores.size)
.where(UPDATE_USER_QUEUE.USER_ID.eq(userId)) .where(UPDATE_USER_QUEUE.USER_ID.eq(userId))
.and(UPDATE_USER_QUEUE.PROCESSED.isFalse) .and(UPDATE_USER_QUEUE.PROCESSED.isFalse)
.execute() .execute()
@ -326,7 +269,7 @@ class ImportScores(
lastCompletedUpdate = lastCompletedUpdate, lastCompletedUpdate = lastCompletedUpdate,
canUpdate = false, canUpdate = false,
progressCurrent = current, progressCurrent = current,
progressTotal = userScores.size progressTotal = allUserScores.size
) )
// Update the frontend // Update the frontend
@ -341,7 +284,7 @@ class ImportScores(
} }
// Check for stolen replays. // Check for stolen replays.
val uniqueBeatmapIds = userScores val uniqueBeatmapIds = allUserScores
.groupBy { it.beatmap!!.id } .groupBy { it.beatmap!!.id }
this.logger.info("Checking similarity for ${uniqueBeatmapIds.size} beatmaps.") this.logger.info("Checking similarity for ${uniqueBeatmapIds.size} beatmaps.")
@ -841,24 +784,6 @@ class ImportScores(
) )
} }
if (replayCacheEnabled) {
// Insert into replay cache
val replayCacheReplay = ReplayCacheReplay(
score.best_id,
beatmapId,
score.user_id.toInt(),
Base64.getDecoder().decode(scoreReplay.content),
Mod.combineModStrings(score.mods),
)
val replayCacheInsertSuccess = replayCacheService.insertReplay(replayCacheReplay)
if (replayCacheInsertSuccess) {
logger.info("Inserted replay ${score.id} into replay cache")
} else {
logger.error("Could not insert replay ${score.id} into replay cache")
}
}
this.statistics.scoresWithReplayAndAnalyzed++ this.statistics.scoresWithReplayAndAnalyzed++
if (scoreId == null) { if (scoreId == null) {

View File

@ -79,11 +79,12 @@ class UpdateUserQueueService(
/** /**
* Retrieves the full update queue, only pending users. * Retrieves the full update queue, only pending users.
*/ */
fun getQueue(): List<UpdateUserQueueRecord> { fun getQueue(): List<Long> {
return dslContext.selectFrom(UPDATE_USER_QUEUE) return dslContext.select(UPDATE_USER_QUEUE.USER_ID)
.from(UPDATE_USER_QUEUE)
.where(UPDATE_USER_QUEUE.PROCESSED.isFalse) .where(UPDATE_USER_QUEUE.PROCESSED.isFalse)
.orderBy(UPDATE_USER_QUEUE.CREATED_AT.asc()) .orderBy(UPDATE_USER_QUEUE.CREATED_AT.asc())
.fetch() .fetchInto(Long::class.java)
} }
/** /**

View File

@ -0,0 +1,14 @@
spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:postgres}:${POSTGRES_PORT:5432}/${POSTGRES_DB:postgres}?currentSchema=public
spring.datasource.username=${POSTGRES_USER:postgres}
spring.datasource.password=${POSTGRES_PASS:postgres}
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.name=HikariPool-PostgreSQL
spring.flyway.enabled=${FLYWAY_ENABLED:true}
spring.flyway.schemas=public
# Batching
spring.datasource.hikari.data-source-properties.prepStmtCacheSize=250
spring.datasource.hikari.data-source-properties.prepStmtCacheSqlLimit=2048
spring.datasource.hikari.data-source-properties.useServerPrepStmts=true
spring.datasource.hikari.data-source-properties.rewriteBatchedStatements=true

View File

@ -33,24 +33,3 @@ spring.security.oauth2.client.provider.osu.authorization-uri=https://osu.ppy.sh/
spring.security.oauth2.client.provider.osu.token-uri=https://osu.ppy.sh/oauth/token spring.security.oauth2.client.provider.osu.token-uri=https://osu.ppy.sh/oauth/token
spring.security.oauth2.client.provider.osu.user-info-uri=https://osu.ppy.sh/api/v2/me/osu spring.security.oauth2.client.provider.osu.user-info-uri=https://osu.ppy.sh/api/v2/me/osu
spring.security.oauth2.client.provider.osu.user-name-attribute=username spring.security.oauth2.client.provider.osu.user-name-attribute=username
spring.datasource.nise.jdbcUrl=jdbc:postgresql://${POSTGRES_HOST:postgres}:${POSTGRES_PORT:5432}/${POSTGRES_DB:postgres}?currentSchema=public
spring.datasource.nise.username=${POSTGRES_USER:postgres}
spring.datasource.nise.password=${POSTGRES_PASS:postgres}
spring.datasource.nise.driver-class-name=org.postgresql.Driver
spring.datasource.nise.name=HikariPool-PostgreSQL
spring.datasource.replay-cache.jdbcUrl=jdbc:postgresql://${REPLAY_CACHE_HOST:postgres}:${REPLAY_CACHE_PORT:5433}/${REPLAY_CACHE_DB:REPLAY_CACHE}?currentSchema=public
spring.datasource.replay-cache.username=${REPLAY_CACHE_USER:postgres}
spring.datasource.replay-cache.password=${REPLAY_CACHE_PASS:postgres}
spring.datasource.replay-cache.driver-class-name=org.postgresql.Driver
spring.datasource.replay-cache.name=HikariPool-PostgreSQL
spring.flyway.enabled=${FLYWAY_ENABLED:true}
spring.flyway.schemas=public
# Batching
spring.datasource.hikari.data-source-properties.prepStmtCacheSize=250
spring.datasource.hikari.data-source-properties.prepStmtCacheSqlLimit=2048
spring.datasource.hikari.data-source-properties.useServerPrepStmts=true
spring.datasource.hikari.data-source-properties.rewriteBatchedStatements=true

View File

@ -1,13 +1,16 @@
FROM python:3.11.8-slim FROM python:3.11.8-slim
ENV version=2 ENV version=2
ENV PYTHONPATH=/app ENV PYTHONPATH /app
WORKDIR /app WORKDIR /app
RUN apt update
COPY requirements.txt ./requirements.txt COPY requirements.txt ./requirements.txt
RUN pip3 install --no-cache-dir -r requirements.txt RUN pip3 install --upgrade pip && \
pip3 install -r requirements.txt
# This is *really* bad, but I'd rather get this working rather than forking packages and re-publishing them. # This is *really* bad, but I'd rather get this working rather than forking packages and re-publishing them.
# It'll probably break some day. # It'll probably break some day.
@ -19,6 +22,7 @@ RUN sed -i '238s|return \[x for x in arr if lower_limit < x < upper_limit\]|arr_
COPY ./src/ ./src/ COPY ./src/ ./src/
WORKDIR /app/src ENV GUNICORN_CMD_ARGS="--bind=0.0.0.0:5000 --workers=16"
CMD ["python", "main.py"] # Run gunicorn with the application
CMD ["gunicorn", "--chdir", "src", "main:app"]

View File

@ -1,4 +1,5 @@
ossapi==3.4.3 ossapi==3.4.3
circleguard==5.4.2 circleguard==5.4.1
flask==3.0.2
brparser==1.0.4 brparser==1.0.4
sanic==24.6.0 gunicorn==21.2.0

View File

@ -5,21 +5,21 @@ from dataclasses import dataclass, asdict
from typing import List, Iterable from typing import List, Iterable
import numpy as np import numpy as np
from sanic import Request, Sanic, exceptions, json
import scipy import scipy
from brparser import Replay, BeatmapOsu, Mod from brparser import Replay, BeatmapOsu, Mod
from circleguard import Circleguard, ReplayString, Hit from circleguard import Circleguard, ReplayString, Hit
from flask import Flask, request, jsonify, abort
from itertools import combinations from itertools import combinations
from math import isnan from math import isnan
from slider import Beatmap, Circle, Slider, Spinner from slider import Beatmap, Circle, Slider, Spinner
from WriteStreamWrapper import WriteStreamWrapper from src.WriteStreamWrapper import WriteStreamWrapper
from keypresses import get_kp_sliders from src.keypresses import get_kp_sliders
# Circleguard # Circleguard
cg = Circleguard(os.getenv("OSU_API_KEY"), db_path="./dbs/db.db", slider_dir="./dbs/") cg = Circleguard(os.getenv("OSU_API_KEY"), db_path="./dbs/db.db", slider_dir="./dbs/")
app = Sanic(__name__) app = Flask(__name__)
def my_filter_outliers(arr, bias=1.5): def my_filter_outliers(arr, bias=1.5):
""" """
@ -123,11 +123,11 @@ class ScoreJudgement:
@app.post("/replay") @app.post("/replay")
async def process_replay(request: Request): def process_replay():
try: try:
request_data = request.json request_data = request.get_json()
if not request_data: if not request_data:
raise exceptions.BadRequest("Bad Request: No JSON data provided.") abort(400, description="Bad Request: No JSON data provided.")
replay_request = ReplayRequest.from_dict(request_data) replay_request = ReplayRequest.from_dict(request_data)
@ -219,10 +219,10 @@ async def process_replay(request: Request):
judgements=judgements judgements=judgements
) )
return json(ur_response.to_dict()) return jsonify(ur_response.to_dict())
except ValueError as e: except ValueError as e:
raise exceptions.BadRequest(str(e)) abort(400, description=str(e))
@dataclass @dataclass
@ -242,11 +242,11 @@ class ReplayDto:
@app.post("/similarity") @app.post("/similarity")
async def process_similarity(request: Request): def process_similarity():
try: try:
request_data = request.json request_data = request.get_json()
if not request_data: if not request_data:
raise exceptions.BadRequest("Bad Request: No JSON data provided.") abort(400, description="Bad Request: No JSON data provided.")
replays: List[ReplayDto] = request_data['replays'] replays: List[ReplayDto] = request_data['replays']
replay_cache = {} replay_cache = {}
@ -287,10 +287,11 @@ async def process_similarity(request: Request):
) )
response.append(new_score_similarity) response.append(new_score_similarity)
return json({'result': response}) return jsonify({'result': response})
except ValueError as e: except ValueError as e:
raise exceptions.BadRequest(str(e)) abort(400, description=str(e))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000) if __name__ == "__main__":
app.run(host='0.0.0.0', debug=False)

View File

@ -1,4 +1,4 @@
FROM nginx:1.27.0-alpine FROM nginx:1.27.0
RUN rm -rf /usr/share/nginx/html/* RUN rm -rf /usr/share/nginx/html/*

View File

@ -39,7 +39,7 @@ const routes: Routes = [
{path: 'neko', component: MetabaseComponent, title: 'metabase integration'}, {path: 'neko', component: MetabaseComponent, title: 'metabase integration'},
{path: '**', component: HomeComponent, title: '/nise.stedos.dev/'}, {path: '**', component: HomeComponent, title: '/nise.moe/'},
]; ];
@NgModule({ @NgModule({

View File

@ -5,12 +5,13 @@
</a> </a>
</div> </div>
<div> <div>
<h2 style="margin-top: 6px">/nise.stedos.dev/</h2> <h2 style="margin-top: 6px">/nise.moe/</h2>
<ul style="font-size: 15px; line-height: 19px;"> <ul style="font-size: 15px; line-height: 19px;">
<li><a [routerLink]="['/']">./home</a></li> <li><a [routerLink]="['/']">./home</a></li>
<li><a [routerLink]="['/sus']">./suspicious-scores</a></li> <li><a [routerLink]="['/sus']">./suspicious-scores</a></li>
<li><a [routerLink]="['/stolen']">./stolen-replays</a></li> <li><a [routerLink]="['/stolen']">./stolen-replays</a></li>
<li><a [routerLink]="['/search']">./advanced-search</a></li> <li><a [routerLink]="['/search']">./advanced-search</a></li>
<li *ngIf="this.userService.ephemeralUserInfo.showContributions"><a class="link-pink" [routerLink]="['/contribute']">./contribute ♥</a></li>
</ul> </ul>
<form (ngSubmit)="onSubmit()"> <form (ngSubmit)="onSubmit()">
<input style="width: 100%" type="text" [(ngModel)]="term" [ngModelOptions]="{standalone: true}" id="nise-osu-username" required minlength="2" maxlength="50" placeholder="Search for users..."> <input style="width: 100%" type="text" [(ngModel)]="term" [ngModelOptions]="{standalone: true}" id="nise-osu-username" required minlength="2" maxlength="50" placeholder="Search for users...">
@ -34,5 +35,5 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
<div class="text-center version"> <div class="text-center version">
v20250213 v20240511
</div> </div>

View File

@ -2,15 +2,20 @@
<div class="subcontainer"> <div class="subcontainer">
<div class="term"> <div class="term">
<h1># Welcome to [nise.stedos.dev] (formerly nise.moe)</h1> <h1># Welcome to [nise.moe]</h1>
<h3>wtf is this?</h3> <h3>wtf is this?</h3>
<p>This application will automatically crawl [osu!std] top scores and search for stolen replays or obvious relax/timewarp scores.</p> <p>This application will automatically crawl [osu!std] top scores and search for stolen replays or obvious relax/timewarp scores.</p>
<p>It is currently in an <i>ALPHA</i> state and will not persist any data. This will change soon.</p> <p>It started collecting replays on <i>2024-01-12</i></p>
<p>This website is not affiliated with the osu! game nor ppy. It is an unrelated, unaffiliated, 3rd party project.</p> <p>This website is not affiliated with the osu! game nor ppy. It is an unrelated, unaffiliated, 3rd party project.</p>
<p>If you have any suggestions or want to report bugs, feel free to join the Discord server below.</p>
<div class="text-center mt-4">
<a href="https://discord.gg/wn4gWpA36w" target="_blank" class="btn">Join the Discord!</a>
<a [routerLink]="['/docs']" class="btn api-docs-btn" style="margin-left: 20px;">Check out the /api/ docs.</a>
</div>
<h3 class="mt-4"># do you use rss? (nerd)</h3> <h3 class="mt-4"># do you use rss? (nerd)</h3>
<p>you can keep up with newly detected scores with the rss feed, subscribe to it using your favorite reader.</p> <p>you can keep up with newly detected scores with the rss feed, subscribe to it using your favorite reader.</p>
<div class="text-center"> <div class="text-center">
<a href="https://nise.stedos.dev/api/rss.xml" target="_blank"> <a href="https://nise.moe/api/rss.xml" target="_blank">
<img title="rss-chan!" src="/assets/rss.png" width="64" style="filter: grayscale(40%) sepia(10%) brightness(90%);"> <img title="rss-chan!" src="/assets/rss.png" width="64" style="filter: grayscale(40%) sepia(10%) brightness(90%);">
<br> <br>
<span style="padding: 2px; border: 1px dotted #b3b8c3;"> <span style="padding: 2px; border: 1px dotted #b3b8c3;">

View File

@ -1,16 +1,12 @@
import {UserDetails} from './userDetails'; import {UserDetails} from './userDetails';
import {SimilarReplay, SuspiciousScore} from './replays'; import {SimilarReplay, SuspiciousScore} from './replays';
import {environment} from "../environments/environment";
export class TextReportService { export class TextReportService {
static generateTextReportForUserScores( static generateTextReportForUserScores(
userDetails: UserDetails, userDetails: UserDetails,
suspiciousScores: SuspiciousScore[], suspiciousScores: SuspiciousScore[],
similarReplays: SimilarReplay[], similarReplays: SimilarReplay[],
) { ) {
const site = 'nise.stedos.dev';
const detections: string[] = []; const detections: string[] = [];
if (suspiciousScores.length > 0) { if (suspiciousScores.length > 0) {
@ -25,29 +21,26 @@ export class TextReportService {
report += `Profile: https://osu.ppy.sh/users/${userDetails.user_id}\n`; report += `Profile: https://osu.ppy.sh/users/${userDetails.user_id}\n`;
for (const suspiciousScore of suspiciousScores) { for (const suspiciousScore of suspiciousScores) {
report += `\n\n${this.getRelaxReport(suspiciousScore)}\n`; report += `\n${this.getRelaxReport(suspiciousScore)}\n`;
} }
for (const similarReplay of similarReplays) { for (const similarReplay of similarReplays) {
report += `\n\n${this.getStealingReport(similarReplay)}\n`; report += `\n${this.getStealingReport(similarReplay)}\n`;
} }
report += `\n\nGenerated on ${site} - [${userDetails.username} on ${site}](${environment.webUrl}/u/${userDetails.user_id})`; report += `\nGenerated on nise.moe - [${userDetails.username} on nise.moe](https://nise.moe/u/${userDetails.user_id})`;
return report; return report;
} }
private static getRelaxReport(suspiciousScore: SuspiciousScore): string { private static getRelaxReport(suspiciousScore: SuspiciousScore): string {
return `[Replay on ${suspiciousScore.beatmap_title}](https://osu.ppy.sh/scores/osu/${suspiciousScore.replay_id}) return `[Replay on ${suspiciousScore.beatmap_title}](https://osu.ppy.sh/scores/osu/${suspiciousScore.replay_id})
cvUR: ${suspiciousScore.ur.toFixed(2)} according to Circleguard`; cvUR: ${suspiciousScore.ur.toFixed(2)} according to Circleguard`;
} }
private static getStealingReport(similarReplay: SimilarReplay): string { private static getStealingReport(similarReplay: SimilarReplay): string {
return `[${similarReplay.username_2}'s replay (cheated)](https://osu.ppy.sh/scores/osu/${similarReplay.replay_id_2}) return `[${similarReplay.username_2}'s replay (cheated)](https://osu.ppy.sh/scores/osu/${similarReplay.replay_id_2})
[${similarReplay.username_1}'s replay (original)](https://osu.ppy.sh/scores/osu/${similarReplay.replay_id_1}) [${similarReplay.username_1}'s replay (original)](https://osu.ppy.sh/scores/osu/${similarReplay.replay_id_1})
${similarReplay.similarity.toFixed(2)} similarity according to Circleguard`; ${similarReplay.similarity.toFixed(2)} similarity according to Circleguard`;
} }
} }

View File

@ -29,9 +29,9 @@
</a> </a>
</div> </div>
<!-- <div class="text-center mt-2"> <div class="text-center mt-2">
<a class="btn" [href]="'https://replay.nise.moe/' + this.pair.replays[0].replay_id + '/' + this.pair.replays[1].replay_id" target="_blank">Open in Replay Viewer</a> <a class="btn" [href]="'https://replay.nise.moe/' + this.pair.replays[0].replay_id + '/' + this.pair.replays[1].replay_id" target="_blank">Open in Replay Viewer</a>
</div> --> </div>
<div class="some-page-wrapper text-center"> <div class="some-page-wrapper text-center">
<div class="row"> <div class="row">

View File

@ -53,9 +53,9 @@
Open in CircleGuard Open in CircleGuard
</a> </a>
<!-- <a style="flex: 1" class="text-center" [href]="'https://replay.nise.moe/' + this.replayData.replay_id" target="_blank" [class.disabled]="!hasReplay()"> <a style="flex: 1" class="text-center" [href]="'https://replay.nise.moe/' + this.replayData.replay_id" target="_blank" [class.disabled]="!hasReplay()">
Open in Replay Viewer Open in Replay Viewer
</a> --> </a>
</div> </div>

View File

@ -1,7 +1,7 @@
<div class="main term"> <div class="main term">
<h1><span class="board">/sus/</span> - Suspicious Scores</h1> <h1><span class="board">/sus/</span> - Suspicious Scores</h1>
<div class="alert mb-2"> <div class="alert mb-2">
This includes all replays with <35 cvUR. Low values can indicate cheating but always manually review users and This includes all replays with <25 cvUR. Low values can indicate cheating but always manually review users and
replays before making judgements. replays before making judgements.
</div> </div>
@ -24,7 +24,7 @@
<input class="form-control" type="number" id="maxPP" [(ngModel)]="this.filterManager.filters.maxPP" (input)="filterScores()" <input class="form-control" type="number" id="maxPP" [(ngModel)]="this.filterManager.filters.maxPP" (input)="filterScores()"
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters"> [readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
</p> </p>
`
<!-- Min cvUR --> <!-- Min cvUR -->
<p> <p>
<label for="minUR" class="form-label">Min cvUR</label> <label for="minUR" class="form-label">Min cvUR</label>

View File

@ -69,7 +69,7 @@ html {
} }
.header { .header {
width: 600px; width: 555px;
text-align: center; text-align: center;
} }

View File

@ -23,8 +23,8 @@ export class ChartHitDistributionComponent implements OnInit, OnChanges {
@Input() mods!: string[]; @Input() mods!: string[];
removeOutliers = true; removeOutliers = true;
groupData = false; groupData = true;
showPercentages = false; showPercentages = true;
public barChartLegend = true; public barChartLegend = true;
public barChartPlugins = []; public barChartPlugins = [];

View File

@ -43,8 +43,8 @@ export class ChartComponent implements OnChanges {
@Input() data!: number[]; @Input() data!: number[];
removeOutliers = true; removeOutliers = true;
groupData = false; groupData = true;
showPercentages = false; showPercentages = true;
calculateStatistics(): Array<{ name: string, value: number }> { calculateStatistics(): Array<{ name: string, value: number }> {
if (this.data.length === 0) { if (this.data.length === 0) {

View File

@ -1,6 +1,5 @@
export const environment = { export const environment = {
production: false, production: false,
webUrl: 'http://localhost:4200',
apiUrl: 'http://localhost:8080', apiUrl: 'http://localhost:8080',
wsUrl: 'ws://localhost:8080/websocket', wsUrl: 'ws://localhost:8080/websocket',
}; };

View File

@ -1,8 +1,5 @@
const URL = 'nise.stedos.dev';
export const environment = { export const environment = {
production: true, production: true,
webUrl: `https://${URL}`, apiUrl: 'https://nise.moe/api',
apiUrl: `https://${URL}/api`, wsUrl: 'wss://nise.moe/api/websocket',
wsUrl: `wss://${URL}/api/websocket`,
}; };

View File

@ -6,14 +6,14 @@
<title></title> <title></title>
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico"> <link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
<!-- Embed data --> <!-- Embed data -->
<meta property="og:title" content="/nise.stedos.dev/ - osu!cheaters finder"> <meta property="og:title" content="/nise.moe/ - osu!cheaters finder">
<meta property="og:description" content="crawls osu!std replays and tries to find naughty boys."> <meta property="og:description" content="crawls osu!std replays and tries to find naughty boys.">
<meta property="og:url" content="https://nise.stedos.dev"> <meta property="og:url" content="https://nise.moe">
<meta property="og:image" content="https://nise.stedos.dev/assets/banner.png"> <meta property="og:image" content="https://nise.moe/assets/banner.png">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta name="theme-color" content="#151515"> <meta name="theme-color" content="#151515">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image:src" content="https://nise.stedos.dev/assets/banner.png"> <meta name="twitter:image:src" content="https://nise.moe/assets/banner.png">
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>

View File

@ -1,3 +0,0 @@
nise.stedos.dev {
reverse_proxy nise-nginx
}

View File

@ -1,25 +1,23 @@
version: '3'
services: services:
caddy-main: nginx-main:
image: caddy:alpine image: nginx:latest
container_name: caddy-main container_name: nginx-main
restart: always restart: always
volumes: volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro - ./nginx-main.conf:/etc/nginx/nginx.conf:ro
# nise.moe certificates (by Cloudflare)
- ./nise-data/certificate.pem:/etc/ssl/certs/nisemoe/certificate.pem:ro
- ./nise-data/private.key:/etc/ssl/certs/nisemoe/private.key:ro
ports: ports:
- "443:443" - "443:443"
- "80:80" - "80:80"
depends_on:
- nise-nginx
# Shared services which are used by others # Shared services which are used by others
redis:
image: redis:alpine
container_name: redis
restart: always
postgres: postgres:
image: postgres:alpine image: groonga/pgroonga:3.1.6-alpine-15
container_name: postgres container_name: postgres
restart: always restart: always
environment: environment:
@ -27,6 +25,47 @@ services:
POSTGRES_PASSWORD: ${DB_PASS} POSTGRES_PASSWORD: ${DB_PASS}
volumes: volumes:
- postgres-data:/var/lib/postgresql/data - postgres-data:/var/lib/postgresql/data
command: >
-c shared_buffers=6GB
-c effective_cache_size=12GB
-c work_mem=64MB
-c maintenance_work_mem=2GB
-c checkpoint_completion_target=0.9
-c checkpoint_timeout=15min
-c max_wal_size=2GB
-c wal_buffers=16MB
-c max_connections=100
-c max_worker_processes=8
-c max_parallel_workers_per_gather=4
-c max_parallel_workers=8
-c effective_io_concurrency=40
shm_size: '128mb'
redis:
image: redis:alpine
container_name: redis
restart: always
# ------------------------------------------------------------------
gitea:
image: gitea/gitea
container_name: gitea
restart: always
environment:
USER_UID: 1336
USER_GID: 1336
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: ${DB_HOST}:5432
GITEA__database__NAME: gitea
GITEA__database__USER: ${DB_USER}
GITEA__database__PASSWD: ${DB_PASS}
depends_on:
- postgres
- redis
volumes:
- ./gitea-data/app.ini:/data/gitea/conf/app.ini
- gitea-data:/data
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -36,31 +75,19 @@ services:
restart: always restart: always
volumes: volumes:
- ./nise-data/nginx.conf:/etc/nginx/nginx.conf:ro - ./nise-data/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- nise-backend
- nise-frontend
nise-circleguard:
image: code.stedos.dev/stedos/nise-circleguard:latest
container_name: nise-circleguard
environment:
OSU_API_KEY: ${OSU_API_KEY}
restart: always
volumes:
- ./nise-data/beatmaps:/app/dbs
nise-backend: nise-backend:
image: code.stedos.dev/stedos/nise-backend:latest image: git.nise.moe/nuff/nise-backend:latest
container_name: nise-backend container_name: nise-backend
environment: environment:
SPRING_PROFILES_ACTIVE: postgres,import:scores,import:users,fix:scores SPRING_PROFILES_ACTIVE: postgres,discord,import:scores,import:users,fix:scores
# App configuration # App configuration
OLD_SCORES_PAGE_SIZE: 1000 OLD_SCORES_PAGE_SIZE: 1000
# Postgres # Postgres
POSTGRES_HOST: ${DB_HOST} POSTGRES_HOST: ${DB_HOST}
POSTGRES_USER: ${DB_USER} POSTGRES_USER: ${DB_USER}
POSTGRES_PASS: ${DB_PASS} POSTGRES_PASS: ${DB_PASS}
POSTGRES_DB: ${DB_NAME} POSTGRES_DB: nise
# redis # redis
REDIS_DB: 4 REDIS_DB: 4
# Discord # Discord
@ -70,36 +97,69 @@ services:
OSU_API_KEY: ${OSU_API_KEY} OSU_API_KEY: ${OSU_API_KEY}
OSU_CLIENT_ID: ${OSU_CLIENT_ID} OSU_CLIENT_ID: ${OSU_CLIENT_ID}
OSU_CLIENT_SECRET: ${OSU_CLIENT_SECRET} OSU_CLIENT_SECRET: ${OSU_CLIENT_SECRET}
OSU_CALLBACK: "https://nise.stedos.dev/api/login/oauth2/code/osu" OSU_CALLBACK: "https://nise.moe/api/login/oauth2/code/osu"
# Metabase # Metabase
METABASE_API_KEY: ${METABASE_API_KEY} METABASE_API_KEY: ${METABASE_API_KEY}
# Internal API # Internal API
CIRCLEGUARD_API_URL: http://nise-circleguard:5000 CIRCLEGUARD_API_URL: http://nise-circleguard:5000
# Auth # Auth
ORIGIN: "https://nise.stedos.dev" ORIGIN: "https://nise.moe"
REPLAY_ORIGIN: "https://replay.nise.moe" REPLAY_ORIGIN: "https://replay.nise.moe"
COOKIE_SECURE: false COOKIE_SECURE: false
BEATMAPS_PATH: "/app/dbs" BEATMAPS_PATH: "/app/dbs"
# Replay cache
REPLAY_CACHE_ENABLED: ${REPLAY_CACHE_ENABLED}
REPLAY_CACHE_HOST: ${REPLAY_CACHE_HOST}
REPLAY_CACHE_PORT: ${REPLAY_CACHE_PORT}
REPLAY_CACHE_DB: ${REPLAY_CACHE_DB}
REPLAY_CACHE_USER: ${REPLAY_CACHE_USER}
REPLAY_CACHE_PASS: ${REPLAY_CACHE_PASS}
restart: always restart: always
volumes: volumes:
- ./nise-data/beatmaps:/app/dbs - ./nise-data/beatmaps:/app/dbs
depends_on: depends_on:
- postgres - postgres
- redis - redis
- nise-circleguard
nise-frontend: nise-circleguard:
image: code.stedos.dev/stedos/nise-frontend:latest image: git.nise.moe/nuff/nise-circleguard:latest
container_name: nise-frontend container_name: nise-circleguard
environment:
OSU_API_KEY: ${OSU_API_KEY}
restart: always
volumes:
- ./nise-data/beatmaps:/app/dbs
nise-frontend2:
image: git.nise.moe/nuff/nise-frontend:latest
container_name: nise-frontend2
restart: always restart: always
nise-replay-viewer:
image: git.nise.moe/nuff/nise-replay-viewer:latest
container_name: nise-replay-viewer
restart: always
nise-discord:
image: git.nise.moe/nuff/nise-discord:latest
container_name: nise-discord
environment:
DISCORD_TOKEN: ${DISCORD_TOKEN}
REACTION_CHANNEL_ID: ${REACTION_CHANNEL_ID}
REACTION_EMOJI_ID: ${REACTION_EMOJI_ID}
restart: always
nise-metabase:
image: metabase/metabase:latest
container_name: nise-metabase
volumes:
- /dev/urandom:/dev/random:ro
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: metabase
MB_DB_PORT: 5432
MB_DB_USER: ${DB_METABASE_USER}
MB_DB_PASS: ${DB_METABASE_PASS}
MB_DB_HOST: postgres
healthcheck:
test: curl --fail -I http://localhost:3000/api/health || exit 1
interval: 15s
timeout: 5s
retries: 5
volumes: volumes:
postgres-data: postgres-data:
gitea-data:

View File

@ -9,13 +9,13 @@ http {
# Redirect HTTP to HTTPS # Redirect HTTP to HTTPS
server { server {
listen 80; listen 80;
server_name nise.stedos.dev; server_name nise.moe replay.nise.moe neko.nise.moe;
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
server { server {
listen 443 ssl; listen 443 ssl;
server_name nise.stedos.dev; server_name nise.moe replay.nise.moe git.nise.moe neko.nise.moe;
ssl_certificate /etc/ssl/certs/nisemoe/certificate.pem; ssl_certificate /etc/ssl/certs/nisemoe/certificate.pem;
ssl_certificate_key /etc/ssl/certs/nisemoe/private.key; ssl_certificate_key /etc/ssl/certs/nisemoe/private.key;

View File

@ -2,17 +2,29 @@ events {}
http { http {
upstream gitea {
server gitea:3000;
}
upstream nise-frontend { upstream nise-frontend {
server nise-frontend:80; server nise-frontend2:80;
}
upstream nise-replay-viewer {
server nise-replay-viewer:80;
} }
upstream nise-backend { upstream nise-backend {
server nise-backend:8080; server nise-backend:8080;
} }
upstream nise-metabase {
server nise-metabase:3000;
}
server { server {
listen 80; listen 80;
server_name nise.stedos.dev; server_name nise.moe;
location / { location / {
proxy_pass http://nise-frontend; proxy_pass http://nise-frontend;
@ -37,4 +49,46 @@ http {
} }
server {
listen 80;
server_name git.nise.moe;
location / {
client_max_body_size 10G;
proxy_pass http://gitea/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name replay.nise.moe;
location / {
proxy_pass http://nise-replay-viewer/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name neko.nise.moe;
location / {
proxy_pass http://nise-metabase/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
} }