mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-09-27 17:27:55 +03:00
[M] Move common models to shared
This commit is contained in:
@@ -118,7 +118,7 @@ hibernate {
|
||||
}
|
||||
|
||||
kapt {
|
||||
includeCompileClasspath = false
|
||||
includeCompileClasspath = true
|
||||
keepJavacAnnotationProcessors = true
|
||||
}
|
||||
|
||||
|
||||
@@ -179,14 +179,14 @@ class Fedy(
|
||||
val card = cardRepo.findByExtId(req.extId)
|
||||
?: (404 - "Card with extId ${req.extId} not found")
|
||||
val cardTimestamp = cardService.getCardTimestamp(card, req.game)
|
||||
if (cardTimestamp.updatedAt.toEpochMilli() == req.updatedAtMs) return@handleFedy DataPullRes(error = null, result = null) // No changes
|
||||
val isRebased = req.createdAtMs > 0 && cardTimestamp.createdAt.toEpochMilli() > req.createdAtMs
|
||||
if (cardTimestamp.updatedAt == req.updatedAtMs) return@handleFedy DataPullRes(error = null, result = null) // No changes
|
||||
val isRebased = req.createdAtMs > 0 && cardTimestamp.createdAt > req.createdAtMs
|
||||
val exportOptions = if (!isRebased) { req.exportOptions } else { req.exportOptions.copy(playlogAfter = null) }
|
||||
{
|
||||
DataPullRes(result = DataPullResult(data = when (req.game) {
|
||||
"mai2" -> mai2Import.export(card, exportOptions)
|
||||
else -> 406 - "Unsupported game"
|
||||
}, createdAtMs = cardTimestamp.createdAt.toEpochMilli(), updatedAtMs = cardTimestamp.updatedAt.toEpochMilli(), isRebased = isRebased))
|
||||
}, createdAtMs = cardTimestamp.createdAt, updatedAtMs = cardTimestamp.updatedAt, isRebased = isRebased))
|
||||
} caught { DataPullRes(error = it) }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,82 +1,22 @@
|
||||
package icu.samnyan.aqua.net
|
||||
|
||||
import ext.HTTP
|
||||
import ext.mut
|
||||
import ext.toJson
|
||||
import icu.samnyan.aqua.net.games.BaseEntity
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import jakarta.persistence.Entity
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.springframework.stereotype.Service
|
||||
import java.text.Normalizer
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "aqua-net.openai")
|
||||
class OpenAIConfig {
|
||||
var apiKey: String = ""
|
||||
@Repository
|
||||
interface SafetyRepo : JpaRepository<Safety, Long> {
|
||||
fun findBySafetyId(safetyId: String): Safety?
|
||||
}
|
||||
|
||||
@Entity
|
||||
class AquaNetSafety : BaseEntity() {
|
||||
var content: String = ""
|
||||
var safe: Boolean = false
|
||||
}
|
||||
|
||||
interface AquaNetSafetyRepo : JpaRepository<AquaNetSafety, Long> {
|
||||
fun findByContent(content: String): AquaNetSafety?
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class OpenAIResp<T>(
|
||||
val id: String,
|
||||
val model: String,
|
||||
val results: List<T>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OpenAIMod(
|
||||
val flagged: Boolean,
|
||||
val categories: Map<String, Boolean>,
|
||||
val categoryScores: Map<String, Double>,
|
||||
)
|
||||
|
||||
@Service
|
||||
class AquaNetSafetyService(
|
||||
val safety: AquaNetSafetyRepo,
|
||||
val openAIConfig: OpenAIConfig
|
||||
) {
|
||||
/**
|
||||
* It is very inefficient to have query inside a loop, so we batch the query.
|
||||
*/
|
||||
suspend fun isSafeBatch(rawContents: List<String>): List<Boolean> {
|
||||
val contents = rawContents.map { Normalizer.normalize(it, Normalizer.Form.NFKC) }
|
||||
val origMap = safety.findAll().associateBy { it.content }.mut
|
||||
val map = safety.findAll().associateBy { it.content.lowercase().trim() }.mut
|
||||
class AquaNetSafetyService(val safetyRepo: SafetyRepo) {
|
||||
fun isSafe(safetyId: String): Boolean {
|
||||
val safety = safetyRepo.findBySafetyId(safetyId)
|
||||
return safety == null || safety.status == 0
|
||||
}
|
||||
|
||||
// Process unseen content with OpenAI
|
||||
val news = contents.filter { it.lowercase().trim() !in map && it !in contents }.map { inp ->
|
||||
HTTP.post("https://api.openai.com/v1/moderations") {
|
||||
header("Authorization", "Bearer ${openAIConfig.apiKey}")
|
||||
header("Content-Type", "application/json")
|
||||
setBody(mapOf("input" to inp).toJson())
|
||||
}.let {
|
||||
if (!it.status.isSuccess()) throw Exception("OpenAI request failed for $inp")
|
||||
val body = it.body<OpenAIResp<OpenAIMod>>()
|
||||
AquaNetSafety().apply {
|
||||
content = inp
|
||||
safe = !body.results.first().flagged
|
||||
}
|
||||
}
|
||||
}
|
||||
if (news.isNotEmpty()) safety.saveAll(news)
|
||||
news.associateByTo(origMap) { it.content }
|
||||
news.associateByTo(map) { it.content.lowercase().trim() }
|
||||
|
||||
return contents.map { map[it.lowercase().trim()]?.safe ?: origMap[it]?.safe ?: true }
|
||||
fun isSafeBatch(safetyIds: List<String>): List<Boolean> {
|
||||
return safetyIds.map { isSafe(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,10 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
@Table(name = "aqua_net_email_confirmation")
|
||||
class EmailConfirmation(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@Column(nullable = false)
|
||||
var token: String = "",
|
||||
|
||||
// Token creation time
|
||||
@Column(nullable = false)
|
||||
var createdAt: Instant = Instant.now(),
|
||||
|
||||
// Linking to the AquaNetUser
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "auId", referencedColumnName = "auId")
|
||||
var aquaNetUser: AquaNetUser = AquaNetUser()
|
||||
) : Serializable
|
||||
|
||||
@Repository
|
||||
interface EmailConfirmationRepo : JpaRepository<EmailConfirmation, Long> {
|
||||
fun findByToken(token: String): EmailConfirmation?
|
||||
fun findByAquaNetUserAuId(auId: Long): List<EmailConfirmation>
|
||||
}
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface EmailConfirmationRepo : JpaRepository<EmailConfirmation, Long> {
|
||||
fun findByToken(token: String): EmailConfirmation?
|
||||
fun findByAquaNetUserAuId(auId: Long): List<EmailConfirmation>
|
||||
}
|
||||
|
||||
@@ -1,33 +1,10 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
@Table(name = "aqua_net_email_reset_password")
|
||||
class ResetPassword(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@Column(nullable = false)
|
||||
var token: String = "",
|
||||
|
||||
// Token creation time
|
||||
@Column(nullable = false)
|
||||
var createdAt: Instant = Instant.now(),
|
||||
|
||||
// Linking to the AquaNetUser
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "auId", referencedColumnName = "auId")
|
||||
var aquaNetUser: AquaNetUser = AquaNetUser()
|
||||
) : Serializable
|
||||
|
||||
@Repository
|
||||
interface ResetPasswordRepo : JpaRepository<ResetPassword, Long> {
|
||||
fun findByToken(token: String): ResetPassword?
|
||||
fun findByAquaNetUserAuId(auId: Long): List<ResetPassword>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,7 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import ext.SettingField
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
@Entity
|
||||
class AquaGameOptions(
|
||||
@Id @JsonIgnore
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_music")
|
||||
var mai2UnlockMusic: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_chara")
|
||||
var mai2UnlockChara: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_chara_max_level")
|
||||
var mai2UnlockCharaMaxLevel: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_partners")
|
||||
var mai2UnlockPartners: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_collectables")
|
||||
var mai2UnlockCollectables: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_tickets")
|
||||
var mai2UnlockTickets: Boolean = false,
|
||||
|
||||
@SettingField("wacca")
|
||||
var waccaUnlockMusic: Boolean = false,
|
||||
@SettingField("wacca")
|
||||
var waccaUnlockPlates: Boolean = false,
|
||||
@SettingField("wacca")
|
||||
var waccaUnlockCollectables: Boolean = false,
|
||||
@SettingField("wacca")
|
||||
var waccaUnlockTickets: Boolean = false,
|
||||
@SettingField("wacca")
|
||||
var waccaInfiniteWp: Boolean = false,
|
||||
@SettingField("wacca")
|
||||
var waccaAlwaysVip: Boolean = false,
|
||||
|
||||
@SettingField("chu3")
|
||||
var chusanTeamName: String = "",
|
||||
|
||||
@SettingField("chu3")
|
||||
var chusanInfinitePenguins: Boolean = false,
|
||||
|
||||
@SettingField("chu3-matching")
|
||||
var chusanMatchingServer: String = "",
|
||||
|
||||
@SettingField("chu3-matching")
|
||||
var chusanMatchingReflector: String = "",
|
||||
|
||||
@SettingField("chu3-linked-verse")
|
||||
var chusanLvUnlockAll: Boolean = false,
|
||||
@SettingField("chu3-linked-verse")
|
||||
var chusanLvDifficulty: Int = 1,
|
||||
|
||||
@SettingField("chu3-matching-chat")
|
||||
var chusanSymbolChat1: Int? = null,
|
||||
@SettingField("chu3-matching-chat")
|
||||
var chusanSymbolChat2: Int? = null,
|
||||
@SettingField("chu3-matching-chat")
|
||||
var chusanSymbolChat3: Int? = null,
|
||||
@SettingField("chu3-matching-chat")
|
||||
var chusanSymbolChat4: Int? = null,
|
||||
|
||||
@SettingField("mai2")
|
||||
var enableMusicRank: Boolean = true,
|
||||
|
||||
@SettingField("ongeki")
|
||||
var ongekiInfiniteKaika: Boolean = false,
|
||||
|
||||
@SettingField("profile")
|
||||
var countryOverride: String = "",
|
||||
)
|
||||
|
||||
interface AquaGameOptionsRepo : JpaRepository<AquaGameOptions, Long>
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
// Entity AquaGameOptions is now in :shared
|
||||
|
||||
interface AquaGameOptionsRepo : JpaRepository<AquaGameOptions, Long>
|
||||
|
||||
@@ -1,30 +1,7 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
fun getTokenExpiry() = Instant.now().plusSeconds(7 * 86400)
|
||||
|
||||
@Entity
|
||||
@Table(name = "aqua_net_session")
|
||||
class SessionToken(
|
||||
@Id
|
||||
@Column(nullable = false)
|
||||
var token: String = UUID.randomUUID().toString(),
|
||||
|
||||
// Token creation time
|
||||
@Column(nullable = false)
|
||||
var expiry: Instant = getTokenExpiry(),
|
||||
|
||||
// Linking to the AquaNetUser
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "auId", referencedColumnName = "auId")
|
||||
var aquaNetUser: AquaNetUser = AquaNetUser()
|
||||
) : Serializable
|
||||
|
||||
@Repository
|
||||
interface SessionTokenRepo : JpaRepository<SessionToken, String> {
|
||||
|
||||
@@ -1,251 +1,165 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.net.UserRegistrar.Companion.cardExtIdEnd
|
||||
import icu.samnyan.aqua.net.UserRegistrar.Companion.cardExtIdStart
|
||||
import icu.samnyan.aqua.net.components.JWT
|
||||
import icu.samnyan.aqua.sega.allnet.AllNetProps
|
||||
import icu.samnyan.aqua.sega.allnet.KeyChipRepo
|
||||
import icu.samnyan.aqua.sega.allnet.KeychipSession
|
||||
import icu.samnyan.aqua.sega.general.GameMusicPopularity
|
||||
import icu.samnyan.aqua.sega.general.dao.CardRepository
|
||||
import icu.samnyan.aqua.sega.general.model.Card
|
||||
import icu.samnyan.aqua.sega.general.service.CardService
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.Serializable
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.KMutableProperty
|
||||
import kotlin.reflect.full.functions
|
||||
|
||||
@Entity
|
||||
class AquaNetUser(
|
||||
@JsonIgnore
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var auId: Long = 0,
|
||||
|
||||
@Column(nullable = false, unique = true, length = 32)
|
||||
var username: String = "",
|
||||
|
||||
// Login credentials
|
||||
@Column(nullable = false, unique = true)
|
||||
var email: String = "",
|
||||
|
||||
@JsonIgnore
|
||||
@Column(nullable = false)
|
||||
var pwHash: String = "",
|
||||
|
||||
@Column(nullable = true, length = 32)
|
||||
var displayName: String = "",
|
||||
|
||||
// Country code at most 3 characters
|
||||
@Column(length = 3)
|
||||
var country: String = "",
|
||||
|
||||
// Region code at most 2 characters
|
||||
@Column(length = 2)
|
||||
var region: String = "",
|
||||
|
||||
// Last login time
|
||||
var lastLogin: Long = 0L,
|
||||
|
||||
// Registration time
|
||||
var regTime: Long = 0L,
|
||||
|
||||
// Profile fields
|
||||
var profileLocation: String? = "",
|
||||
var profileBio: String? = "",
|
||||
var profilePicture: String? = "",
|
||||
var optOutOfLeaderboard: Boolean = false,
|
||||
|
||||
// Email confirmation
|
||||
var emailConfirmed: Boolean = false,
|
||||
|
||||
@OneToOne(cascade = [CascadeType.ALL])
|
||||
@JoinColumn(name = "ghostCard", unique = true, nullable = false)
|
||||
var ghostCard: Card = Card(),
|
||||
|
||||
// One user can have multiple cards
|
||||
@OneToMany(mappedBy = "aquaUser", cascade = [CascadeType.ALL])
|
||||
var cards: MutableList<Card> = mutableListOf(),
|
||||
|
||||
// Each user can have one keychip (if the user owns a cabinet)
|
||||
@JsonIgnore
|
||||
@Column(nullable = true, length = 32, unique = true)
|
||||
var keychip: Str? = null,
|
||||
|
||||
// Each user's keychip can have multiple sessions
|
||||
@JsonIgnore
|
||||
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL])
|
||||
var keychipSessions: MutableList<KeychipSession> = mutableListOf(),
|
||||
|
||||
@OneToOne(cascade = [CascadeType.ALL])
|
||||
@JoinColumn(name = "gameOptions", unique = true, nullable = true)
|
||||
var gameOptions: AquaGameOptions? = null,
|
||||
) : Serializable {
|
||||
val computedName get() = displayName.ifEmpty { username }
|
||||
|
||||
val publicFields get() = mapOf(
|
||||
"username" to username,
|
||||
"displayName" to displayName,
|
||||
"country" to country,
|
||||
"regTime" to regTime,
|
||||
"profileLocation" to profileLocation,
|
||||
"profileBio" to profileBio,
|
||||
"profilePicture" to profilePicture,
|
||||
)
|
||||
}
|
||||
|
||||
interface AquaNetUserRepo : JpaRepository<AquaNetUser, Long> {
|
||||
fun findByAuId(auId: Long): AquaNetUser?
|
||||
fun findByEmailIgnoreCase(email: String): AquaNetUser?
|
||||
fun findByUsernameIgnoreCase(username: String): AquaNetUser?
|
||||
fun findByKeychip(keychip: String): AquaNetUser?
|
||||
fun findByGhostCardExtId(extId: Long): AquaNetUser?
|
||||
}
|
||||
|
||||
data class SettingField(
|
||||
val name: Str,
|
||||
val checker: KFunction<*>,
|
||||
val setter: KMutableProperty.Setter<*>,
|
||||
)
|
||||
|
||||
/**
|
||||
* This class is a validator for user fields. It will return the parsed value if the field is valid, or
|
||||
* throw an ApiException if the field is invalid.
|
||||
*/
|
||||
@Service
|
||||
class AquaUserServices(
|
||||
val userRepo: AquaNetUserRepo,
|
||||
val cardRepo: CardRepository,
|
||||
val hasher: PasswordEncoder,
|
||||
val keyChipRepo: KeyChipRepo,
|
||||
val allNetProps: AllNetProps,
|
||||
val jwt: JWT,
|
||||
val em: EntityManager,
|
||||
val pop: GameMusicPopularity,
|
||||
val cardService: CardService,
|
||||
val sessionRepo: SessionTokenRepo,
|
||||
) {
|
||||
companion object {
|
||||
val SETTING_FIELDS = AquaUserServices::class.functions
|
||||
.filter { it.name.startsWith("check") }
|
||||
.map {
|
||||
val name = it.name.removePrefix("check").replaceFirstChar { c -> c.lowercase() }
|
||||
val prop = AquaNetUser::class.members.find { m -> m.name == name } as KMutableProperty<*>
|
||||
SettingField(name, it, prop.setter)
|
||||
}
|
||||
}
|
||||
|
||||
fun create(username: Str, email: Str, password: Str, country: Str, emailConfirmed: Boolean = false): AquaNetUser {
|
||||
// Create user
|
||||
val u = AquaNetUser(
|
||||
username = checkUsername(username),
|
||||
email = validateEmail(email),
|
||||
pwHash = checkPwHash(password),
|
||||
regTime = millis(), lastLogin = millis(), country = country,
|
||||
emailConfirmed = emailConfirmed
|
||||
)
|
||||
|
||||
// Create a ghost card
|
||||
val card = Card().apply {
|
||||
extId = cardService.randExtID(cardExtIdStart, cardExtIdEnd)
|
||||
luid = extId.toString()
|
||||
registerTime = LocalDateTime.now()
|
||||
accessTime = registerTime
|
||||
aquaUser = u
|
||||
isGhost = true
|
||||
}
|
||||
u.ghostCard = card
|
||||
|
||||
// Save the user
|
||||
userRepo.save(u)
|
||||
cardRepo.save(card)
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
fun update(user: AquaNetUser, key: Str, value: Str) {
|
||||
// Check if the key is a settable field
|
||||
val field = SETTING_FIELDS.find { it.name == key } ?: (400 - "Invalid setting")
|
||||
// Set the validated field
|
||||
field.setter.call(user, field.checker.call(this, value))
|
||||
}
|
||||
|
||||
fun clearAllSessions(user: AquaNetUser) = sessionRepo.deleteAll(sessionRepo.findByAquaNetUserAuId(user.auId))
|
||||
|
||||
suspend fun <T> byName(username: Str, callback: suspend (AquaNetUser) -> T) =
|
||||
async { userRepo.findByUsernameIgnoreCase(username) }?.let { callback(it) } ?: (404 - "User not found")
|
||||
|
||||
suspend fun cardByName(username: Str) =
|
||||
if (username.startsWith("user")) username.substring(4).toLongOrNull()
|
||||
?.let { cardRepo.findById(it)() } ?: (404 - "Card not found")
|
||||
else byName(username) { it.ghostCard }
|
||||
|
||||
suspend fun <T> cardByName(username: Str, callback: suspend (Card) -> T) = callback(cardByName(username))
|
||||
|
||||
fun validKeychip(keychipId: Str): Bool {
|
||||
if (!allNetProps.checkKeychip) return true
|
||||
if (keychipId.isBlank()) return false
|
||||
if (userRepo.findByKeychip(keychipId) != null || keyChipRepo.existsByKeychipId(keychipId)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun checkUsername(username: Str) = username.apply {
|
||||
// Check if username is valid
|
||||
if (length < 2) 400 - "Username must be at least 2 letters"
|
||||
if (length > 32) 400 - "Username too long (max 32 letters)"
|
||||
if (contains(" ")) 400 - "Username cannot contain spaces"
|
||||
|
||||
// card{id} is a reserved format
|
||||
if (startsWith("user") && substring(4).toLongOrNull() != null)
|
||||
400 - "Username cannot be 'user' + a number. This format is reserved for user IDs."
|
||||
|
||||
// Check if username is within A-Za-z0-9_-~.
|
||||
find { !it.isLetterOrDigit() && it != '_' && it != '-' && it != '~' && it != '.' }?.let {
|
||||
400 - "Username cannot contain `$it`. Please only use letters (A-Z), numbers (0-9), and `_-~.` characters. You can set a display name later."
|
||||
}
|
||||
|
||||
// Check if user with the same username exists
|
||||
if (userRepo.findByUsernameIgnoreCase(this) != null)
|
||||
400 - "User with username `$this` already exists"
|
||||
}
|
||||
|
||||
fun validateEmail(email: Str) = email.apply {
|
||||
// Check if email is valid
|
||||
if (!isValidEmail()) 400 - "Invalid email"
|
||||
|
||||
// Check if user with the same email exists
|
||||
if (userRepo.findByEmailIgnoreCase(email) != null)
|
||||
400 - "User with email `$email` already exists"
|
||||
}
|
||||
|
||||
fun checkPwHash(password: Str) = password.run {
|
||||
// Validate password
|
||||
if (length < 8) 400 - "Password must be at least 8 characters"
|
||||
|
||||
hasher.encode(this)
|
||||
}
|
||||
|
||||
fun checkDisplayName(displayName: Str) = displayName.apply {
|
||||
// Check if display name is valid
|
||||
if (length > 32) 400 - "Display name too long (max 32 letters)"
|
||||
}
|
||||
|
||||
fun checkProfileLocation(profileLocation: Str) = profileLocation.apply {
|
||||
// Check if profile location is valid
|
||||
if (length > 64) 400 - "Profile location too long (max 64 letters)"
|
||||
}
|
||||
|
||||
fun checkProfileBio(profileBio: Str) = profileBio.apply {
|
||||
// Check if profile bio is valid
|
||||
if (length > 255) 400 - "Profile bio too long (max 255 letters)"
|
||||
}
|
||||
|
||||
fun checkOptOutOfLeaderboard(optOutOfLeaderboard: Str) = optOutOfLeaderboard.toBoolean()
|
||||
}
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.net.UserRegistrar.Companion.cardExtIdEnd
|
||||
import icu.samnyan.aqua.net.UserRegistrar.Companion.cardExtIdStart
|
||||
import icu.samnyan.aqua.net.components.JWT
|
||||
import icu.samnyan.aqua.sega.allnet.AllNetProps
|
||||
import icu.samnyan.aqua.sega.allnet.KeyChipRepo
|
||||
import icu.samnyan.aqua.sega.allnet.KeychipSession
|
||||
import icu.samnyan.aqua.sega.general.GameMusicPopularity
|
||||
import icu.samnyan.aqua.sega.general.dao.CardRepository
|
||||
import icu.samnyan.aqua.sega.general.model.Card
|
||||
import icu.samnyan.aqua.sega.general.service.CardService
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.KMutableProperty
|
||||
import kotlin.reflect.full.functions
|
||||
|
||||
// Entity AquaNetUser and SettingField are now in :shared
|
||||
|
||||
interface AquaNetUserRepo : JpaRepository<AquaNetUser, Long> {
|
||||
fun findByAuId(auId: Long): AquaNetUser?
|
||||
fun findByEmailIgnoreCase(email: String): AquaNetUser?
|
||||
fun findByUsernameIgnoreCase(username: String): AquaNetUser?
|
||||
fun findByKeychip(keychip: String): AquaNetUser?
|
||||
fun findByGhostCardExtId(extId: Long): AquaNetUser?
|
||||
}
|
||||
|
||||
/**
|
||||
* User services (formerly inside AquaNetUser.kt in the monolithic setup).
|
||||
*/
|
||||
@Service
|
||||
class AquaUserServices(
|
||||
val userRepo: AquaNetUserRepo,
|
||||
val cardRepo: CardRepository,
|
||||
val hasher: PasswordEncoder,
|
||||
val keyChipRepo: KeyChipRepo,
|
||||
val allNetProps: AllNetProps,
|
||||
val jwt: JWT,
|
||||
val em: EntityManager,
|
||||
val pop: GameMusicPopularity,
|
||||
val cardService: CardService,
|
||||
val sessionRepo: SessionTokenRepo,
|
||||
) {
|
||||
companion object {
|
||||
val SETTING_FIELDS = AquaUserServices::class.functions
|
||||
.filter { it.name.startsWith("check") }
|
||||
.map {
|
||||
val name = it.name.removePrefix("check").replaceFirstChar { c -> c.lowercase() }
|
||||
val prop = AquaNetUser::class.members.find { m -> m.name == name } as KMutableProperty<*>
|
||||
SettingField(name, it, prop.setter)
|
||||
}
|
||||
}
|
||||
|
||||
fun create(username: Str, email: Str, password: Str, country: Str, emailConfirmed: Boolean = false): AquaNetUser {
|
||||
// Create user
|
||||
val u = AquaNetUser(
|
||||
username = checkUsername(username),
|
||||
email = validateEmail(email),
|
||||
pwHash = checkPwHash(password),
|
||||
regTime = millis(), lastLogin = millis(), country = country,
|
||||
emailConfirmed = emailConfirmed
|
||||
)
|
||||
|
||||
// Create a ghost card
|
||||
val card = Card().apply {
|
||||
extId = cardService.randExtID(cardExtIdStart, cardExtIdEnd)
|
||||
luid = extId.toString()
|
||||
registerTime = LocalDateTime.now()
|
||||
accessTime = registerTime
|
||||
aquaUser = u
|
||||
isGhost = true
|
||||
}
|
||||
u.ghostCard = card
|
||||
|
||||
// Save the user
|
||||
userRepo.save(u)
|
||||
cardRepo.save(card)
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
fun update(user: AquaNetUser, key: Str, value: Str) {
|
||||
// Check if the key is a settable field
|
||||
val field = SETTING_FIELDS.find { it.name == key } ?: (400 - "Invalid setting")
|
||||
// Set the validated field
|
||||
field.setter.call(user, field.checker.call(this, value))
|
||||
}
|
||||
|
||||
fun clearAllSessions(user: AquaNetUser) = sessionRepo.deleteAll(sessionRepo.findByAquaNetUserAuId(user.auId))
|
||||
|
||||
suspend fun <T> byName(username: Str, callback: suspend (AquaNetUser) -> T) =
|
||||
async { userRepo.findByUsernameIgnoreCase(username) }?.let { callback(it) } ?: (404 - "User not found")
|
||||
|
||||
suspend fun cardByName(username: Str) =
|
||||
if (username.startsWith("user")) username.substring(4).toLongOrNull()
|
||||
?.let { cardRepo.findById(it)() } ?: (404 - "Card not found")
|
||||
else byName(username) { it.ghostCard }
|
||||
|
||||
suspend fun <T> cardByName(username: Str, callback: suspend (Card) -> T) = callback(cardByName(username))
|
||||
|
||||
fun validKeychip(keychipId: Str): Bool {
|
||||
if (!allNetProps.checkKeychip) return true
|
||||
if (keychipId.isBlank()) return false
|
||||
if (userRepo.findByKeychip(keychipId) != null || keyChipRepo.existsByKeychipId(keychipId)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun checkUsername(username: Str) = username.apply {
|
||||
// Check if username is valid
|
||||
if (length < 2) 400 - "Username must be at least 2 letters"
|
||||
if (length > 32) 400 - "Username too long (max 32 letters)"
|
||||
if (contains(" ")) 400 - "Username cannot contain spaces"
|
||||
|
||||
// card{id} is a reserved format
|
||||
if (startsWith("user") && substring(4).toLongOrNull() != null)
|
||||
400 - "Username cannot be 'user' + a number. This format is reserved for user IDs."
|
||||
|
||||
// Check if username is within A-Za-z0-9_-~.
|
||||
find { !it.isLetterOrDigit() && it != '_' && it != '-' && it != '~' && it != '.' }?.let {
|
||||
400 - "Username cannot contain `$it`. Please only use letters (A-Z), numbers (0-9), and `_-~.` characters. You can set a display name later."
|
||||
}
|
||||
|
||||
// Check if user with the same username exists
|
||||
if (userRepo.findByUsernameIgnoreCase(this) != null)
|
||||
400 - "User with username `$this` already exists"
|
||||
}
|
||||
|
||||
fun validateEmail(email: Str) = email.apply {
|
||||
// Check if email is valid
|
||||
if (!isValidEmail()) 400 - "Invalid email"
|
||||
|
||||
// Check if user with the same email exists
|
||||
if (userRepo.findByEmailIgnoreCase(email) != null)
|
||||
400 - "User with email `$email` already exists"
|
||||
}
|
||||
|
||||
fun checkPwHash(password: Str) = password.run {
|
||||
// Validate password
|
||||
if (length < 8) 400 - "Password must be at least 8 characters"
|
||||
|
||||
hasher.encode(this)
|
||||
}
|
||||
|
||||
fun checkDisplayName(displayName: Str) = displayName.apply {
|
||||
// Check if display name is valid
|
||||
if (length > 32) 400 - "Display name too long (max 32 letters)"
|
||||
}
|
||||
|
||||
fun checkProfileLocation(profileLocation: Str) = profileLocation.apply {
|
||||
// Check if profile location is valid
|
||||
if (length > 64) 400 - "Profile location too long (max 64 letters)"
|
||||
}
|
||||
|
||||
fun checkProfileBio(profileBio: Str) = profileBio.apply {
|
||||
// Check if profile bio is valid
|
||||
if (length > 255) 400 - "Profile bio too long (max 255 letters)"
|
||||
}
|
||||
|
||||
fun checkOptOutOfLeaderboard(optOutOfLeaderboard: Str) = optOutOfLeaderboard.toBoolean()
|
||||
}
|
||||
|
||||
@@ -1,146 +1,29 @@
|
||||
package icu.samnyan.aqua.net.games
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import ext.JACKSON
|
||||
import ext.JavaSerializable
|
||||
import icu.samnyan.aqua.sega.general.model.Card
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.MappedSuperclass
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.NoRepositoryBean
|
||||
import java.util.*
|
||||
|
||||
data class TrendOut(val date: String, val rating: Int, val plays: Int)
|
||||
|
||||
data class RankCount(val name: String, val count: Int)
|
||||
|
||||
data class GenericGameSummary(
|
||||
val name: String,
|
||||
|
||||
val aquaUser: Map<String, Any?>?,
|
||||
|
||||
val serverRank: Long,
|
||||
val accuracy: Double,
|
||||
val rating: Int,
|
||||
val ratingHighest: Int,
|
||||
val ranks: List<RankCount>,
|
||||
val detailedRanks: Map<Int, Map<String, Int>>,
|
||||
val maxCombo: Int,
|
||||
val fullCombo: Int,
|
||||
val allPerfect: Int,
|
||||
val totalScore: Long,
|
||||
|
||||
val plays: Int,
|
||||
val totalPlayTime: Long,
|
||||
val joined: String,
|
||||
val lastSeen: String,
|
||||
val lastVersion: String,
|
||||
val lastPlayedHost: String? = null,
|
||||
|
||||
val ratingComposition: Map<String, Any>,
|
||||
|
||||
val recent: List<IGenericGamePlaylog>,
|
||||
|
||||
val rival: Boolean?,
|
||||
val favorites: List<Int>?
|
||||
)
|
||||
|
||||
data class GenericRankingPlayer(
|
||||
var rank: Int,
|
||||
val name: String,
|
||||
val username: String?,
|
||||
val accuracy: Double,
|
||||
val rating: Int,
|
||||
val allPerfect: Int,
|
||||
val fullCombo: Int,
|
||||
val lastSeen: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GenericMusicMeta(
|
||||
val name: String?,
|
||||
val ver: String,
|
||||
val notes: List<GenericNoteMeta>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GenericNoteMeta(
|
||||
val lv: Double?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GenericItemMeta(
|
||||
val name: String? = null,
|
||||
val disable: Boolean? = null,
|
||||
val ver: String? = null
|
||||
)
|
||||
|
||||
// Here are some interfaces to generalize across multiple games
|
||||
interface IUserData {
|
||||
val id: Long
|
||||
var userName: String
|
||||
val playerRating: Int
|
||||
val highestRating: Int
|
||||
val firstPlayDate: Any
|
||||
val lastPlayDate: Any
|
||||
val lastRomVersion: String
|
||||
val totalScore: Long
|
||||
var card: Card?
|
||||
val lastClientId: String?
|
||||
}
|
||||
|
||||
interface IGenericGamePlaylog {
|
||||
val user: IUserData
|
||||
val musicId: Int
|
||||
val level: Int
|
||||
val userPlayDate: Any
|
||||
val achievement: Int
|
||||
val maxCombo: Int
|
||||
val isFullCombo: Boolean
|
||||
val beforeRating: Int
|
||||
val afterRating: Int
|
||||
val isAllPerfect: Boolean
|
||||
}
|
||||
|
||||
interface IGenericUserMusic {
|
||||
val musicId: Int
|
||||
}
|
||||
|
||||
@MappedSuperclass
|
||||
open class BaseEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@JsonIgnore
|
||||
open var id: Long = 0
|
||||
) : JavaSerializable {
|
||||
override fun toString() = JACKSON.writeValueAsString(this)
|
||||
}
|
||||
|
||||
@NoRepositoryBean
|
||||
interface GenericUserDataRepo<T : IUserData> : JpaRepository<T, Long> {
|
||||
fun findByCard(card: Card): T?
|
||||
fun findByCard_ExtId(extId: Long): T?
|
||||
|
||||
@Query("select e from #{#entityName} e where e.card.rankingBanned = false")
|
||||
fun findAllNonBanned(): List<T>
|
||||
}
|
||||
|
||||
@NoRepositoryBean
|
||||
interface GenericPlaylogRepo<T: IGenericGamePlaylog> : JpaRepository<T, Long> {
|
||||
fun findByUserCardExtId(extId: Long): List<T>
|
||||
fun findByUserCardExtId(extId: Long, page: Pageable): Page<T>
|
||||
}
|
||||
|
||||
@NoRepositoryBean
|
||||
interface GenericUserMusicRepo<T: IGenericUserMusic> : JpaRepository<T, Long> {
|
||||
fun findByUserCardExtId(extId: Long): List<T>
|
||||
fun findByUser_Card_ExtIdAndMusicIdIn(userId: Long, musicId: List<Int>): List<T>
|
||||
}
|
||||
|
||||
data class ImportResult(val errors: List<String>, val warnings: List<String>, val json: String)
|
||||
package icu.samnyan.aqua.net.games
|
||||
|
||||
import icu.samnyan.aqua.sega.general.model.Card
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.NoRepositoryBean
|
||||
|
||||
@NoRepositoryBean
|
||||
interface GenericUserDataRepo<T : IUserData> : JpaRepository<T, Long> {
|
||||
fun findByCard(card: Card): T?
|
||||
fun findByCard_ExtId(extId: Long): T?
|
||||
|
||||
@Query("select e from #{'#'}{#entityName} e where e.card.rankingBanned = false")
|
||||
fun findAllNonBanned(): List<T>
|
||||
}
|
||||
|
||||
@NoRepositoryBean
|
||||
interface GenericPlaylogRepo<T: IGenericGamePlaylog> : JpaRepository<T, Long> {
|
||||
fun findByUserCardExtId(extId: Long): List<T>
|
||||
fun findByUserCardExtId(extId: Long, page: Pageable): Page<T>
|
||||
}
|
||||
|
||||
@NoRepositoryBean
|
||||
interface GenericUserMusicRepo<T: IGenericUserMusic> : JpaRepository<T, Long> {
|
||||
fun findByUserCardExtId(extId: Long): List<T>
|
||||
fun findByUser_Card_ExtIdAndMusicIdIn(userId: Long, musicId: List<Int>): List<T>
|
||||
}
|
||||
|
||||
@@ -1,32 +1,10 @@
|
||||
package icu.samnyan.aqua.sega.allnet
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* This is the old method of securing requests - a keychip whitelist,
|
||||
* it's kept here only for backwards compatibility.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "allnet_keychips")
|
||||
class Keychip(
|
||||
@Id
|
||||
val id: Long = 0,
|
||||
|
||||
@Column(unique = true, nullable = false)
|
||||
val keychipId: String = "",
|
||||
) : Serializable {
|
||||
companion object {
|
||||
const val serialVersionUID = 1L
|
||||
}
|
||||
@Repository
|
||||
interface KeyChipRepo : JpaRepository<Keychip, Long> {
|
||||
fun findByKeychipId(keychipId: String): Keychip?
|
||||
fun existsByKeychipId(keychipId: String): Boolean
|
||||
}
|
||||
|
||||
@Repository("KeyChipRepository")
|
||||
interface KeyChipRepo : JpaRepository<Keychip?, Long?> {
|
||||
fun existsByKeychipId(keychipId: String?): Boolean
|
||||
}
|
||||
@@ -1,91 +1,60 @@
|
||||
package icu.samnyan.aqua.sega.allnet
|
||||
|
||||
import ext.async
|
||||
import icu.samnyan.aqua.net.db.AquaNetUser
|
||||
import jakarta.persistence.*
|
||||
import jakarta.transaction.Transactional
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.springframework.stereotype.Service
|
||||
import java.security.SecureRandom
|
||||
|
||||
/**
|
||||
* This is a one-to-many mapping of keychip to session token.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "allnet_keychip_sessions", indexes = [
|
||||
Index(name = "idx_last_use", columnList = "lastUse")
|
||||
])
|
||||
class KeychipSession(
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "au_id")
|
||||
var user: AquaNetUser? = null,
|
||||
|
||||
@Column(length = 4)
|
||||
val gameId: String,
|
||||
|
||||
@Id
|
||||
@Column(length = 32)
|
||||
val token: String = genUrlSafeToken(32),
|
||||
|
||||
@Column(nullable = false)
|
||||
var lastUse: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
|
||||
val urlSafeChars = ('a'..'z') + ('A'..'Z') + ('0'..'9') + listOf('-', '_', '.', '~')
|
||||
|
||||
fun genUrlSafeToken(length: Int): String {
|
||||
val random = SecureRandom()
|
||||
return (1..length)
|
||||
.map { urlSafeChars[random.nextInt(urlSafeChars.size)] }
|
||||
.joinToString("")
|
||||
}
|
||||
|
||||
@Repository("KeychipSessionRepo")
|
||||
interface KeychipSessionRepo : JpaRepository<KeychipSession, String> {
|
||||
fun findByToken(token: String): KeychipSession?
|
||||
|
||||
@Transactional
|
||||
fun deleteAllByLastUseBefore(expire: Long)
|
||||
}
|
||||
|
||||
@Service
|
||||
class KeychipSessionService(
|
||||
val repo: KeychipSessionRepo,
|
||||
val props: AllNetProps
|
||||
) {
|
||||
val logger = LoggerFactory.getLogger(KeychipSessionService::class.java)
|
||||
|
||||
/**
|
||||
* Delete sessions that are older than the expire time.
|
||||
*/
|
||||
@Scheduled(fixedDelayString = "\${allnet.server.keychip-ses-clean-interval}")
|
||||
suspend fun cleanup() = async {
|
||||
logger.info("!!! Keychip session cleanup !!!")
|
||||
val expire = System.currentTimeMillis() - props.keychipSesExpire
|
||||
repo.deleteAllByLastUseBefore(expire)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session.
|
||||
*/
|
||||
fun new(user: AquaNetUser?, gameId: String): KeychipSession {
|
||||
val session = KeychipSession(user = user, gameId = gameId)
|
||||
return repo.save(session)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a session. If found, renew the last use time.
|
||||
*/
|
||||
fun find(token: String) = repo.findByToken(token)?.apply {
|
||||
lastUse = System.currentTimeMillis()
|
||||
try {
|
||||
repo.save(this)
|
||||
} catch (_: Exception) {
|
||||
logger.error("Failed to update last use time for session $token")
|
||||
}
|
||||
}
|
||||
}
|
||||
package icu.samnyan.aqua.sega.allnet
|
||||
|
||||
import ext.async
|
||||
import icu.samnyan.aqua.net.db.AquaNetUser
|
||||
import jakarta.transaction.Transactional
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
/**
|
||||
* Repository and Service for KeychipSession (Entity is located in :shared module).
|
||||
*/
|
||||
|
||||
@Repository("KeychipSessionRepo")
|
||||
interface KeychipSessionRepo : JpaRepository<KeychipSession, String> {
|
||||
fun findByToken(token: String): KeychipSession?
|
||||
|
||||
@Transactional
|
||||
fun deleteAllByLastUseBefore(expire: Long)
|
||||
}
|
||||
|
||||
@Service
|
||||
class KeychipSessionService(
|
||||
val repo: KeychipSessionRepo,
|
||||
val props: AllNetProps
|
||||
) {
|
||||
val logger = LoggerFactory.getLogger(KeychipSessionService::class.java)
|
||||
|
||||
/**
|
||||
* Delete sessions that are older than the expire time.
|
||||
*/
|
||||
@Scheduled(fixedDelayString = "\${allnet.server.keychip-ses-clean-interval}")
|
||||
suspend fun cleanup() = async {
|
||||
logger.info("!!! Keychip session cleanup !!!")
|
||||
val expire = System.currentTimeMillis() - props.keychipSesExpire
|
||||
repo.deleteAllByLastUseBefore(expire)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session.
|
||||
*/
|
||||
fun new(user: AquaNetUser?, gameId: String): KeychipSession {
|
||||
val session = KeychipSession(user = user, gameId = gameId)
|
||||
return repo.save(session)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a session. If found, renew the last use time.
|
||||
*/
|
||||
fun find(token: String) = repo.findByToken(token)?.apply {
|
||||
lastUse = System.currentTimeMillis()
|
||||
try {
|
||||
repo.save(this)
|
||||
} catch (_: Exception) {
|
||||
logger.error("Failed to update last use time for session $token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,12 @@
|
||||
package icu.samnyan.aqua.sega.diva.util
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||
import icu.samnyan.aqua.sega.diva.PlayerPvRecordRepository
|
||||
import icu.samnyan.aqua.sega.diva.model.common.Edition
|
||||
import icu.samnyan.aqua.sega.diva.model.common.LevelInfo
|
||||
import icu.samnyan.aqua.sega.diva.model.db.userdata.PlayerProfile
|
||||
import org.springframework.stereotype.Component
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
object DivaStringUtils {
|
||||
@JvmStatic
|
||||
fun getDummyString(content: String, length: Int) = "$content,".repeat(length).removeSuffix(",")
|
||||
}
|
||||
|
||||
object DivaTime {
|
||||
val now get() = getString(LocalDateTime.now())
|
||||
|
||||
@JvmStatic
|
||||
fun getString(time: LocalDateTime) = URIEncoder.encode(format(time))
|
||||
|
||||
@JvmStatic
|
||||
fun format(time: LocalDateTime) = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0").format(time)
|
||||
}
|
||||
|
||||
class DivaDateTimeSerializer(t: Class<LocalDateTime>? = null) : StdSerializer<LocalDateTime>(t) {
|
||||
override fun serialize(value: LocalDateTime, gen: JsonGenerator, provider: SerializerProvider) {
|
||||
gen.writeString(DivaTime.getString(value))
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
class DivaCalculator(private val playerPvRecordRepository: PlayerPvRecordRepository) {
|
||||
fun getLevelInfo(profile: PlayerProfile): LevelInfo {
|
||||
@@ -50,8 +22,3 @@ class DivaCalculator(private val playerPvRecordRepository: PlayerPvRecordReposit
|
||||
return LevelInfo(level + 1, exp)
|
||||
}
|
||||
}
|
||||
|
||||
object URIEncoder {
|
||||
@JvmStatic
|
||||
fun encode(str: String) = URLEncoder.encode(str, StandardCharsets.UTF_8).replace("\\+".toRegex(), "%20")
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package icu.samnyan.aqua.sega.general.model
|
||||
|
||||
import ext.Str
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.Instant
|
||||
|
||||
@Entity(name = "SegaCardTimestamp")
|
||||
@Table(name = "sega_card_timestamp")
|
||||
class CardTimestamp(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@Column(nullable = false)
|
||||
var createdAt: Instant = Instant.now(),
|
||||
|
||||
@Column(nullable = false)
|
||||
var updatedAt: Instant = Instant.now(),
|
||||
|
||||
var game: Str,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "card_id")
|
||||
var card: Card? = null,
|
||||
)
|
||||
|
||||
@Repository
|
||||
interface CardTimestampRepo : JpaRepository<CardTimestamp, Long> {
|
||||
fun findByCardIdAndGame(cardId: Long, game: Str): CardTimestamp?
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package icu.samnyan.aqua.sega.general.model
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.util.*
|
||||
|
||||
@Repository
|
||||
interface CardTimestampRepo : JpaRepository<CardTimestamp, Long> {
|
||||
fun findByCardIdAndGameId(cardId: Long, gameId: String): Optional<CardTimestamp>
|
||||
}
|
||||
@@ -104,12 +104,19 @@ class CardService(val cardRepo: CardRepository, val cardTimestampRepo: CardTimes
|
||||
return eid
|
||||
}
|
||||
|
||||
fun getCardTimestamp(card: Card, game: Str, now: Instant = Instant.now()) =
|
||||
cardTimestampRepo.findByCardIdAndGame(card.id, game) ?: CardTimestamp(game = game, card = card, createdAt = now, updatedAt = now);
|
||||
fun getCardTimestamp(card: Card, game: Str, now: Instant = Instant.now()): CardTimestamp =
|
||||
cardTimestampRepo.findByCardIdAndGameId(card.id, game).orElseGet {
|
||||
CardTimestamp().apply {
|
||||
this.gameId = game
|
||||
this.cardId = card.id
|
||||
this.createdAt = now.toEpochMilli()
|
||||
this.updatedAt = now.toEpochMilli()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCardTimestamp(card: Card, game: Str, now: Instant = Instant.now(), resetCreatedAt: Bool = false) {
|
||||
cardTimestampRepo.save(getCardTimestamp(card, game, now).apply { updatedAt = now }
|
||||
.apply { if (resetCreatedAt) createdAt = now });
|
||||
cardTimestampRepo.save(getCardTimestamp(card, game, now).apply { updatedAt = now.toEpochMilli() }
|
||||
.apply { if (resetCreatedAt) createdAt = now.toEpochMilli() });
|
||||
fedy.onDataUpdated(card.extId, game, resetCreatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.jpa")
|
||||
kotlin("plugin.serialization")
|
||||
kotlin("kapt")
|
||||
id("org.hibernate.orm") version "6.4.4.Final"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -11,9 +13,30 @@ dependencies {
|
||||
api("com.fasterxml.jackson.core:jackson-databind:2.17.0")
|
||||
api("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0")
|
||||
|
||||
// QueryDSL
|
||||
implementation("io.github.openfeign.querydsl:querydsl-jpa:6.10.1")
|
||||
kapt("io.github.openfeign.querydsl:querydsl-apt:6.10.1:jpa")
|
||||
|
||||
// Core libraries
|
||||
api("org.slf4j:slf4j-api:2.0.12")
|
||||
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
|
||||
api("org.jetbrains.kotlin:kotlin-reflect:2.1.10")
|
||||
|
||||
// Hibernate enhancement requires the core library in shared as well
|
||||
implementation("org.hibernate.orm:hibernate-core:6.4.4.Final")
|
||||
}
|
||||
|
||||
hibernate {
|
||||
enhancement {
|
||||
enableLazyInitialization = true
|
||||
enableAssociationManagement = false
|
||||
enableExtendedEnhancement = false
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDir("${layout.buildDirectory.get()}/generated/source/kapt/main")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package icu.samnyan.aqua.net
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.io.Serializable
|
||||
|
||||
@Entity
|
||||
@Table(name = "aqua_net_safety")
|
||||
class Safety(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@Column(nullable = false, unique = true, length = 64)
|
||||
var safetyId: String = "",
|
||||
|
||||
@Column(nullable = false)
|
||||
var status: Int = 0
|
||||
) : Serializable
|
||||
@@ -0,0 +1,23 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
@Table(name = "aqua_net_email_confirmation")
|
||||
class EmailConfirmation(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "auId")
|
||||
var aquaNetUser: AquaNetUser = AquaNetUser(),
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
var token: String = "",
|
||||
|
||||
@Column(nullable = false)
|
||||
var createdAt: Instant = Instant.now()
|
||||
) : Serializable
|
||||
@@ -0,0 +1,23 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
@Table(name = "aqua_net_email_reset_password")
|
||||
class ResetPassword(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "auId")
|
||||
var aquaNetUser: AquaNetUser = AquaNetUser(),
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
var token: String = "",
|
||||
|
||||
@Column(nullable = false)
|
||||
var createdAt: Instant = Instant.now()
|
||||
) : Serializable
|
||||
@@ -0,0 +1,75 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import ext.SettingField
|
||||
import jakarta.persistence.*
|
||||
import java.io.Serializable
|
||||
|
||||
@Entity
|
||||
@Table(name = "aqua_net_game_options")
|
||||
class AquaGameOptions(
|
||||
@Id @JsonIgnore
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_music")
|
||||
var mai2UnlockMusic: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_chara")
|
||||
var mai2UnlockChara: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_chara_max_level")
|
||||
var mai2UnlockCharaMaxLevel: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_partners")
|
||||
var mai2UnlockPartners: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_collectables")
|
||||
var mai2UnlockCollectables: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_tickets")
|
||||
var mai2UnlockTickets: Boolean = false,
|
||||
|
||||
@SettingField("wacca")
|
||||
var waccaUnlockMusic: Boolean = false,
|
||||
@SettingField("wacca")
|
||||
var waccaUnlockPlates: Boolean = false,
|
||||
@SettingField("wacca")
|
||||
var waccaUnlockCollectables: Boolean = false,
|
||||
@SettingField("wacca")
|
||||
var waccaUnlockTickets: Boolean = false,
|
||||
@SettingField("wacca")
|
||||
var waccaInfiniteWp: Boolean = false,
|
||||
@SettingField("wacca")
|
||||
var waccaAlwaysVip: Boolean = false,
|
||||
|
||||
@SettingField("chu3")
|
||||
var chusanTeamName: String = "",
|
||||
|
||||
@SettingField("chu3")
|
||||
var chusanInfinitePenguins: Boolean = false,
|
||||
|
||||
@SettingField("chu3-matching")
|
||||
var chusanMatchingServer: String = "",
|
||||
|
||||
@SettingField("chu3-matching")
|
||||
var chusanMatchingReflector: String = "",
|
||||
|
||||
@SettingField("chu3-linked-verse")
|
||||
var chusanLvUnlockAll: Boolean = false,
|
||||
@SettingField("chu3-linked-verse")
|
||||
var chusanLvDifficulty: Int = 1,
|
||||
|
||||
@SettingField("chu3-matching-chat")
|
||||
var chusanSymbolChat1: Int? = null,
|
||||
@SettingField("chu3-matching-chat")
|
||||
var chusanSymbolChat2: Int? = null,
|
||||
@SettingField("chu3-matching-chat")
|
||||
var chusanSymbolChat3: Int? = null,
|
||||
@SettingField("chu3-matching-chat")
|
||||
var chusanSymbolChat4: Int? = null,
|
||||
|
||||
@SettingField("mai2")
|
||||
var enableMusicRank: Boolean = true,
|
||||
|
||||
@SettingField("ongeki")
|
||||
var ongekiInfiniteKaika: Boolean = false,
|
||||
|
||||
@SettingField("profile")
|
||||
var countryOverride: String = "",
|
||||
) : Serializable
|
||||
@@ -0,0 +1,24 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
fun getTokenExpiry() = Instant.now().plusSeconds(7 * 86400)
|
||||
|
||||
@Entity
|
||||
@Table(name = "aqua_net_session")
|
||||
class SessionToken(
|
||||
@Id
|
||||
@Column(nullable = false)
|
||||
var token: String = UUID.randomUUID().toString(),
|
||||
|
||||
// Token creation time
|
||||
@Column(nullable = false)
|
||||
var expiry: Instant = getTokenExpiry(),
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "auId", referencedColumnName = "auId")
|
||||
var aquaNetUser: AquaNetUser = AquaNetUser()
|
||||
) : Serializable
|
||||
@@ -0,0 +1,94 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.sega.allnet.KeychipSession
|
||||
import icu.samnyan.aqua.sega.general.model.Card
|
||||
import jakarta.persistence.*
|
||||
import java.io.Serializable
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.KMutableProperty
|
||||
|
||||
@Entity
|
||||
class AquaNetUser(
|
||||
@JsonIgnore
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var auId: Long = 0,
|
||||
|
||||
@Column(nullable = false, unique = true, length = 32)
|
||||
var username: String = "",
|
||||
|
||||
// Login credentials
|
||||
@Column(nullable = false, unique = true)
|
||||
var email: String = "",
|
||||
|
||||
@JsonIgnore
|
||||
@Column(nullable = false)
|
||||
var pwHash: String = "",
|
||||
|
||||
@Column(nullable = true, length = 32)
|
||||
var displayName: String = "",
|
||||
|
||||
// Country code at most 3 characters
|
||||
@Column(length = 3)
|
||||
var country: String = "",
|
||||
|
||||
// Region code at most 2 characters
|
||||
@Column(length = 2)
|
||||
var region: String = "",
|
||||
|
||||
// Last login time
|
||||
var lastLogin: Long = 0L,
|
||||
|
||||
// Registration time
|
||||
var regTime: Long = 0L,
|
||||
|
||||
// Profile fields
|
||||
var profileLocation: String? = "",
|
||||
var profileBio: String? = "",
|
||||
var profilePicture: String? = "",
|
||||
var optOutOfLeaderboard: Boolean = false,
|
||||
|
||||
// Email confirmation
|
||||
var emailConfirmed: Boolean = false,
|
||||
|
||||
@OneToOne(cascade = [CascadeType.ALL])
|
||||
@JoinColumn(name = "ghostCard", unique = true, nullable = false)
|
||||
var ghostCard: Card = Card(),
|
||||
|
||||
// One user can have multiple cards
|
||||
@OneToMany(mappedBy = "aquaUser", cascade = [CascadeType.ALL])
|
||||
var cards: MutableList<Card> = mutableListOf(),
|
||||
|
||||
// Each user can have one keychip (if the user owns a cabinet)
|
||||
@JsonIgnore
|
||||
@Column(nullable = true, length = 32, unique = true)
|
||||
var keychip: Str? = null,
|
||||
|
||||
// Each user's keychip can have multiple sessions
|
||||
@JsonIgnore
|
||||
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL])
|
||||
var keychipSessions: MutableList<KeychipSession> = mutableListOf(),
|
||||
|
||||
@OneToOne(cascade = [CascadeType.ALL])
|
||||
@JoinColumn(name = "gameOptions", unique = true, nullable = true)
|
||||
var gameOptions: AquaGameOptions? = null,
|
||||
) : Serializable {
|
||||
val computedName get() = displayName.ifEmpty { username }
|
||||
|
||||
val publicFields get() = mapOf(
|
||||
"username" to username,
|
||||
"displayName" to displayName,
|
||||
"country" to country,
|
||||
"regTime" to regTime,
|
||||
"profileLocation" to profileLocation,
|
||||
"profileBio" to profileBio,
|
||||
"profilePicture" to profilePicture,
|
||||
)
|
||||
}
|
||||
|
||||
data class SettingField(
|
||||
val name: Str,
|
||||
val checker: KFunction<*>,
|
||||
val setter: KMutableProperty.Setter<*>,
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
package icu.samnyan.aqua.net.games
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import ext.JACKSON
|
||||
import ext.JavaSerializable
|
||||
import icu.samnyan.aqua.sega.general.model.Card
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.MappedSuperclass
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
data class TrendOut(val date: String, val rating: Int, val plays: Int)
|
||||
|
||||
data class RankCount(val name: String, val count: Int)
|
||||
|
||||
data class GenericGameSummary(
|
||||
val name: String,
|
||||
val aquaUser: Map<String, Any?>?,
|
||||
val serverRank: Long,
|
||||
val accuracy: Double,
|
||||
val rating: Int,
|
||||
val ratingHighest: Int,
|
||||
val ranks: List<RankCount>,
|
||||
val detailedRanks: Map<Int, Map<String, Int>>,
|
||||
val maxCombo: Int,
|
||||
val fullCombo: Int,
|
||||
val allPerfect: Int,
|
||||
val totalScore: Long,
|
||||
val plays: Int,
|
||||
val totalPlayTime: Long,
|
||||
val joined: String,
|
||||
val lastSeen: String,
|
||||
val lastVersion: String,
|
||||
val lastPlayedHost: String? = null,
|
||||
val ratingComposition: Map<String, Any>,
|
||||
val recent: List<IGenericGamePlaylog>,
|
||||
val rival: Boolean?,
|
||||
val favorites: List<Int>?
|
||||
)
|
||||
|
||||
data class GenericRankingPlayer(
|
||||
var rank: Int,
|
||||
val name: String,
|
||||
val username: String?,
|
||||
val accuracy: Double,
|
||||
val rating: Int,
|
||||
val allPerfect: Int,
|
||||
val fullCombo: Int,
|
||||
val lastSeen: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GenericMusicMeta(
|
||||
val name: String?,
|
||||
val ver: String,
|
||||
val notes: List<GenericNoteMeta>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GenericNoteMeta(
|
||||
val lv: Double?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GenericItemMeta(
|
||||
val name: String? = null,
|
||||
val disable: Boolean? = null,
|
||||
val ver: String? = null
|
||||
)
|
||||
|
||||
// Here are some interfaces to generalize across multiple games
|
||||
interface IUserData {
|
||||
val id: Long
|
||||
var userName: String
|
||||
val playerRating: Int
|
||||
val highestRating: Int
|
||||
val firstPlayDate: Any
|
||||
val lastPlayDate: Any
|
||||
val lastRomVersion: String
|
||||
val totalScore: Long
|
||||
var card: Card?
|
||||
val lastClientId: String?
|
||||
}
|
||||
|
||||
interface IGenericGamePlaylog {
|
||||
val user: IUserData
|
||||
val musicId: Int
|
||||
val level: Int
|
||||
val userPlayDate: Any
|
||||
val achievement: Int
|
||||
val maxCombo: Int
|
||||
val isFullCombo: Boolean
|
||||
val beforeRating: Int
|
||||
val afterRating: Int
|
||||
val isAllPerfect: Boolean
|
||||
}
|
||||
|
||||
interface IGenericUserMusic {
|
||||
val musicId: Int
|
||||
}
|
||||
|
||||
interface IUserEntity<UserModel: IUserData> {
|
||||
var id: Long
|
||||
var user: UserModel
|
||||
}
|
||||
|
||||
interface IExportClass<UserModel: IUserData> {
|
||||
var gameId: String
|
||||
var userData: UserModel
|
||||
}
|
||||
|
||||
@MappedSuperclass
|
||||
open class BaseEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@JsonIgnore
|
||||
open var id: Long = 0
|
||||
) : JavaSerializable {
|
||||
override fun toString() = JACKSON.writeValueAsString(this)
|
||||
}
|
||||
|
||||
data class ImportResult(val errors: List<String>, val warnings: List<String>, val json: String)
|
||||
@@ -0,0 +1,20 @@
|
||||
package icu.samnyan.aqua.sega.allnet
|
||||
|
||||
import icu.samnyan.aqua.net.db.AquaNetUser
|
||||
import jakarta.persistence.*
|
||||
import java.io.Serializable
|
||||
|
||||
@Entity
|
||||
@Table(name = "allnet_keychips")
|
||||
class Keychip(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@Column(nullable = false, unique = true, length = 32)
|
||||
var keychipId: String = "",
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "auId")
|
||||
var user: AquaNetUser? = null
|
||||
) : Serializable
|
||||
@@ -0,0 +1,34 @@
|
||||
package icu.samnyan.aqua.sega.allnet
|
||||
|
||||
import icu.samnyan.aqua.net.db.AquaNetUser
|
||||
import jakarta.persistence.*
|
||||
import java.security.SecureRandom
|
||||
|
||||
@Entity
|
||||
@Table(name = "allnet_keychip_sessions", indexes = [
|
||||
Index(name = "idx_last_use", columnList = "lastUse")
|
||||
])
|
||||
class KeychipSession(
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "au_id")
|
||||
var user: AquaNetUser? = null,
|
||||
|
||||
@Column(length = 4)
|
||||
val gameId: String,
|
||||
|
||||
@Id
|
||||
@Column(length = 32)
|
||||
val token: String = genUrlSafeToken(32),
|
||||
|
||||
@Column(nullable = false)
|
||||
var lastUse: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val urlSafeChars = ('a'..'z') + ('A'..'Z') + ('0'..'9') + listOf('-', '_', '.', '~')
|
||||
|
||||
fun genUrlSafeToken(length: Int): String {
|
||||
val random = SecureRandom()
|
||||
return (1..length)
|
||||
.map { urlSafeChars[random.nextInt(urlSafeChars.size)] }
|
||||
.joinToString("")
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@ import icu.samnyan.aqua.sega.chusan.model.request.UserEmoney
|
||||
import icu.samnyan.aqua.sega.general.model.Card
|
||||
import icu.samnyan.aqua.sega.util.AccessCodeSerializer
|
||||
import jakarta.persistence.*
|
||||
import kotlinx.io.IOException
|
||||
import java.io.IOException
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import jakarta.persistence.ManyToOne
|
||||
import jakarta.persistence.MappedSuperclass
|
||||
|
||||
@MappedSuperclass
|
||||
class Chu3UserEntity : BaseEntity(), IUserEntity<Chu3UserData> {
|
||||
open class Chu3UserEntity : BaseEntity(), IUserEntity<Chu3UserData> {
|
||||
@JsonIgnore
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
+15
-25
@@ -6,7 +6,6 @@ import icu.samnyan.aqua.sega.diva.model.common.ContestNormaType
|
||||
import icu.samnyan.aqua.sega.diva.util.DivaTime
|
||||
import icu.samnyan.aqua.sega.diva.util.URIEncoder
|
||||
import jakarta.persistence.*
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.io.Serializable
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@@ -25,7 +24,7 @@ class Contest : Serializable {
|
||||
@Enumerated(EnumType.STRING)
|
||||
var league: ContestLeague = ContestLeague.BEGINNER
|
||||
var stars = 0
|
||||
var minComplexity = 0 // Only use when Pv difficulty list is not set.
|
||||
var minComplexity = 0
|
||||
var maxComplexity = 0
|
||||
var stages = 0
|
||||
var stageLimit: String = ""
|
||||
@@ -36,45 +35,36 @@ class Contest : Serializable {
|
||||
var sliverBorders = 0
|
||||
var goldBorders = 0
|
||||
|
||||
// Pv List format: "pv_id_start:pv_id_end,pv_id_start:pv_id_end,pv_id_start:pv_id_end" more than 20 group will be ignore, put in -1 for empty end
|
||||
var pvList: String = ""
|
||||
|
||||
// Pv difficulty list format: "pv_difficulty:min_complexity:max_complexity"
|
||||
var pvDiffList: String = ""
|
||||
|
||||
// ContestReward format:
|
||||
// Reward Type: (-1 None, 0 VP, 1 Skin, 2 Callsign, 3 Customize)
|
||||
// Format: "rewardType:reward:string1:string2" string1 and 2 should be urlencoded and must exist. use *** aka %2A%2A%2A as placeholder
|
||||
var bronzeContestReward: String = ""
|
||||
var sliverContestReward: String = ""
|
||||
var goldContestReward: String = ""
|
||||
|
||||
// ContestReward format: "rewardType:reward:string1:string2"
|
||||
var contestEntryReward: String = ""
|
||||
|
||||
constructor()
|
||||
|
||||
val string: String
|
||||
get() {
|
||||
val list = mutableListOf(
|
||||
this.id, // Contest ID
|
||||
DivaTime.format(this.startTime), // Start time
|
||||
DivaTime.format(this.endTime), // End time
|
||||
URIEncoder.encode(this.name), // Contest name
|
||||
URIEncoder.encode(this.description), // Contest description
|
||||
this.league.value, // Contest league
|
||||
this.stars, // Contest starts
|
||||
this.stages, // Contest stage, 1~9
|
||||
this.stageLimit, // list_lump_num ( 0 will be all stage same. > 1 will became stage max defined chart )
|
||||
val list = mutableListOf<Any>(
|
||||
this.id,
|
||||
DivaTime.format(this.startTime),
|
||||
DivaTime.format(this.endTime),
|
||||
URIEncoder.encode(this.name),
|
||||
URIEncoder.encode(this.description),
|
||||
this.league.value,
|
||||
this.stars,
|
||||
this.stages,
|
||||
this.stageLimit,
|
||||
this.normaType.value,
|
||||
this.bronzeBorders,
|
||||
this.sliverBorders,
|
||||
this.goldBorders
|
||||
)
|
||||
for (i in 1..20) {
|
||||
// format is "pv_range_start,pv_range_end,min_complexity,max_complexity,difficulty,unknown"
|
||||
if (pvList.isBlank() || !pvList.contains(":")) {
|
||||
list += listOf(-1, -1)
|
||||
list.addAll(listOf(-1, -1))
|
||||
if (i == 1) {
|
||||
list.add(this.minComplexity)
|
||||
list.add(this.maxComplexity)
|
||||
@@ -82,16 +72,16 @@ class Contest : Serializable {
|
||||
list.add(-2)
|
||||
list.add(-2)
|
||||
}
|
||||
list += listOf(-1, -2, "7fffffffffffffffffffffffffffffff")
|
||||
list.addAll(listOf(-1, -2, "7fffffffffffffffffffffffffffffff"))
|
||||
} else {
|
||||
val groups = pvList.split(',').dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (groups.size < i) {
|
||||
list += listOf(-1, -1, -2, -2, -1, -2, "7fffffffffffffffffffffffffffffff")
|
||||
list.addAll(listOf(-1, -1, -2, -2, -1, -2, "7fffffffffffffffffffffffffffffff"))
|
||||
} else {
|
||||
val ids = groups[i - 1].split(':').dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
list.add(ids[0])
|
||||
list.add(ids[1])
|
||||
if (StringUtils.isBlank(pvDiffList) || !pvDiffList.contains(":")) {
|
||||
if (pvDiffList.isBlank() || !pvDiffList.contains(":")) {
|
||||
list.add(this.minComplexity)
|
||||
list.add(this.maxComplexity)
|
||||
list.add(-1)
|
||||
@@ -0,0 +1,35 @@
|
||||
package icu.samnyan.aqua.sega.diva.util
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
object DivaStringUtils {
|
||||
@JvmStatic
|
||||
fun getDummyString(content: String, length: Int) = ",".repeat(length).removeSuffix(",")
|
||||
}
|
||||
|
||||
object DivaTime {
|
||||
val now get() = getString(LocalDateTime.now())
|
||||
|
||||
@JvmStatic
|
||||
fun getString(time: LocalDateTime) = URIEncoder.encode(format(time))
|
||||
|
||||
@JvmStatic
|
||||
fun format(time: LocalDateTime) = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0").format(time)
|
||||
}
|
||||
|
||||
class DivaDateTimeSerializer(t: Class<LocalDateTime>? = null) : StdSerializer<LocalDateTime>(t) {
|
||||
override fun serialize(value: LocalDateTime, gen: JsonGenerator, provider: SerializerProvider) {
|
||||
gen.writeString(DivaTime.getString(value))
|
||||
}
|
||||
}
|
||||
|
||||
object URIEncoder {
|
||||
@JvmStatic
|
||||
fun encode(str: String) = URLEncoder.encode(str, StandardCharsets.UTF_8).replace("\\+".toRegex(), "%20")
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package icu.samnyan.aqua.sega.general.model
|
||||
|
||||
import icu.samnyan.aqua.net.games.BaseEntity
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "sega_card_timestamp")
|
||||
class CardTimestamp : BaseEntity() {
|
||||
@Column(name = "card_id")
|
||||
var cardId: Long = 0
|
||||
|
||||
@Column(name = "game_id")
|
||||
var gameId: String = ""
|
||||
|
||||
@Column(name = "created_at")
|
||||
var createdAt: Long = 0
|
||||
|
||||
@Column(name = "updated_at")
|
||||
var updatedAt: Long = 0
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@MappedSuperclass
|
||||
class OngekiUserEntity : BaseEntity(), IUserEntity<UserData> {
|
||||
open class OngekiUserEntity : BaseEntity(), IUserEntity<UserData> {
|
||||
@JsonIgnore
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user