[M] Move common models to shared

This commit is contained in:
Azalea
2026-03-29 13:08:12 -04:00
parent 3dbb0095bf
commit 0985f73753
104 changed files with 844 additions and 842 deletions
+1 -1
View File
@@ -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)
}
}
+23
View File
@@ -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("")
}
@@ -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
@@ -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")
@@ -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
}
@@ -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