Compare commits

..

No commits in common. "main" and "sanic-cg-service" have entirely different histories.

35 changed files with 259 additions and 431 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,15 +70,14 @@ 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&")
} }
if(appendToUrl != null) if(appendToUrl != null)
@ -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 val topUserScores = this.osuApi.getTopUserScores(userId = userId)
val recentUserScores = this.osuApi.getTopUserScores(userId = userId, type = "recent")
// We should only 'full fetch' a user if they have been explicitly added by another user, val firstPlaceUserScores = this.osuApi.getTopUserScores(userId = userId, type = "firsts")
// 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 recentUserScores = this.osuApi.getTopUserScores(userId = userId, type = "recent")
val firstPlaceUserScores = this.osuApi.getTopUserScores(userId = userId, type = "firsts")
if (topUserScores == null || recentUserScores == null || firstPlaceUserScores == null) {
this.logger.error("Failed to fetch top scores for user with id = $userId")
this.updateUserQueueService.setUserAsProcessed(userId, failed = true)
continue
}
userScores += (topUserScores + recentUserScores + firstPlaceUserScores)
userScores = userScores
.filter { it.beatmap != null && it.beatmapset != null }
.distinctBy { it.best_id }
.toMutableList()
}
this.logger.info("Processing user with id = $userId") this.logger.info("Processing user with id = $userId")
this.logger.info("User has ${userScores.size} total scores") 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) Thread.sleep(SLEEP_AFTER_API_CALL)
this.logger.info("Unique scores: ${userScores.size}") if(topUserScores == null || recentUserScores == null || firstPlaceUserScores == null) {
this.logger.error("Failed to fetch top scores for user with id = $userId")
this.updateUserQueueService.setUserAsProcessed(userId, failed = true)
continue
}
val allUserScores = (topUserScores + recentUserScores + firstPlaceUserScores)
.filter { it.beatmap != null && it.beatmapset != null }
.distinctBy { it.best_id }
this.logger.info("Unique scores: ${allUserScores.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")
this.statistics.usersAddedToDatabase++ if(apiUser != null) {
this.userService.insertApiUser(apiUser)
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

@ -32,25 +32,4 @@ spring.security.oauth2.client.registration.osu.provider=osu
spring.security.oauth2.client.provider.osu.authorization-uri=https://osu.ppy.sh/oauth/authorize spring.security.oauth2.client.provider.osu.authorization-uri=https://osu.ppy.sh/oauth/authorize
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,10 +1,12 @@
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 --no-cache-dir -r requirements.txt
@ -19,6 +21,9 @@ RUN sed -i '238s|return \[x for x in arr if lower_limit < x < upper_limit\]|arr_
COPY ./src/ ./src/ COPY ./src/ ./src/
ENV GUNICORN_CMD_ARGS="--bind=0.0.0.0:5000 --workers=16"
WORKDIR /app/src WORKDIR /app/src
CMD ["python", "main.py"] # Run gunicorn with the application
CMD ["sanic", "main", "--port=5000"]

View File

@ -1,4 +1,4 @@
ossapi==3.4.3 ossapi==3.4.3
circleguard==5.4.2 circleguard==5.4.1
brparser==1.0.4 brparser==1.0.4
sanic==24.6.0 sanic==24.6.0

View File

@ -291,6 +291,3 @@ async def process_similarity(request: Request):
except ValueError as e: except ValueError as e:
raise exceptions.BadRequest(str(e)) raise exceptions.BadRequest(str(e))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

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;
}
}
} }