feat: some data management APIs (#176)

This commit is contained in:
Menci 2025-10-07 00:27:39 +08:00 committed by GitHub
parent 967d311ee4
commit b0d0f8ef7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 223 additions and 141 deletions

View File

@ -30,7 +30,8 @@ class CardController(
val cardService: CardService, val cardService: CardService,
val cardGameService: CardGameService, val cardGameService: CardGameService,
val cardRepository: CardRepository, val cardRepository: CardRepository,
val props: AquaNetProps val props: AquaNetProps,
val fedy: Fedy
) { ) {
companion object { companion object {
val log = logger() val log = logger()
@ -80,10 +81,12 @@ class CardController(
val id = cardService.sanitizeCardId(cardId) val id = cardService.sanitizeCardId(cardId)
// Create a new card // Create a new card
cardService.registerByAccessCode(id, u) val newCard = cardService.registerByAccessCode(id, u)
log.info("Net /card/link : Created new card $id for user ${u.username}") 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 return SUCCESS
} }
@ -98,6 +101,9 @@ class CardController(
val games = migrate.split(',') val games = migrate.split(',')
cardGameService.migrate(card, games) 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()}") log.info("Net /card/link : Linked card ${card.id} to user ${u.username} and migrated data to ${games.joinToString()}")
SUCCESS SUCCESS
@ -115,10 +121,14 @@ class CardController(
// Ghost cards cannot be unlinked // Ghost cards cannot be unlinked
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked" if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
val luid = card.luid
// Unbind the card // Unbind the card
card.aquaUser = null card.aquaUser = null
async { cardRepository.save(card) } async { cardRepository.save(card) }
fedy.onCardUnlinked(luid)
log.info("Net /card/unlink : Unlinked card ${card.id} from user ${u.username}") log.info("Net /card/unlink : Unlinked card ${card.id} from user ${u.username}")
SUCCESS SUCCESS
@ -136,7 +146,7 @@ class CardController(
* *
* Assumption: The card is already linked to the user. * Assumption: The card is already linked to the user.
*/ */
suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card): Bool { suspend fun <T : IUserData> migrateCard(gameName: Str, repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card): Bool {
val ghost = card.aquaUser!!.ghostCard val ghost = card.aquaUser!!.ghostCard
// Check if data already exists in the user's ghost card // Check if data already exists in the user's ghost card
@ -144,7 +154,7 @@ suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, cardRepo:
// Create a new dummy card for deleted data // Create a new dummy card for deleted data
it.card = async { it.card = async {
cardRepo.save(Card().apply { cardRepo.save(Card().apply {
luid = "Migrated data of ghost card ${ghost.id} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()}" luid = "Migrated data of ghost card ${ghost.id} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()} (${gameName})"
// Randomize an extId outside the normal range // Randomize an extId outside the normal range
extId = Random.nextLong(0x7FFFFFF7L shl 32, 0x7FFFFFFFL shl 32) extId = Random.nextLong(0x7FFFFFF7L shl 32, 0x7FFFFFFFL shl 32)
registerTime = LocalDateTime.now() registerTime = LocalDateTime.now()
@ -162,6 +172,23 @@ suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, cardRepo:
return true 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>? { suspend fun getSummaryFor(repo: GenericUserDataRepo<*>, card: Card): Map<Str, Any>? {
val data = async { repo.findByCard(card) } ?: return null val data = async { repo.findByCard(card) } ?: return null
return mapOf( return mapOf(
@ -189,18 +216,20 @@ class CardGameService(
suspend fun migrate(crd: Card, games: List<String>) = async { suspend fun migrate(crd: Card, games: List<String>) = async {
// Migrate data from the card to the user's ghost card // 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 // 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 -> games.forEach { game ->
when (game) { val dataRepo = dataRepos[game] ?: return@forEach
"mai2" -> migrateCard(maimai2, cardRepo, crd) migrateCard(game, dataRepo, cardRepo, crd)
"chu3" -> migrateCard(chusan, cardRepo, crd) remainingGames.remove(game)
"ongeki" -> migrateCard(ongeki, cardRepo, crd)
"wacca" -> migrateCard(wacca, cardRepo, crd)
// TODO: diva
// "diva" -> diva.findByPdId(card.extId.toInt()).getOrNull()?.let {
// it.pdId = card.aquaUser!!.ghostCard
// }
}
} }
// 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 { suspend fun getSummary(card: Card) = async {

View File

@ -1,31 +1,29 @@
package icu.samnyan.aqua.net package icu.samnyan.aqua.net
import ext.* import ext.*
import icu.samnyan.aqua.sega.general.service.CardService
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import java.security.MessageDigest import java.security.MessageDigest
import icu.samnyan.aqua.net.db.AquaNetUserRepo
import icu.samnyan.aqua.net.db.AquaNetUserFedyRepo
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.net.components.JWT import icu.samnyan.aqua.net.components.JWT
import icu.samnyan.aqua.net.db.AquaNetUserFedy import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.net.games.ImportController
import icu.samnyan.aqua.net.games.mai2.Mai2Import import icu.samnyan.aqua.net.games.mai2.Mai2Import
import icu.samnyan.aqua.net.games.ExportOptions import icu.samnyan.aqua.net.games.ExportOptions
import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler as Mai2UploadUserPlaylogHandler import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler as Mai2UploadUserPlaylogHandler
import icu.samnyan.aqua.sega.maimai2.handler.UpsertUserAllHandler as Mai2UpsertUserAllHandler import icu.samnyan.aqua.sega.maimai2.handler.UpsertUserAllHandler as Mai2UpsertUserAllHandler
import icu.samnyan.aqua.net.utils.ApiException import icu.samnyan.aqua.net.utils.ApiException
import java.util.Arrays
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.PlatformTransactionManager import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate import org.springframework.transaction.support.TransactionTemplate
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
import icu.samnyan.aqua.net.games.GenericUserDataRepo import icu.samnyan.aqua.net.games.GenericUserDataRepo
import icu.samnyan.aqua.net.games.IUserData import icu.samnyan.aqua.net.games.IUserData
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.ongeki.OgkUserDataRepo
import icu.samnyan.aqua.sega.wacca.model.db.WcUserRepo
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@Configuration @Configuration
@ -36,23 +34,32 @@ class FedyProps {
var remote: String = "" var remote: String = ""
} }
enum class FedyEvent { private data class CardCreatedEvent(val luid: Str, val extId: Long)
Linked, private data class CardLinkedEvent(val luid: Str, val oldExtId: Long?, val ghostExtId: Long, val migratedGames: List<Str>)
Unlinked, private data class CardUnlinkedEvent(val luid: Str)
Upserted, private data class DataUpdatedEvent(val extId: Long, val isGhostCard: Bool, val game: Str, val removeOldData: Bool)
Imported,
} private data class FedyEvent(
var cardCreated: CardCreatedEvent? = null,
var cardLinked: CardLinkedEvent? = null,
var cardUnlinked: CardUnlinkedEvent? = null,
var dataUpdated: DataUpdatedEvent? = null,
)
@RestController @RestController
@API("/api/v2/fedy") @API("/api/v2/fedy")
class Fedy( class Fedy(
val jwt: JWT, val jwt: JWT,
val userRepo: AquaNetUserRepo, val us: AquaUserServices,
val userFedyRepo: AquaNetUserFedyRepo, val cardRepo: CardRepository,
val cardService: CardService,
val mai2Import: Mai2Import, val mai2Import: Mai2Import,
val mai2UserDataRepo: Mai2UserDataRepo, val mai2UserDataRepo: Mai2UserDataRepo,
val mai2UploadUserPlaylog: Mai2UploadUserPlaylogHandler, val mai2UploadUserPlaylog: Mai2UploadUserPlaylogHandler,
val mai2UpsertUserAll: Mai2UpsertUserAllHandler, val mai2UpsertUserAll: Mai2UpsertUserAllHandler,
val chu3UserDataRepo: Chu3UserDataRepo,
val ongekiUserDataRepo: OgkUserDataRepo,
val waccaUserDataRepo: WcUserRepo,
val props: FedyProps, val props: FedyProps,
val transactionManager: PlatformTransactionManager val transactionManager: PlatformTransactionManager
) { ) {
@ -63,73 +70,37 @@ class Fedy(
if (!MessageDigest.isEqual(this.toByteArray(), props.key.toByteArray())) 403 - "Invalid Key" if (!MessageDigest.isEqual(this.toByteArray(), props.key.toByteArray())) 403 - "Invalid Key"
} }
@API("/status") val suppressEvents = ThreadLocal.withInitial { false }
fun handleStatus(@RP token: Str): Any { private fun <T> handleFedy(key: Str, block: () -> T): T {
val user = jwt.auth(token) val old = suppressEvents.get()
val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) suppressEvents.set(true)
return mapOf("linkedAt" to (userFedy?.createdAt?.toEpochMilli() ?: 0)) try {
key.checkKey()
return block()
} finally { suppressEvents.set(old) }
} }
@API("/link") data class DataPullReq(val extId: Long, val game: Str, val exportOptions: ExportOptions)
fun handleLink(@RP token: Str, @RP nonce: Str): Any { data class DataPullRes(val error: DataPullErr? = null, val result: Any? = null)
val user = jwt.auth(token) data class DataPullErr(val code: Int, val message: Str)
@API("/data/pull")
if (userFedyRepo.findByAquaNetUserAuId(user.auId) != null) 412 - "User already linked" fun handleDataPull(@RH(KEY_HEADER) key: Str, @RB req: DataPullReq): DataPullRes = handleFedy(key) {
val userFedy = AquaNetUserFedy(aquaNetUser = user) val card = cardRepo.findByExtId(req.extId).orElse(null)
userFedyRepo.save(userFedy) ?: (404 - "Card with extId ${req.extId} not found")
fun caught(block: () -> Any) =
notify(FedyEvent.Linked, mapOf("auId" to user.auId, "nonce" to nonce)) try { DataPullRes(result = block()) }
return mapOf("linkedAt" to userFedy.createdAt.toEpochMilli()) catch (e: ApiException) { DataPullRes(error = DataPullErr(code = e.code, message = e.message.toString())) }
} when (req.game) {
"mai2" -> caught { mai2Import.export(card, req.exportOptions) }
@API("/unlink")
fun handleUnlink(@RP token: Str): Any {
val user = jwt.auth(token)
val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) ?: 412 - "User not linked"
userFedyRepo.delete(userFedy)
notify(FedyEvent.Unlinked, mapOf("auId" to user.auId))
return SUCCESS
}
private fun ensureUser(auId: Long): AquaNetUser {
val userFedy = userFedyRepo.findByAquaNetUserAuId(auId) ?: 404 - "User not linked"
val user = userRepo.findByAuId(auId) ?: 404 - "User not found"
return user
}
data class UnlinkByRemoteReq(val auId: Long)
@API("/unlink-by-remote")
fun handleUnlinkByRemote(@RH(KEY_HEADER) key: Str, @RB req: UnlinkByRemoteReq): Any {
key.checkKey()
val user = ensureUser(req.auId)
userFedyRepo.deleteByAquaNetUserAuId(user.auId)
// No need to notify remote, because initiated by remote
return SUCCESS
}
data class PullReq(val auId: Long, val game: Str, val exportOptions: ExportOptions)
@API("/pull")
fun handlePull(@RH(KEY_HEADER) key: Str, @RB req: PullReq): Any {
key.checkKey()
val user = ensureUser(req.auId)
fun catched(block: () -> Any) =
try { mapOf("result" to block()) }
catch (e: ApiException) { mapOf("error" to mapOf("code" to e.code, "message" to e.message.toString())) }
return when (req.game) {
"mai2" -> catched { mai2Import.export(user, req.exportOptions) }
else -> 406 - "Unsupported game" else -> 406 - "Unsupported game"
} }
} }
data class PushReq(val auId: Long, val game: Str, val data: JDict, val removeOldData: Bool) data class DataPushReq(val extId: Long, val game: Str, val data: JDict, val removeOldData: Bool)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@API("/push") @API("/data/push")
fun handlePush(@RH(KEY_HEADER) key: Str, @RB req: PushReq): Any { fun handleDataPush(@RH(KEY_HEADER) key: Str, @RB req: DataPushReq): Any = handleFedy(key) {
key.checkKey() val extId = req.extId
val user = ensureUser(req.auId)
val extId = user.ghostCard.extId
fun<UserData : IUserData, UserRepo : GenericUserDataRepo<UserData>> removeOldData(repo: UserRepo) { fun<UserData : IUserData, UserRepo : GenericUserDataRepo<UserData>> removeOldData(repo: UserRepo) {
val oldData = repo.findByCard_ExtId(extId) val oldData = repo.findByCard_ExtId(extId)
if (oldData.isPresent) { if (oldData.isPresent) {
@ -149,29 +120,111 @@ class Fedy(
else -> 406 - "Unsupported game" else -> 406 - "Unsupported game"
} } } }
return SUCCESS SUCCESS
} }
fun onUpserted(game: Str, maybeExtId: Any?) = maybeNotifyAsync(FedyEvent.Upserted, game, maybeExtId) data class CardResolveReq(val luid: Str, val pairedLuid: Str?, val createIfNotFound: Bool)
fun onImported(game: Str, maybeExtId: Any?) = maybeNotifyAsync(FedyEvent.Imported, game, maybeExtId) data class CardResolveRes(val extId: Long, val isGhostCard: Bool, val isNewlyCreated: Bool, val isPairedLuidDiverged: Bool)
@API("/card/resolve")
fun handleCardResolve(@RH(KEY_HEADER) key: Str, @RB req: CardResolveReq): CardResolveRes = handleFedy(key) {
var card = cardService.tryLookup(req.luid)
var isNewlyCreated = false
if (card != null) {
card = card.maybeGhost()
if (!card.isGhost) isNewlyCreated = isCardFresh(card)
} else if (req.createIfNotFound) {
card = cardService.registerByAccessCode(req.luid, null)
isNewlyCreated = true
log.info("Fedy /card/resolve : Created new card ${card.id} (${card.luid})")
}
var isPairedLuidDiverged = false
if (req.pairedLuid != null) {
var pairedCard = cardService.tryLookup(req.pairedLuid)?.maybeGhost()
if (pairedCard?.extId != card?.extId) {
var isGhost = pairedCard?.isGhost == true
var isFresh = pairedCard != null && isCardFresh(pairedCard)
if (isGhost && isFresh) isPairedLuidDiverged = true
else if (!isGhost && card?.isGhost == true) {
// Ensure paired card is linked, if the main card is linked
// If the main card is not linked, there's nothing Fedy can do. It's Fedy's best effort.
if (pairedCard == null) { pairedCard = cardService.registerByAccessCode(req.pairedLuid, card.aquaUser) }
else { pairedCard.aquaUser = card.aquaUser; cardRepo.save(pairedCard) }
log.info("Fedy /card/resolve : Created paired card ${pairedCard.id} (${pairedCard.luid}) for user ${card.aquaUser?.auId} (${card.aquaUser?.username})")
}
}
}
private fun maybeNotifyAsync(event: FedyEvent, game: Str, maybeExtId: Any?) = if (!props.enabled) {} else CompletableFuture.runAsync { try { CardResolveRes(
val extId = maybeExtId?.long ?: return@runAsync card?.extId ?: 0,
val user = userRepo.findByGhostCardExtId(extId) ?: return@runAsync card?.isGhost ?: false,
val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) ?: return@runAsync isNewlyCreated,
notify(event, mapOf("auId" to user.auId, "game" to game)) isPairedLuidDiverged)
} catch (e: Exception) { }
log.error("Error handling Fedy on maybeNotifyAsync($event, $game, $maybeExtId)", e)
} }
private fun notify(event: FedyEvent, body: Any?) { data class CardLinkReq(val auId: Long, val luid: Str)
@API("/card/link")
fun handleCardLink(@RH(KEY_HEADER) key: Str, @RB req: CardLinkReq): Any = handleFedy(key) {
val ru = us.userRepo.findByAuId(req.auId) ?: (404 - "User not found")
var card = cardService.tryLookup(req.luid)
if (card == null) {
card = cardService.registerByAccessCode(req.luid, ru)
log.info("Fedy /card/link : Linked new card ${card.id} (${card.luid}) to user ${ru.auId} (${ru.username})")
} else {
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
val cu = card.aquaUser
if (cu != null) {
if (cu.auId == req.auId) log.info("Fedy /card/link : Existing card ${card.id} (${card.luid}) already linked to user ${ru.auId} (${ru.username})")
else 400 - "Card linked to another user"
} else {
card.aquaUser = ru
cardRepo.save(card)
log.info("Fedy /card/link : Linked existing card ${card.id} (${card.luid}) to user ${ru.auId} (${ru.username})")
}
}
}
data class CardUnlinkReq(val auId: Long, val luid: Str)
@API("/card/unlink")
fun handleCardUnlink(@RH(KEY_HEADER) key: Str, @RB req: CardUnlinkReq): Any = handleFedy(key) {
val card = cardService.tryLookup(req.luid)
val cu = card?.aquaUser ?: return@handleFedy SUCCESS // Nothing to do
if (cu.auId != req.auId) 400 - "Card linked to another user"
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
card.aquaUser = null
cardRepo.save(card)
log.info("Fedy /card/unlink : Unlinked card ${card.id} (${card.luid}) from user ${cu.auId} (${cu.username})")
}
fun onCardCreated(luid: Str, extId: Long) = maybeNotifyAsync(FedyEvent(cardCreated = CardCreatedEvent(luid, extId)))
fun onCardLinked(luid: Str, oldExtId: Long?, ghostExtId: Long, migratedGames: List<Str>) = maybeNotifyAsync(FedyEvent(cardLinked = CardLinkedEvent(luid, oldExtId, ghostExtId, migratedGames)))
fun onCardUnlinked(luid: Str) = maybeNotifyAsync(FedyEvent(cardUnlinked = CardUnlinkedEvent(luid)))
fun onDataUpdated(extId: Long, game: Str, removeOldData: Bool) = maybeNotifyAsync({
val card = cardRepo.findByExtId(extId).orElse(null) ?: return@maybeNotifyAsync null // Card not found, nothing to do
FedyEvent(dataUpdated = DataUpdatedEvent(extId, card.isGhost, game, removeOldData))
})
private fun maybeNotifyAsync(event: FedyEvent) = maybeNotifyAsync({ event })
private fun maybeNotifyAsync(getEvent: () -> FedyEvent?) = if (!props.enabled && !suppressEvents.get()) {} else CompletableFuture.runAsync {
var event: FedyEvent? = null
try {
event = getEvent()
if (event == null) return@runAsync // Nothing to do
notify(event)
} catch (e: Exception) {
log.error("Error handling Fedy on maybeNotifyAsync($event)", e)
}
}.let {}
private fun notify(event: FedyEvent) {
val MAX_RETRY = 3 val MAX_RETRY = 3
val body = body?.toJson() ?: "{}" val body = event.toJson() ?: "{}"
var retry = 0 var retry = 0
var shouldRetry = true var shouldRetry = true
while (retry < MAX_RETRY) { while (true) {
try { try {
val response = "${props.remote.trimEnd('/')}/notify/${event.name}".request() val response = "${props.remote.trimEnd('/')}/notify".request()
.header("Content-Type" to "application/json") .header("Content-Type" to "application/json")
.header(KEY_HEADER to props.key) .header(KEY_HEADER to props.key)
.post(body) .post(body)
@ -191,13 +244,32 @@ class Fedy(
} }
} }
// Apparently existing cards could possibly be fresh and never used in any game. Treat them as new cards.
private fun isCardFresh(c: Card): Bool {
fun <T : IUserData> checkForGame(repo: GenericUserDataRepo<T>, card: Card): Bool = repo.findByCard(card) == null
return when {
checkForGame(mai2UserDataRepo, c) -> false
checkForGame(chu3UserDataRepo, c) -> false
checkForGame(ongekiUserDataRepo, c) -> false
checkForGame(waccaUserDataRepo, c) -> false
else -> true
}
}
companion object companion object
{ {
const val KEY_HEADER = "X-Fedy-Key" const val KEY_HEADER = "X-Fedy-Key"
val log = logger() val log = logger()
fun getGameName(gameId: Str) = when (gameId) { fun getGameName(gameId: Str) = when (gameId) {
"mai2" -> "mai2"
"SDEZ" -> "mai2" "SDEZ" -> "mai2"
"chu3" -> "chu3"
"SDHD" -> "chu3"
"ongeki" -> "mu3"
"SDDT" -> "mu3"
"wacca" -> "wacca"
"SDFE" -> "wacca"
else -> null // Not supported else -> null // Not supported
} }
} }

View File

@ -19,7 +19,8 @@ class FrontierProps {
@API("/api/v2/frontier") @API("/api/v2/frontier")
class Frontier( class Frontier(
val cardService: CardService, val cardService: CardService,
val props: FrontierProps val props: FrontierProps,
val fedy: Fedy
) { ) {
fun Str.checkFtk() { fun Str.checkFtk() {
if (this != props.ftk) 403 - "Invalid FTK" if (this != props.ftk) 403 - "Invalid FTK"
@ -35,6 +36,9 @@ class Frontier(
if (async { cardService.cardRepo.findByLuid(accessCode) }.isPresent) 400 - "Card already registered" if (async { cardService.cardRepo.findByLuid(accessCode) }.isPresent) 400 - "Card already registered"
val card = async { cardService.registerByAccessCode(accessCode) } val card = async { cardService.registerByAccessCode(accessCode) }
fedy.onCardCreated(accessCode, card.extId)
return mapOf( return mapOf(
"card" to card, "card" to card,
"id" to card.extId // Expose hidden ID "id" to card.extId // Expose hidden ID

View File

@ -1,29 +0,0 @@
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_user_fedy")
class AquaNetUserFedy(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
@Column(nullable = false)
var createdAt: Instant = Instant.now(),
// Linking to the AquaNetUser
@OneToOne
@JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser,
) : Serializable
@Repository
interface AquaNetUserFedyRepo : JpaRepository<AquaNetUserFedy, Long> {
fun findByAquaNetUserAuId(auId: Long): AquaNetUserFedy?
fun deleteByAquaNetUserAuId(auId: Long): Unit
}

View File

@ -6,6 +6,7 @@ import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.Fedy import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.utils.AquaNetProps import icu.samnyan.aqua.net.utils.AquaNetProps
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.model.Card
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.NoRepositoryBean import org.springframework.data.repository.NoRepositoryBean
@ -81,11 +82,11 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
val listRepos = exportRepos.filter { it.key returns List::class } val listRepos = exportRepos.filter { it.key returns List::class }
val singleRepos = exportRepos.filter { !(it.key returns List::class) } val singleRepos = exportRepos.filter { !(it.key returns List::class) }
fun export(u: AquaNetUser): ExportModel = export(u, ExportOptions()) fun export(u: AquaNetUser): ExportModel = export(u.ghostCard, ExportOptions())
fun export(u: AquaNetUser, options: ExportOptions) = createEmpty().apply { fun export(c: Card, options: ExportOptions) = createEmpty().apply {
gameId = game gameId = game
userData = userDataRepo.findByCard(u.ghostCard) ?: (404 - "User not found") userData = userDataRepo.findByCard(c) ?: (404 - "User not found")
exportRepos.forEach { (f, u) -> exportRepos.forEach { (f, u) ->
if (f returns List::class) f.set(this, u.findByUser(userData)) if (f returns List::class) f.set(this, u.findByUser(userData))
else u.findSingleByUser(userData)()?.let { f.set(this, it) } else u.findSingleByUser(userData)()?.let { f.set(this, it) }
@ -147,7 +148,7 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
} }
} }
Fedy.getGameName(game)?.let { fedy.onImported(it, u.ghostCard.extId) } Fedy.getGameName(game)?.let { fedy.onDataUpdated(u.ghostCard.extId, it, true) }
SUCCESS SUCCESS
} }

View File

@ -2,6 +2,7 @@ package icu.samnyan.aqua.sega.aimedb
import ext.* import ext.*
import icu.samnyan.aqua.net.BotProps import icu.samnyan.aqua.net.BotProps
import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.sega.allnet.AllNetProps import icu.samnyan.aqua.sega.allnet.AllNetProps
import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.model.Card
@ -26,6 +27,7 @@ class AimeDB(
val cardService: CardService, val cardService: CardService,
val us: AquaUserServices, val us: AquaUserServices,
val allNetProps: AllNetProps, val allNetProps: AllNetProps,
val fedy: Fedy,
): ChannelInboundHandlerAdapter() { ): ChannelInboundHandlerAdapter() {
val logger = logger() val logger = logger()
@ -200,6 +202,8 @@ class AimeDB(
status = 1 status = 1
aimeId = card.extId aimeId = card.extId
fedy.onCardCreated(luid, card.extId)
} }
else logger.warn("> Duplicated Aime Card Register detected, access code: $luid") else logger.warn("> Duplicated Aime Card Register detected, access code: $luid")

View File

@ -95,7 +95,7 @@ class Maimai2ServletController(
val ctx = RequestContext(req, data.mut) val ctx = RequestContext(req, data.mut)
serialize(api, handlers[api]!!(ctx) ?: noop).also { serialize(api, handlers[api]!!(ctx) ?: noop).also {
log.info("$token : $api > ${it.truncate(500)}") log.info("$token : $api > ${it.truncate(500)}")
if (api == "UpsertUserAllApi") { fedy.onUpserted("mai2", data["userId"]) } if (api == "UpsertUserAllApi") { data["userId"]?.long?.let { fedy.onDataUpdated(it, "mai2", false) } }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -0,0 +1 @@
DROP TABLE aqua_net_user_fedy;