Ability for visitors to add users to queue and processing with live progress
This commit is contained in:
parent
45b4334011
commit
63be57dfc8
@ -17,7 +17,7 @@ import org.jooq.Identity
|
||||
import org.jooq.Name
|
||||
import org.jooq.Record
|
||||
import org.jooq.Records
|
||||
import org.jooq.Row5
|
||||
import org.jooq.Row7
|
||||
import org.jooq.Schema
|
||||
import org.jooq.SelectField
|
||||
import org.jooq.Table
|
||||
@ -88,6 +88,16 @@ open class UpdateUserQueue(
|
||||
*/
|
||||
val PROCESSED_AT: TableField<UpdateUserQueueRecord, LocalDateTime?> = createField(DSL.name("processed_at"), SQLDataType.LOCALDATETIME(6), this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.update_user_queue.progress_current</code>.
|
||||
*/
|
||||
val PROGRESS_CURRENT: TableField<UpdateUserQueueRecord, Int?> = createField(DSL.name("progress_current"), SQLDataType.INTEGER, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.update_user_queue.progress_total</code>.
|
||||
*/
|
||||
val PROGRESS_TOTAL: TableField<UpdateUserQueueRecord, Int?> = createField(DSL.name("progress_total"), SQLDataType.INTEGER, this, "")
|
||||
|
||||
private constructor(alias: Name, aliased: Table<UpdateUserQueueRecord>?): this(alias, null, null, aliased, null)
|
||||
private constructor(alias: Name, aliased: Table<UpdateUserQueueRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)
|
||||
|
||||
@ -130,18 +140,18 @@ open class UpdateUserQueue(
|
||||
override fun rename(name: Table<*>): UpdateUserQueue = UpdateUserQueue(name.getQualifiedName(), null)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Row5 type methods
|
||||
// Row7 type methods
|
||||
// -------------------------------------------------------------------------
|
||||
override fun fieldsRow(): Row5<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?> = super.fieldsRow() as Row5<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?>
|
||||
override fun fieldsRow(): Row7<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?, Int?, Int?> = super.fieldsRow() as Row7<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?, Int?, Int?>
|
||||
|
||||
/**
|
||||
* Convenience mapping calling {@link SelectField#convertFrom(Function)}.
|
||||
*/
|
||||
fun <U> mapping(from: (Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?) -> U): SelectField<U> = convertFrom(Records.mapping(from))
|
||||
fun <U> mapping(from: (Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?, Int?, Int?) -> U): SelectField<U> = convertFrom(Records.mapping(from))
|
||||
|
||||
/**
|
||||
* Convenience mapping calling {@link SelectField#convertFrom(Class,
|
||||
* Function)}.
|
||||
*/
|
||||
fun <U> mapping(toType: Class<U>, from: (Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?) -> U): SelectField<U> = convertFrom(toType, Records.mapping(from))
|
||||
fun <U> mapping(toType: Class<U>, from: (Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?, Int?, Int?) -> U): SelectField<U> = convertFrom(toType, Records.mapping(from))
|
||||
}
|
||||
|
||||
@ -10,8 +10,8 @@ import java.time.LocalDateTime
|
||||
|
||||
import org.jooq.Field
|
||||
import org.jooq.Record1
|
||||
import org.jooq.Record5
|
||||
import org.jooq.Row5
|
||||
import org.jooq.Record7
|
||||
import org.jooq.Row7
|
||||
import org.jooq.impl.UpdatableRecordImpl
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ import org.jooq.impl.UpdatableRecordImpl
|
||||
* This class is generated by jOOQ.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open class UpdateUserQueueRecord private constructor() : UpdatableRecordImpl<UpdateUserQueueRecord>(UpdateUserQueue.UPDATE_USER_QUEUE), Record5<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?> {
|
||||
open class UpdateUserQueueRecord private constructor() : UpdatableRecordImpl<UpdateUserQueueRecord>(UpdateUserQueue.UPDATE_USER_QUEUE), Record7<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?, Int?, Int?> {
|
||||
|
||||
open var id: Int?
|
||||
set(value): Unit = set(0, value)
|
||||
@ -41,6 +41,14 @@ open class UpdateUserQueueRecord private constructor() : UpdatableRecordImpl<Upd
|
||||
set(value): Unit = set(4, value)
|
||||
get(): LocalDateTime? = get(4) as LocalDateTime?
|
||||
|
||||
open var progressCurrent: Int?
|
||||
set(value): Unit = set(5, value)
|
||||
get(): Int? = get(5) as Int?
|
||||
|
||||
open var progressTotal: Int?
|
||||
set(value): Unit = set(6, value)
|
||||
get(): Int? = get(6) as Int?
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Primary key information
|
||||
// -------------------------------------------------------------------------
|
||||
@ -48,26 +56,32 @@ open class UpdateUserQueueRecord private constructor() : UpdatableRecordImpl<Upd
|
||||
override fun key(): Record1<Int?> = super.key() as Record1<Int?>
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Record5 type implementation
|
||||
// Record7 type implementation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
override fun fieldsRow(): Row5<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?> = super.fieldsRow() as Row5<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?>
|
||||
override fun valuesRow(): Row5<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?> = super.valuesRow() as Row5<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?>
|
||||
override fun fieldsRow(): Row7<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?, Int?, Int?> = super.fieldsRow() as Row7<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?, Int?, Int?>
|
||||
override fun valuesRow(): Row7<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?, Int?, Int?> = super.valuesRow() as Row7<Int?, Long?, Boolean?, LocalDateTime?, LocalDateTime?, Int?, Int?>
|
||||
override fun field1(): Field<Int?> = UpdateUserQueue.UPDATE_USER_QUEUE.ID
|
||||
override fun field2(): Field<Long?> = UpdateUserQueue.UPDATE_USER_QUEUE.USER_ID
|
||||
override fun field3(): Field<Boolean?> = UpdateUserQueue.UPDATE_USER_QUEUE.PROCESSED
|
||||
override fun field4(): Field<LocalDateTime?> = UpdateUserQueue.UPDATE_USER_QUEUE.CREATED_AT
|
||||
override fun field5(): Field<LocalDateTime?> = UpdateUserQueue.UPDATE_USER_QUEUE.PROCESSED_AT
|
||||
override fun field6(): Field<Int?> = UpdateUserQueue.UPDATE_USER_QUEUE.PROGRESS_CURRENT
|
||||
override fun field7(): Field<Int?> = UpdateUserQueue.UPDATE_USER_QUEUE.PROGRESS_TOTAL
|
||||
override fun component1(): Int? = id
|
||||
override fun component2(): Long = userId
|
||||
override fun component3(): Boolean? = processed
|
||||
override fun component4(): LocalDateTime? = createdAt
|
||||
override fun component5(): LocalDateTime? = processedAt
|
||||
override fun component6(): Int? = progressCurrent
|
||||
override fun component7(): Int? = progressTotal
|
||||
override fun value1(): Int? = id
|
||||
override fun value2(): Long = userId
|
||||
override fun value3(): Boolean? = processed
|
||||
override fun value4(): LocalDateTime? = createdAt
|
||||
override fun value5(): LocalDateTime? = processedAt
|
||||
override fun value6(): Int? = progressCurrent
|
||||
override fun value7(): Int? = progressTotal
|
||||
|
||||
override fun value1(value: Int?): UpdateUserQueueRecord {
|
||||
set(0, value)
|
||||
@ -94,24 +108,38 @@ open class UpdateUserQueueRecord private constructor() : UpdatableRecordImpl<Upd
|
||||
return this
|
||||
}
|
||||
|
||||
override fun values(value1: Int?, value2: Long?, value3: Boolean?, value4: LocalDateTime?, value5: LocalDateTime?): UpdateUserQueueRecord {
|
||||
override fun value6(value: Int?): UpdateUserQueueRecord {
|
||||
set(5, value)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun value7(value: Int?): UpdateUserQueueRecord {
|
||||
set(6, value)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun values(value1: Int?, value2: Long?, value3: Boolean?, value4: LocalDateTime?, value5: LocalDateTime?, value6: Int?, value7: Int?): UpdateUserQueueRecord {
|
||||
this.value1(value1)
|
||||
this.value2(value2)
|
||||
this.value3(value3)
|
||||
this.value4(value4)
|
||||
this.value5(value5)
|
||||
this.value6(value6)
|
||||
this.value7(value7)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a detached, initialised UpdateUserQueueRecord
|
||||
*/
|
||||
constructor(id: Int? = null, userId: Long, processed: Boolean? = null, createdAt: LocalDateTime? = null, processedAt: LocalDateTime? = null): this() {
|
||||
constructor(id: Int? = null, userId: Long, processed: Boolean? = null, createdAt: LocalDateTime? = null, processedAt: LocalDateTime? = null, progressCurrent: Int? = null, progressTotal: Int? = null): this() {
|
||||
this.id = id
|
||||
this.userId = userId
|
||||
this.processed = processed
|
||||
this.createdAt = createdAt
|
||||
this.processedAt = processedAt
|
||||
this.progressCurrent = progressCurrent
|
||||
this.progressTotal = progressTotal
|
||||
resetChangedOnNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
package com.nisemoe.nise
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class UserQueueDetails(
|
||||
val isProcessing: Boolean,
|
||||
val lastCompletedUpdate: LocalDateTime?,
|
||||
|
||||
val progressCurrent: Int?,
|
||||
val progressTotal: Int?,
|
||||
)
|
||||
|
||||
data class UserDetails(
|
||||
val user_id: Long,
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package com.nisemoe.nise.config
|
||||
|
||||
import jakarta.servlet.*
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
|
||||
class CustomCorsFilter : Filter {
|
||||
|
||||
@Value("\${ORIGIN:http://localhost:4200}")
|
||||
private lateinit var origin: String
|
||||
|
||||
override fun init(filterConfig: FilterConfig) {
|
||||
// We don't really need to do anything special here.
|
||||
}
|
||||
|
||||
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, filterChain: FilterChain) {
|
||||
val response = servletResponse as HttpServletResponse
|
||||
response.setHeader("Access-Control-Allow-Origin", origin)
|
||||
response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS,PATCH")
|
||||
response.setHeader(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Access-Control-Allow-Headers, Origin,Accept, X-NISE-API, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
)
|
||||
response.setHeader("Access-Control-Allow-Credentials", "true")
|
||||
filterChain.doFilter(servletRequest, servletResponse)
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
// We don't really need to do anything special here.
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
|
||||
import org.springframework.session.web.http.CookieSerializer
|
||||
import org.springframework.session.web.http.DefaultCookieSerializer
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.filter.CorsFilter
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
@ -58,10 +59,16 @@ class SecurityConfig {
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun customCorsFilter(): CustomCorsFilter {
|
||||
return CustomCorsFilter()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
http
|
||||
.csrf { csrf -> csrf.disable() }
|
||||
.addFilterBefore(customCorsFilter(), CorsFilter::class.java)
|
||||
.oauth2Login { oauthLogin ->
|
||||
oauthLogin.successHandler(CustomAuthenticationSuccessHandler())
|
||||
}
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
package com.nisemoe.nise.config
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
class WebConfig: WebMvcConfigurer {
|
||||
|
||||
@Value("\${ORIGIN:http://localhost:4200}")
|
||||
private lateinit var origin: String
|
||||
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins(origin)
|
||||
.allowCredentials(true)
|
||||
}
|
||||
|
||||
}
|
||||
@ -5,24 +5,45 @@ import com.nisemoe.nise.SimilarReplayEntry
|
||||
import com.nisemoe.nise.database.ScoreService
|
||||
import com.nisemoe.nise.SuspiciousScoreEntry
|
||||
import com.nisemoe.nise.UserDetails
|
||||
import com.nisemoe.nise.UserQueueDetails
|
||||
import com.nisemoe.nise.database.UserService
|
||||
import com.nisemoe.nise.service.UpdateUserQueueService
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@RestController
|
||||
class UserDetailsController(
|
||||
private val scoreService: ScoreService,
|
||||
private val userService: UserService
|
||||
private val userService: UserService,
|
||||
private val userQueueService: UpdateUserQueueService
|
||||
) {
|
||||
|
||||
data class UserDetailsResponse(
|
||||
val user_details: UserDetails,
|
||||
val queue_details: UserQueueDetails,
|
||||
val suspicious_scores: List<SuspiciousScoreEntry>,
|
||||
val similar_replays: List<SimilarReplayEntry>
|
||||
val similar_replays: List<SimilarReplayEntry>,
|
||||
val total_scores: Int = 0,
|
||||
)
|
||||
|
||||
data class UserQueueRequest(
|
||||
val userId: Long
|
||||
)
|
||||
|
||||
@PostMapping("user-queue")
|
||||
fun addUserToQueue(@RequestBody request: UserQueueRequest): ResponseEntity<Unit> {
|
||||
val inserted = this.userQueueService.insertUser(request.userId)
|
||||
return if(inserted)
|
||||
ResponseEntity.ok().build()
|
||||
else
|
||||
ResponseEntity.badRequest().build()
|
||||
}
|
||||
|
||||
@GetMapping("user-details/{userId}")
|
||||
fun getUserDetails(@PathVariable userId: String): ResponseEntity<UserDetailsResponse> {
|
||||
val userDetails = this.userService.getUserDetails(username = userId)
|
||||
@ -33,8 +54,10 @@ class UserDetailsController(
|
||||
|
||||
val response = UserDetailsResponse(
|
||||
user_details = userDetails,
|
||||
queue_details = this.userQueueService.getUserQueueDetails(userDetails.user_id),
|
||||
suspicious_scores = this.scoreService.getSuspiciousScores(suspiciousScoresCondition),
|
||||
similar_replays = this.scoreService.getSimilarReplaysForUserId(userDetails.user_id)
|
||||
similar_replays = this.scoreService.getSimilarReplaysForUserId(userDetails.user_id),
|
||||
total_scores = this.userService.getTotalUserScores(userDetails.user_id)
|
||||
)
|
||||
return ResponseEntity.ok(response)
|
||||
}
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
package com.nisemoe.nise.database
|
||||
|
||||
import com.nisemoe.generated.tables.records.UpdateUserQueueRecord
|
||||
import com.nisemoe.generated.tables.records.UsersRecord
|
||||
import com.nisemoe.generated.tables.references.SCORES
|
||||
import com.nisemoe.generated.tables.references.UPDATE_USER_QUEUE
|
||||
import com.nisemoe.generated.tables.references.USERS
|
||||
import com.nisemoe.nise.osu.OsuApi
|
||||
import com.nisemoe.nise.osu.OsuApiModels
|
||||
import com.nisemoe.nise.Format
|
||||
import com.nisemoe.nise.UserDetails
|
||||
import com.nisemoe.nise.UserQueueDetails
|
||||
import com.nisemoe.nise.osu.OsuApi
|
||||
import com.nisemoe.nise.osu.OsuApiModels
|
||||
import org.jooq.DSLContext
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
@ -36,6 +40,10 @@ class UserService(
|
||||
|
||||
}
|
||||
|
||||
fun getTotalUserScores(userId: Long): Int {
|
||||
return dslContext.fetchCount(SCORES, SCORES.USER_ID.eq(userId))
|
||||
}
|
||||
|
||||
fun getUserDetails(username: String): UserDetails? {
|
||||
val user = dslContext.selectFrom(USERS)
|
||||
.where(USERS.USERNAME.equalIgnoreCase(username.lowercase()))
|
||||
|
||||
@ -6,6 +6,7 @@ import com.nisemoe.konata.Replay
|
||||
import com.nisemoe.konata.ReplaySetComparison
|
||||
import com.nisemoe.konata.compareReplaySet
|
||||
import com.nisemoe.nise.Format.Companion.fromJudgementType
|
||||
import com.nisemoe.nise.UserQueueDetails
|
||||
import com.nisemoe.nise.database.ScoreService
|
||||
import com.nisemoe.nise.database.UserService
|
||||
import com.nisemoe.nise.integrations.CircleguardService
|
||||
@ -169,6 +170,16 @@ class ImportScores(
|
||||
}
|
||||
}
|
||||
|
||||
var current = 0
|
||||
|
||||
val lastCompletedUpdate = dslContext.select(UPDATE_USER_QUEUE.PROCESSED_AT)
|
||||
.from(UPDATE_USER_QUEUE)
|
||||
.where(UPDATE_USER_QUEUE.USER_ID.eq(userId))
|
||||
.and(UPDATE_USER_QUEUE.PROCESSED.isTrue)
|
||||
.orderBy(UPDATE_USER_QUEUE.PROCESSED_AT.desc())
|
||||
.limit(1)
|
||||
.fetchOneInto(LocalDateTime::class.java)
|
||||
|
||||
for(topScore in topUserScores) {
|
||||
if(topScore.beatmap != null && topScore.beatmapset != null) {
|
||||
val beatmapExists = dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(topScore.beatmap.id))
|
||||
@ -187,10 +198,34 @@ class ImportScores(
|
||||
this.statistics.beatmapsAddedToDatabase++
|
||||
}
|
||||
|
||||
// Update the database
|
||||
dslContext.update(UPDATE_USER_QUEUE)
|
||||
.set(UPDATE_USER_QUEUE.PROGRESS_CURRENT, current)
|
||||
.set(UPDATE_USER_QUEUE.PROGRESS_TOTAL, topUserScores.size)
|
||||
.where(UPDATE_USER_QUEUE.USER_ID.eq(userId))
|
||||
.and(UPDATE_USER_QUEUE.PROCESSED.isFalse)
|
||||
.execute()
|
||||
|
||||
val currentQueueDetails = UserQueueDetails(
|
||||
isProcessing = true,
|
||||
lastCompletedUpdate = lastCompletedUpdate,
|
||||
progressCurrent = current,
|
||||
progressTotal = topUserScores.size
|
||||
)
|
||||
|
||||
// Update the frontend
|
||||
messagingTemplate.convertAndSend(
|
||||
"/topic/live-user/${userId}",
|
||||
currentQueueDetails
|
||||
)
|
||||
|
||||
this.insertAndProcessNewScore(topScore.beatmap.id, topScore)
|
||||
}
|
||||
current += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Update the backend
|
||||
this.updateUserQueueService.setUserAsProcessed(userId)
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,53 @@
|
||||
package com.nisemoe.nise.service
|
||||
|
||||
import com.nisemoe.generated.tables.records.UpdateUserQueueRecord
|
||||
import com.nisemoe.generated.tables.references.UPDATE_USER_QUEUE
|
||||
import com.nisemoe.nise.UserQueueDetails
|
||||
import org.jooq.DSLContext
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
class UpdateUserQueueService(
|
||||
private val dslContext: DSLContext
|
||||
private val dslContext: DSLContext,
|
||||
private val messagingTemplate: SimpMessagingTemplate
|
||||
) {
|
||||
|
||||
fun getUserQueueDetails(userId: Long): UserQueueDetails {
|
||||
val isProcessing = dslContext.fetchExists(
|
||||
UPDATE_USER_QUEUE,
|
||||
UPDATE_USER_QUEUE.USER_ID.eq(userId).and(UPDATE_USER_QUEUE.PROCESSED.isFalse)
|
||||
)
|
||||
|
||||
val lastCompletedUpdate = dslContext.select(UPDATE_USER_QUEUE.PROCESSED_AT)
|
||||
.from(UPDATE_USER_QUEUE)
|
||||
.where(UPDATE_USER_QUEUE.USER_ID.eq(userId))
|
||||
.and(UPDATE_USER_QUEUE.PROCESSED.isTrue)
|
||||
.orderBy(UPDATE_USER_QUEUE.PROCESSED_AT.desc())
|
||||
.limit(1)
|
||||
.fetchOneInto(LocalDateTime::class.java)
|
||||
|
||||
val currentProgress = dslContext.select(
|
||||
UPDATE_USER_QUEUE.PROGRESS_CURRENT,
|
||||
UPDATE_USER_QUEUE.PROGRESS_TOTAL
|
||||
)
|
||||
.from(UPDATE_USER_QUEUE)
|
||||
.where(UPDATE_USER_QUEUE.USER_ID.eq(userId))
|
||||
.and(UPDATE_USER_QUEUE.PROCESSED.isFalse)
|
||||
.orderBy(UPDATE_USER_QUEUE.PROCESSED_AT.desc())
|
||||
.limit(1)
|
||||
.fetchOneInto(UpdateUserQueueRecord::class.java)
|
||||
|
||||
return UserQueueDetails(
|
||||
isProcessing,
|
||||
lastCompletedUpdate,
|
||||
currentProgress?.progressCurrent,
|
||||
currentProgress?.progressTotal
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun getQueue(): List<Long> {
|
||||
return dslContext.select(UPDATE_USER_QUEUE.USER_ID)
|
||||
.from(UPDATE_USER_QUEUE)
|
||||
@ -18,17 +56,23 @@ class UpdateUserQueueService(
|
||||
.fetchInto(Long::class.java)
|
||||
}
|
||||
|
||||
fun insertUser(userId: Long) {
|
||||
/**
|
||||
* Inserts a user into the update queue if they are not already in the queue.
|
||||
* @return true if the user was inserted, false if the user was already in the queue
|
||||
*/
|
||||
fun insertUser(userId: Long): Boolean {
|
||||
val exists = dslContext.fetchExists(UPDATE_USER_QUEUE,
|
||||
UPDATE_USER_QUEUE.USER_ID.eq(userId),
|
||||
UPDATE_USER_QUEUE.PROCESSED.isFalse
|
||||
)
|
||||
if (exists)
|
||||
return
|
||||
return false
|
||||
|
||||
dslContext.insertInto(UPDATE_USER_QUEUE)
|
||||
val insertedRows = dslContext.insertInto(UPDATE_USER_QUEUE)
|
||||
.set(UPDATE_USER_QUEUE.USER_ID, userId)
|
||||
.execute()
|
||||
|
||||
return insertedRows == 1
|
||||
}
|
||||
|
||||
fun setUserAsProcessed(userId: Long) {
|
||||
@ -38,7 +82,12 @@ class UpdateUserQueueService(
|
||||
.where(UPDATE_USER_QUEUE.USER_ID.eq(userId))
|
||||
.and(UPDATE_USER_QUEUE.PROCESSED.isFalse)
|
||||
.execute()
|
||||
|
||||
// Notify the user that their queue has been processed with fresh info
|
||||
messagingTemplate.convertAndSend(
|
||||
"/topic/live-user/${userId}",
|
||||
this.getUserQueueDetails(userId)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
ALTER TABLE public.update_user_queue
|
||||
ADD COLUMN progress_current int;
|
||||
|
||||
ALTER TABLE public.update_user_queue
|
||||
ADD COLUMN progress_total int;
|
||||
@ -11,3 +11,11 @@ export interface UserDetails {
|
||||
|
||||
suspicious_scores?: number;
|
||||
}
|
||||
|
||||
export interface UserQueueDetails {
|
||||
isProcessing: boolean;
|
||||
lastCompletedUpdate: string | null;
|
||||
|
||||
progressCurrent: number | null;
|
||||
progressTotal: number | null;
|
||||
}
|
||||
|
||||
@ -40,6 +40,24 @@
|
||||
<li *ngIf="this.userInfo.user_details.playcount">Playcount: {{ this.userInfo.user_details.playcount | number: '1.0-1' }}</li>
|
||||
</ul>
|
||||
|
||||
<ng-container *ngIf="!this.userInfo.queue_details.isProcessing; else updateProgress">
|
||||
<a (click)="this.addUserToQueue()" class="btn btn-success pointer" target="_blank">
|
||||
update user scores now!
|
||||
</a> <span style="margin-left: 4px">|</span>
|
||||
last update: {{ this.userInfo.queue_details.lastCompletedUpdate ? this.userInfo.queue_details.lastCompletedUpdate : 'never'}}
|
||||
</ng-container>
|
||||
<ng-template #updateProgress>
|
||||
<div class="progress">
|
||||
<span class="btn-warning">updating now!</span> <span style="margin-left: 4px">|</span> progress: {{ this.userInfo.queue_details.progressCurrent ? this.userInfo.queue_details.progressCurrent : "?" }}/{{ this.userInfo.queue_details.progressTotal ? this.userInfo.queue_details.progressTotal : "?" }}
|
||||
<ng-container *ngIf="!this.userInfo.queue_details.progressTotal && !this.userInfo.queue_details.progressCurrent; else loading">
|
||||
<span style="font-weight: bold">(in queue)</span>
|
||||
</ng-container>
|
||||
<ng-template #loading>
|
||||
<app-cute-loading></app-cute-loading>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<h4>Suspicious Scores ({{ this.userInfo.suspicious_scores.length }})</h4>
|
||||
<div class="table">
|
||||
<table class="table">
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
import {Component, OnChanges, OnInit} from '@angular/core';
|
||||
import {Component, OnChanges, OnDestroy, OnInit} from '@angular/core';
|
||||
import {SimilarReplay, SuspiciousScore} from "../replays";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {catchError, EMPTY, finalize, Observable} from "rxjs";
|
||||
import {catchError, EMPTY, finalize, Observable, Subscription} from "rxjs";
|
||||
import {environment} from "../../environments/environment";
|
||||
import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
|
||||
import {ActivatedRoute, RouterLink} from "@angular/router";
|
||||
import {UserDetails} from "../userDetails";
|
||||
import {UserDetails, UserQueueDetails} from "../userDetails";
|
||||
import {countryCodeToFlag, formatDuration} from "../format";
|
||||
import {Title} from "@angular/platform-browser";
|
||||
import {RxStompService} from "../../corelib/stomp/stomp.service";
|
||||
import {Message} from "@stomp/stompjs/esm6";
|
||||
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
|
||||
|
||||
interface UserInfo {
|
||||
user_details: UserDetails;
|
||||
queue_details: UserQueueDetails;
|
||||
suspicious_scores: SuspiciousScore[];
|
||||
similar_replays: SimilarReplay[];
|
||||
total_scores: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@ -24,22 +29,26 @@ interface UserInfo {
|
||||
NgForOf,
|
||||
RouterLink,
|
||||
NgIf,
|
||||
NgOptimizedImage
|
||||
NgOptimizedImage,
|
||||
CuteLoadingComponent
|
||||
],
|
||||
templateUrl: './view-user.component.html',
|
||||
styleUrl: './view-user.component.css'
|
||||
})
|
||||
export class ViewUserComponent implements OnInit, OnChanges {
|
||||
export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
isLoading = false;
|
||||
notFound = false;
|
||||
userId: string | null = null;
|
||||
userInfo: UserInfo | null = null;
|
||||
|
||||
liveUserSub: Subscription | undefined;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private title: Title
|
||||
private title: Title,
|
||||
private rxStompService: RxStompService,
|
||||
) { }
|
||||
|
||||
getUserInfo(): Observable<UserInfo> {
|
||||
@ -74,14 +83,40 @@ export class ViewUserComponent implements OnInit, OnChanges {
|
||||
this.notFound = false;
|
||||
this.userInfo = response;
|
||||
this.title.setTitle(`${this.userInfo.user_details.username}`);
|
||||
this.subscribeToUser();
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly formatDuration = formatDuration;
|
||||
ngOnDestroy(): void {
|
||||
this.liveUserSub?.unsubscribe();
|
||||
}
|
||||
|
||||
addUserToQueue(): void {
|
||||
const body = {
|
||||
userId: this.userInfo?.user_details.user_id
|
||||
}
|
||||
this.httpClient.post(`${environment.apiUrl}/user-queue`, body).subscribe(
|
||||
() => {
|
||||
this.userInfo!.queue_details.isProcessing = true;
|
||||
},
|
||||
(error) => {
|
||||
alert("Failed to add user to queue.");
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
subscribeToUser(): void {
|
||||
this.liveUserSub = this.rxStompService
|
||||
.watch(`/topic/live-user/${this.userInfo?.user_details.user_id}`)
|
||||
.subscribe((message: Message) => {
|
||||
this.userInfo!.queue_details = JSON.parse(message.body);
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly formatDuration = formatDuration;
|
||||
protected readonly countryCodeToFlag = countryCodeToFlag;
|
||||
|
||||
|
||||
|
||||
@ -141,6 +141,27 @@ a.btn {
|
||||
border: 1px dotted #b3b8c3;
|
||||
}
|
||||
|
||||
a.btn-success {
|
||||
color: #5eaf78;
|
||||
border: 1px dotted #5eaf78;
|
||||
}
|
||||
|
||||
a.btn-success:hover {
|
||||
color: #2af171;
|
||||
border: 1px dotted #2af171;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
padding: 2px;
|
||||
color: #f1c40f;
|
||||
border: 1px dotted #f1c40f;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
color: #f1c40f;
|
||||
border: 1px dotted #f1c40f;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
{{ dots }}
|
||||
@ -0,0 +1,20 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cute-loading',
|
||||
standalone: true,
|
||||
templateUrl: './cute-loading.component.html'
|
||||
})
|
||||
export class CuteLoadingComponent implements OnInit {
|
||||
dots = '';
|
||||
|
||||
ngOnInit() {
|
||||
setInterval(() => {
|
||||
this.dots += '.';
|
||||
if (this.dots.length > 3) {
|
||||
this.dots = '.';
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user