forked from Cookies_Github_mirror/AquaDX
271 lines
10 KiB
Kotlin
271 lines
10 KiB
Kotlin
package icu.samnyan.aqua.net
|
|
|
|
import ext.*
|
|
import icu.samnyan.aqua.net.components.JWT
|
|
import icu.samnyan.aqua.net.db.AquaUserServices
|
|
import icu.samnyan.aqua.net.games.GenericUserDataRepo
|
|
import icu.samnyan.aqua.net.games.IUserData
|
|
import icu.samnyan.aqua.net.utils.AquaNetProps
|
|
import icu.samnyan.aqua.net.utils.SUCCESS
|
|
import icu.samnyan.aqua.sega.chusan.model.Chu3UserDataRepo
|
|
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 icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
|
|
import icu.samnyan.aqua.sega.ongeki.OgkUserDataRepo
|
|
import icu.samnyan.aqua.sega.wacca.model.db.WcUserRepo
|
|
import jakarta.persistence.EntityManager
|
|
import org.springframework.scheduling.annotation.Scheduled
|
|
import org.springframework.stereotype.Service
|
|
import org.springframework.web.bind.annotation.RestController
|
|
import java.time.LocalDateTime
|
|
import kotlin.jvm.optionals.getOrNull
|
|
import kotlin.random.Random
|
|
|
|
@RestController
|
|
@API("/api/v2/card")
|
|
class CardController(
|
|
val jwt: JWT,
|
|
val us: AquaUserServices,
|
|
val cardService: CardService,
|
|
val cardGameService: CardGameService,
|
|
val cardRepository: CardRepository,
|
|
val props: AquaNetProps,
|
|
val fedy: Fedy
|
|
) {
|
|
companion object {
|
|
val log = logger()
|
|
}
|
|
|
|
@API("/summary")
|
|
@Doc("Get a summary of the card, including the user's name, rating, and last login date.", "Summary of the card")
|
|
suspend fun summary(@RP cardId: Str, @RP token: Str): Any {
|
|
val user = jwt.auth(token)
|
|
// DO NOT CHANGE THIS ERROR MESSAGE - The frontend uses it to detect if the card is not found
|
|
val card = cardService.tryLookup(cardId) ?: (404 - "Card not found")
|
|
|
|
if (card.aquaUser != null && card.aquaUser?.auId != user.auId) (404 - "Card not found")
|
|
|
|
// Lookup data for each game
|
|
return mapOf(
|
|
"card" to card,
|
|
"summary" to cardGameService.getSummary(card),
|
|
)
|
|
}
|
|
|
|
@API("/user-games")
|
|
@Doc("Get the game summary of the user, including the user's name, rating, and last login date.", "Summary of the user")
|
|
suspend fun userGames(@RP username: Str) = us.cardByName(username) { card -> cardGameService.getSummary(card) }
|
|
|
|
/**
|
|
* Bind a card to the user. This action will migrate selected data from the card to the user's ghost card.
|
|
*
|
|
* Non-migrated data will not be lost, but will be inaccessible from the card until the card is unbound.
|
|
*
|
|
* @param token JWT token
|
|
* @param cardId Card ID
|
|
* @param migrate Things to migrate, stored as a comma-separated list of game IDs (e.g. "maimai2,chusan")
|
|
*/
|
|
@API("/link")
|
|
@Doc("Bind a card to the user. This action will migrate selected data from the card to the user's ghost card.", "Success message")
|
|
suspend fun link(@RP token: Str, @RP cardId: Str, @RP migrate: Str) = jwt.auth(token) { u ->
|
|
// Check if the user's card limit is reached
|
|
if (u.cards.size >= props.linkCardLimit) 400 - "Card limit reached"
|
|
|
|
// Try to look up the card
|
|
val card = cardService.tryLookup(cardId)
|
|
|
|
// If no card is found, create a new card
|
|
if (card == null) {
|
|
// Ensure the format of the card ID is correct
|
|
val id = cardService.sanitizeCardId(cardId)
|
|
|
|
// Create a new card
|
|
val newCard = cardService.registerByAccessCode(id, u)
|
|
|
|
log.info("Net /card/link : Created new card $id for user ${u.username}")
|
|
|
|
fedy.onCardLinked(newCard.luid, oldExtId = null, ghostExtId = u.ghostCard.extId, emptyList())
|
|
|
|
return SUCCESS
|
|
}
|
|
|
|
// If card is already bound
|
|
if (card.aquaUser != null) 400 - "Card already bound to another user"
|
|
|
|
// Bind the card
|
|
card.aquaUser = u
|
|
async { cardRepository.save(card) }
|
|
|
|
// Migrate selected data to the new user
|
|
val games = migrate.split(',')
|
|
cardGameService.migrate(card, games)
|
|
|
|
fedy.onCardLinked(card.luid, oldExtId = card.extId, ghostExtId = u.ghostCard.extId,
|
|
games.map { Fedy.getGameName(it) }.filterNotNull())
|
|
|
|
log.info("Net /card/link : Linked card ${card.id} to user ${u.username} and migrated data to ${games.joinToString()}")
|
|
|
|
SUCCESS
|
|
}
|
|
|
|
@API("/unlink")
|
|
@Doc("Unbind a card from the user. No data will be migrated during this action.", "Success message")
|
|
suspend fun unlink(@RP token: Str, @RP cardId: Str) = jwt.auth(token) { u ->
|
|
// Try to look up the card
|
|
val card = cardService.tryLookup(cardId) ?: (404 - "Card not found")
|
|
|
|
// If the card is not bound to the user
|
|
if (card.aquaUser != u) 400 - "Card not linked to user"
|
|
|
|
// Ghost cards cannot be unlinked
|
|
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
|
|
|
|
val luid = card.luid
|
|
|
|
// Unbind the card
|
|
card.aquaUser = null
|
|
async { cardRepository.save(card) }
|
|
|
|
fedy.onCardUnlinked(luid)
|
|
|
|
log.info("Net /card/unlink : Unlinked card ${card.id} from user ${u.username}")
|
|
|
|
SUCCESS
|
|
}
|
|
|
|
@API("/default-game")
|
|
@Doc("Get the default game for the card.", "Game ID")
|
|
suspend fun defaultGame(@RP username: Str) = us.cardByName(username) { card ->
|
|
mapOf("game" to cardGameService.getSummary(card).filterValues { it != null }.keys.firstOrNull())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate data from the card to the user's ghost card
|
|
*
|
|
* Assumption: The card is already linked to the user.
|
|
*/
|
|
suspend fun <T : IUserData> migrateCard(gameName: Str, repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card): Bool {
|
|
val ghost = card.aquaUser!!.ghostCard
|
|
|
|
// Check if data already exists in the user's ghost card
|
|
async { repo.findByCard(ghost) }?.let {
|
|
// Create a new dummy card for deleted data
|
|
it.card = async {
|
|
cardRepo.save(Card().apply {
|
|
luid = "Migrated data of ghost card ${ghost.id} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()} (${gameName})"
|
|
// Randomize an extId outside the normal range
|
|
extId = Random.nextLong(0x7FFFFFF7L shl 32, 0x7FFFFFFFL shl 32)
|
|
registerTime = LocalDateTime.now()
|
|
accessTime = registerTime
|
|
})
|
|
}
|
|
async { repo.save(it) }
|
|
}
|
|
|
|
// Migrate data from the card to the user's ghost card
|
|
// An easy migration is to change the UserData card field to the user's ghost card
|
|
val data = async { repo.findByCard(card) } ?: return false
|
|
data.card = card.aquaUser!!.ghostCard
|
|
async { repo.save(data) }
|
|
return true
|
|
}
|
|
|
|
suspend fun <T : IUserData> orphanData(gameName: Str, repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card) {
|
|
// Orphan the data by assigning them to a dummy card
|
|
repo.findByCard(card)?.let {
|
|
// Create a new dummy card for orphaned data
|
|
it.card = async {
|
|
cardRepo.save(Card().apply {
|
|
luid = "Unmigrated data of card ${card.luid} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()} (${gameName})"
|
|
// Randomize an extId outside the normal range
|
|
extId = Random.nextLong(0x7FFFFFF7L shl 32, 0x7FFFFFFFL shl 32)
|
|
registerTime = LocalDateTime.now()
|
|
accessTime = registerTime
|
|
})
|
|
}
|
|
async { repo.save(it) }
|
|
}
|
|
}
|
|
|
|
suspend fun getSummaryFor(repo: GenericUserDataRepo<*>, card: Card): Map<Str, Any>? {
|
|
val data = async { repo.findByCard(card) } ?: return null
|
|
return mapOf(
|
|
"name" to data.userName,
|
|
"rating" to data.playerRating,
|
|
"lastLogin" to data.lastPlayDate,
|
|
)
|
|
}
|
|
|
|
@Service
|
|
class CardGameService(
|
|
val maimai2: Mai2UserDataRepo,
|
|
val chusan: Chu3UserDataRepo,
|
|
val wacca: WcUserRepo,
|
|
val ongeki: OgkUserDataRepo,
|
|
val diva: icu.samnyan.aqua.sega.diva.dao.userdata.PlayerProfileRepository,
|
|
val safety: AquaNetSafetyService,
|
|
val cardRepo: CardRepository,
|
|
val em: EntityManager
|
|
) {
|
|
companion object {
|
|
val log = logger()
|
|
}
|
|
|
|
suspend fun migrate(crd: Card, games: List<String>) = async {
|
|
// Migrate data from the card to the user's ghost card
|
|
// An easy migration is to change the UserData card field to the user's ghost card
|
|
val dataRepos = mapOf(
|
|
"mai2" to maimai2,
|
|
"chu3" to chusan,
|
|
"ongeki" to ongeki,
|
|
"wacca" to wacca,
|
|
)
|
|
val remainingGames = dataRepos.keys.toMutableSet()
|
|
games.forEach { game ->
|
|
val dataRepo = dataRepos[game] ?: return@forEach
|
|
migrateCard(game, dataRepo, cardRepo, crd)
|
|
remainingGames.remove(game)
|
|
}
|
|
// For remaining games, orphan the data by assigning them to a dummy card
|
|
remainingGames.forEach { game -> orphanData(game, dataRepos[game]!!, cardRepo, crd) }
|
|
}
|
|
|
|
suspend fun getSummary(card: Card) = async {
|
|
mapOf(
|
|
"mai2" to getSummaryFor(maimai2, card),
|
|
"chu3" to getSummaryFor(chusan, card),
|
|
"ongeki" to getSummaryFor(ongeki, card),
|
|
"wacca" to getSummaryFor(wacca, card),
|
|
"diva" to diva.findByPdId(card.extId)()?.let {
|
|
mapOf(
|
|
"name" to it.playerName,
|
|
"rating" to it.level,
|
|
)
|
|
},
|
|
)
|
|
}
|
|
|
|
// Every hour
|
|
@Scheduled(fixedDelay = 3600000)
|
|
suspend fun autoBan() {
|
|
log.info("Running auto-ban")
|
|
val time = millis()
|
|
|
|
// Ban any players with unacceptable names
|
|
for (repo in listOf(maimai2, chusan, wacca, ongeki)) {
|
|
val all = async { repo.findAllNonBanned() }
|
|
val isSafe = safety.isSafeBatch(all.map { it.userName })
|
|
val toSave = all.filterIndexed { i, _ -> !isSafe[i] }.mapNotNull { it.card }
|
|
if (toSave.isNotEmpty()) {
|
|
log.info("Banning users ${toSave.joinToString(", ")}")
|
|
toSave.forEach { it.rankingBanned = true }
|
|
async { cardRepo.saveAll(toSave) }
|
|
}
|
|
}
|
|
|
|
log.info("Auto-ban completed in ${millis() - time}ms")
|
|
}
|
|
}
|