mirror of
https://github.com/MewoLab/AquaDX.git
synced 2025-10-25 20:12:39 +00:00
Compare commits
2 Commits
967d311ee4
...
010d4592e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
010d4592e4 | ||
|
|
b0d0f8ef7d |
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE aqua_net_user_fedy;
|
||||||
Loading…
x
Reference in New Issue
Block a user