diff --git a/src/main/java/ext/Ext.kt b/src/main/java/ext/Ext.kt index 0fdcfe53..5cbe6ba1 100644 --- a/src/main/java/ext/Ext.kt +++ b/src/main/java/ext/Ext.kt @@ -218,6 +218,7 @@ fun Str.splitLines() = replace("\r\n", "\n").split('\n') @OptIn(ExperimentalStdlibApi::class) fun Str.md5() = MD5.digest(toByteArray(Charsets.UTF_8)).toHexString() fun Str.fromChusanUsername() = String(this.toByteArray(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8) +fun Str.truncate(len: Int) = if (this.length > len) this.take(len) + "..." else this // Coroutine suspend fun async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() } @@ -250,4 +251,3 @@ val Pair<*, S>.r get() = component2() // Database val Query.exec get() = resultList.map { (it as Array<*>).toList() } - diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt new file mode 100644 index 00000000..4f6e4aab --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt @@ -0,0 +1,240 @@ +package icu.samnyan.aqua.sega.maimai2 + +import ext.* +import icu.samnyan.aqua.sega.general.PagedHandler + +fun Maimai2ServletController.initApis() { + // Used because maimai does not actually require paging implementation + fun String.unpaged(key: String? = null, fn: PagedHandler) { + val k = key ?: (this.replace("get", "").firstCharLower() + "List") + this { + fn(this).let { mapOf("userId" to uid, "nextIndex" to 0, "length" to it.size, k to it) } + } + } + + "GetUserExtend" { mapOf( + "uid" to uid, + "userExtend" to (db.userExtend.findSingleByUser_Card_ExtId(uid)() ?: (404 - "User not found")) + ) } + + "GetUserData" { mapOf( + "uid" to uid, + "userData" to (db.userData.findByCardExtId(uid)() ?: (404 - "User not found")), + "banState" to 0 + ) } + + "GetUserLoginBonus".unpaged { db.userLoginBonus.findByUser_Card_ExtId(uid) } + "GetUserMap".unpaged { db.userMap.findByUser_Card_ExtId(uid) } + "GetUserCard".unpaged { db.userCard.findByUser_Card_ExtId(uid) } + "GetUserCharge".unpaged { db.userCharge.findByUser_Card_ExtId(uid) } + "GetUserFriendSeasonRanking".unpaged { db.userFriendSeasonRanking.findByUser_Card_ExtId(uid) } + "GetUserCourse".unpaged { db.userCourse.findByUser_Card_ExtId(uid) } + + "GetUserMusic".unpaged { + ls(mapOf("userMusicDetailList" to db.userMusicDetail.findByUser_Card_ExtId(uid))) + } + + "GetUserFavorite" { mapOf( + "uid" to uid, + "userFavorite" to db.userFavorite.findByUser_Card_ExtIdAndItemKind(uid, data["itemKind"] as Int) + ) } + + "GetUserActivity" { + db.userAct.findByUser_Card_ExtId(uid).let { act -> mapOf( + "userActivity" to mapOf( + "playList" to act.filter { it.kind == 1 }, + "musicList" to act.filter { it.kind == 2 } + ) + ) } + } + + // Maimai only request for event type 1 + "GetGameEvent" static { mapOf("type" to 1, "gameEventList" to db.gameEvent.findByEnable(true)) } + "GetGameCharge" static { db.gameCharge.findAll().let { mapOf("length" to it.size, "gameChargeList" to it) } } + + "GetUserRivalData" { + val rivalId = parsing { data["rivalId"]!!.long } + + // rivalId should store and fetch with the id column of table rather than card_ext_id + // or user will be able to get others' ext_id by setting them as rival + mapOf( + "uid" to uid, + "userRivalData" to mapOf( + "rivalId" to rivalId, + "rivalName" to (db.userData.findById(rivalId)()?.userName ?: "") + ) + ) + } + + "GetUserOption" { mapOf( + "uid" to uid, + "userOption" to (db.userOption.findSingleByUser_Card_ExtId(uid)() ?: (404 - "User not found")) + ) } + + "CreateToken" static { """{"Bearer":"meow"}""" } + "CMUpsertUserPrintlog" static { """{"returnCode":1,"orderId":"0","serialId":"FAKECARDIMAG12345678"}""" } + + "CMGetSellingCard" static { db.gameSellingCard.findAll().let { + mapOf("length" to it.size, "sellingCardList" to it) + } } + + "CMGetUserCharacter" { db.userCharacter.findByUser_Card_ExtId(uid).let { + mapOf( + "returnCode" to 1, + "length" to it.size, + "userCharacterList" to it + ) + } } + + "CMGetUserPreview" { db.userData.findByCardExtId(uid)()?.let { + mapOf( + "uid" to uid, + "userName" to it.userName, + "rating" to it.playerRating, + "lastDataVersion" to it.lastDataVersion, + "isLogin" to false, + "isExistSellingCard" to false + ) + } ?: (404 - "User not found") } + + "GetUserPreview" { + val d = db.userData.findByCardExtId(uid)() ?: (404 - "User not found") + val option = db.userOption.findSingleByUser_Card_ExtId(uid)() + + mapOf( + "uid" to uid, + "userName" to d.userName, + "isLogin" to false, + "lastGameId" to d.lastGameId, + "lastDataVersion" to d.lastDataVersion, + "lastRomVersion" to d.lastRomVersion, + "lastLoginDate" to d.lastPlayDate, + "lastPlayDate" to d.lastPlayDate, + "playerRating" to d.playerRating, + "nameplateId" to d.plateId, + "iconId" to d.iconId, + "trophyId" to 0, + "partnerId" to d.partnerId, + "frameId" to d.frameId, + "totalAwake" to d.totalAwake, + "isNetMember" to d.isNetMember, + "dailyBonusDate" to d.dailyBonusDate, + "headPhoneVolume" to (option?.headPhoneVolume ?: 0), + "dispRate" to (option?.dispRate ?: 0), + "isInherit" to false, + "banState" to d.banState + ) + } + + "GetUserShopStock" { + val shopItemIdList = data["shopItemIdList"] as List<*> + + mapOf( + "uid" to uid, + "userShopStockList" to shopItemIdList.map { mapOf( + "shopItemId" to it, + "tradeCount" to 0 + ) } + ) + } + + // Empty List Handlers + "GetUserRecommendRateMusic" { mapOf( + "uid" to uid, + "userRecommendRateMusicIdList" to empty + ) } + "GetUserRecommendSelectMusic" { mapOf( + "uid" to uid, + "userRecommendSelectionMusicIdList" to empty + ) } + "GetUserRegion".unpaged { empty } + "GetUserGhost".unpaged { empty } + "GetUserFriendBonus" { mapOf("uid" to uid, "returnCode" to 0, "getMiles" to 0) } + "GetUserIntimate" { mapOf("uid" to uid, "length" to 0, "userIntimateList" to empty) } + "GetTransferFriend" { mapOf("uid" to uid, "transferFriendList" to empty) } + "GetUserKaleidxScope" { mapOf("uid" to uid, "userKaleidxScopeList" to empty) } + "GetUserNewItem" { mapOf("uid" to uid, "itemKind" to 0, "itemId" to 0) } + "GetUserNewItemList" { mapOf("uid" to uid, "userItemList" to empty) } + + "GetUserCardPrintError" static { mapOf("length" to 0, "userPrintDetailList" to empty) } + "GetUserFriendCheck" static { mapOf("returnCode" to 0) } + "UserFriendRegist" static { mapOf("returnCode1" to 0, "returnCode2" to 0) } + "GetGameNgMusicId" static { mapOf("length" to 0, "musicIdList" to empty) } + "GetGameTournamentInfo" static { mapOf("length" to 0, "gameTournamentInfoList" to empty) } + "GetGameKaleidxScope" static { mapOf("gameKaleidxScopeList" to empty) } + + "GetGameSetting" static { + // The client-side implementation for reboot time is extremely cursed. + // Only hour and minute are used, date is discarded and second is set to 0. + // The time is adjusted to the next day if it's 12 hours or more from now. + // And it's using local timezone instead of treating it as UTC. + // The official maimai cabs will reboot every day, but we don't want that + // So, we need to return the hour and minute 5 hours ago + // val rebootStart = Instant.now().atZone(ZoneId.of("Asia/Tokyo")).minusHours(5) + // val rebootEnd = rebootStart.plusSeconds(60) + // Nope that didn't work + + mapOf( + "isAouAccession" to true, + "gameSetting" to mapOf( +// "rebootStartTime" to GAME_SETTING_DATE_FMT.format(rebootStart), +// "rebootEndTime" to GAME_SETTING_DATE_FMT.format(rebootEnd), + "rebootStartTime" to "2020-01-01 23:59:00.0", + "rebootEndTime" to "2020-01-01 23:59:00.0", + "rebootInterval" to 0, + + // Fields below doesn't seem to be used by the client at all + "isMaintenance" to false, + "requestInterval" to 10, + "movieUploadLimit" to 0, + "movieStatus" to 0, + "movieServerUri" to "", + "deliverServerUri" to "", + "oldServerUri" to "", + "usbDlServerUri" to "", + + // Fields below are SDGB-specific settings (not present in SDEZ) + "pingDisable" to true, + "packetTimeout" to 20_000, + "packetTimeoutLong" to 60_000, + "packetRetryCount" to 5, + "userDataDlErrTimeout" to 300_000, + "userDataDlErrRetryCount" to 5, + "userDataDlErrSamePacketRetryCount" to 5, + "userDataUpSkipTimeout" to 0, + "userDataUpSkipRetryCount" to 0, + "iconPhotoDisable" to true, + "uploadPhotoDisable" to false, + "maxCountMusic" to 0, + "maxCountItem" to 0 + ) + ) + } + + "GetGameWeeklyData" static { mapOf( + "gameWeeklyData" to mapOf( + "missionCategory" to 0, + "updateDate" to "2024-01-01 00:00:00.0", + "beforeDate" to "2077-01-01 00:00:00.0" + ) + ) } + + "GetUserMissionData" { mapOf( + "uid" to uid, + "userWeeklyData" to mapOf ( + "lastLoginWeek" to "", + "beforeLoginWeek" to "", + "friendBonusFlag" to false + ), + "userMissionDataList" to empty + ) } + + "GetGameMusicScore" static { mapOf( + "gameMusicScore" to mapOf( + "musicId" to 0, + "level" to 0, + "type" to 0, + "scoreData" to "" + ) + ) } +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt index b1101106..4ad25e62 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt @@ -3,13 +3,13 @@ package icu.samnyan.aqua.sega.maimai2 import ext.* import icu.samnyan.aqua.net.utils.ApiException import icu.samnyan.aqua.net.utils.simpleDescribe -import icu.samnyan.aqua.sega.general.BaseHandler +import icu.samnyan.aqua.sega.general.* import icu.samnyan.aqua.sega.maimai2.handler.* import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos import icu.samnyan.aqua.spring.Metrics import io.ktor.client.request.* +import jakarta.servlet.http.HttpServletRequest import org.slf4j.LoggerFactory -import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import java.time.format.DateTimeFormatter import kotlin.reflect.full.declaredMemberProperties @@ -34,8 +34,9 @@ class Maimai2ServletController( val getUserRivalMusic: GetUserRivalMusicHandler, val getUserCharacter: GetUserCharacterHandler, val getGameRanking: GetGameRankingHandler, - val repos: Mai2Repos -) { + val db: Mai2Repos +): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) { + companion object { private val logger = LoggerFactory.getLogger(Maimai2ServletController::class.java) private val empty = listOf() @@ -43,306 +44,35 @@ class Maimai2ServletController( private val GAME_SETTING_TIME_FMT = DateTimeFormatter.ofPattern("HH:mm:00") } - val getUserExtend = UserReqHandler { _, userId -> mapOf( - "userId" to userId, - "userExtend" to (repos.userExtend.findSingleByUser_Card_ExtId(userId)() ?: (404 - "User not found")) - ) } + init { initApis() } - val getUserData = UserReqHandler { _, userId -> mapOf( - "userId" to userId, - "userData" to (repos.userData.findByCardExtId(userId)() ?: (404 - "User not found")), - "banState" to 0 - ) } - - val getUserLoginBonus = UserReqHandler { _, userId -> mapOf( - "userId" to userId, - "nextIndex" to 0, - "userLoginBonusList" to repos.userLoginBonus.findByUser_Card_ExtId(userId) - ) } - - val getUserMap = UserReqHandler { _, userId -> mapOf( - "userId" to userId, - "nextIndex" to 0, - "userMapList" to repos.userMap.findByUser_Card_ExtId(userId) - ) } - - val getUserMusic = UserReqHandler { _, userId -> mapOf( - "userId" to userId, - "nextIndex" to 0, - "userMusicList" to listOf(mapOf("userMusicDetailList" to repos.userMusicDetail.findByUser_Card_ExtId(userId))) - ) } - - val getUserCard = UserReqHandler { _, userId -> mapOf( - "userId" to userId, - "nextIndex" to 0, - "userCardList" to repos.userCard.findByUser_Card_ExtId(userId) - ) } - - val getUserCharge = UserReqHandler { _, userId -> repos.userCharge.findByUser_Card_ExtId(userId).let { mapOf( - "userId" to userId, - "length" to it.size, - "userChargeList" to it - ) } } - - val getUserFriendSeasonRanking = UserReqHandler { _, userId -> mapOf( - "userId" to userId, - "nextIndex" to 0, - "userFriendSeasonRankingList" to repos.userFriendSeasonRanking.findByUser_Card_ExtId(userId) - ) } - - val getUserCourse = UserReqHandler { _, userId -> mapOf( - "userId" to userId, - "nextIndex" to 0, - "userCourseList" to repos.userCourse.findByUser_Card_ExtId(userId) - ) } - - val getUserFavorite = UserReqHandler { req, userId -> mapOf( - "userId" to userId, - "userFavorite" to repos.userFavorite.findByUser_Card_ExtIdAndItemKind(userId, req["itemKind"] as Int) - ) } - - val getUserActivity = UserReqHandler { _, userId -> - repos.userAct.findByUser_Card_ExtId(userId).let { act -> mapOf( - "userActivity" to mapOf( - "playList" to act.filter { it.kind == 1 }, - "musicList" to act.filter { it.kind == 2 } - ) - ) } + val endpointList = setOf("GetGameRankingApi","GetUserCharacterApi","GetUserItemApi","GetUserPortraitApi", + "GetUserRatingApi","UploadUserPhotoApi","UploadUserPlaylogApi","UploadUserPortraitApi","UserLoginApi", + "UserLogoutApi","UpsertUserAllApi","CMGetUserCardApi","CMGetUserCardPrintErrorApi","CMGetUserDataApi", + "CMGetUserItemApi","CMUpsertUserPrintApi","GetUserFavoriteItemApi","GetUserRivalMusicApi","GetUserScoreRankingApi", + "UpsertClientBookkeepingApi","UpsertClientSettingApi","UpsertClientTestmodeApi","UpsertClientUploadApi", + "Ping","RemoveTokenApi","CMLoginApi","CMLogoutApi","CMUpsertBuyCardApi").mut.also { + println(it.filter { it !in initH.keys }.toJson()) } - val getGameCharge = BaseHandler { repos.gameCharge.findAll().let { - mapOf("length" to it.size, "gameChargeList" to it) - } } - - val getGameEvent = BaseHandler { - val type = parsing { (it["type"] as Number).toInt() } - mapOf( - "type" to type, - "gameEventList" to repos.gameEvent.findByEnable(true) // Maimai only request for type 1 - ) - } - - val getUserRivalData = UserReqHandler { req, userId -> - val rivalId = parsing { (req["rivalId"] as Number).toLong() } - - // rivalId should store and fetch with the id column of table rather than card_ext_id - // or user will be able to get others' ext_id by setting them as rival - mapOf( - "userId" to userId, - "userRivalData" to mapOf( - "rivalId" to rivalId, - "rivalName" to (repos.userData.findById(rivalId)()?.userName ?: "") - ) - ) - } - - val getUserOption = UserReqHandler { _, userId -> mapOf( - "userId" to userId, - "userOption" to (repos.userOption.findSingleByUser_Card_ExtId(userId)() ?: (404 - "User not found")) - ) } - - val cmGetSellingCard = BaseHandler { repos.gameSellingCard.findAll().let { - mapOf("length" to it.size, "sellingCardList" to it) - } } - - val cmGetUserCharacter = UserReqHandler { _, userId -> repos.userCharacter.findByUser_Card_ExtId(userId).let { - mapOf( - "returnCode" to 1, - "length" to it.size, - "userCharacterList" to it - ) - } } - - val cmGetUserPreview = UserReqHandler { _, userId -> repos.userData.findByCardExtId(userId)()?.let { - mapOf( - "userId" to userId, - "userName" to it.userName, - "rating" to it.playerRating, - "lastDataVersion" to it.lastDataVersion, - "isLogin" to false, - "isExistSellingCard" to false - ) - } ?: (404 - "User not found") } - - val getUserPreview = UserReqHandler { _, userId -> - val d = repos.userData.findByCardExtId(userId)() ?: (404 - "User not found") - val option = repos.userOption.findSingleByUser_Card_ExtId(userId)() - - mapOf( - "userId" to userId, - "userName" to d.userName, - "isLogin" to false, - "lastGameId" to d.lastGameId, - "lastDataVersion" to d.lastDataVersion, - "lastRomVersion" to d.lastRomVersion, - "lastLoginDate" to d.lastPlayDate, - "lastPlayDate" to d.lastPlayDate, - "playerRating" to d.playerRating, - "nameplateId" to d.plateId, - "iconId" to d.iconId, - "trophyId" to 0, - "partnerId" to d.partnerId, - "frameId" to d.frameId, - "totalAwake" to d.totalAwake, - "isNetMember" to d.isNetMember, - "dailyBonusDate" to d.dailyBonusDate, - "headPhoneVolume" to (option?.headPhoneVolume ?: 0), - "dispRate" to (option?.dispRate ?: 0), - "isInherit" to false, - "banState" to d.banState - ) - } - - val getUserShopStock = UserReqHandler { req, userId -> - val shopItemIdList = req["shopItemIdList"] as List<*> - - mapOf( - "userId" to userId, - "userShopStockList" to shopItemIdList.map { mapOf( - "shopItemId" to it, - "tradeCount" to 0 - ) } - ) - } - - // Empty List Handlers - val getUserRecommendRateMusic = UserReqHandler { _, userId -> mapOf( - "userId" to userId, - "userRecommendRateMusicIdList" to empty - ) } - val getUserRecommendSelectMusic = UserReqHandler { _, uid -> mapOf( - "userId" to uid, - "userRecommendSelectionMusicIdList" to empty - ) } - val getUserCardPrintError = BaseHandler { mapOf("length" to 0, "userPrintDetailList" to empty) } - val getUserRegion = UserReqHandler { _, uid -> mapOf("userId" to uid, "length" to 0, "userRegionList" to empty) } - val getUserGhost = UserReqHandler { _, uid -> mapOf("userId" to uid, "userGhostList" to empty) } - val getUserFriendBonus = UserReqHandler { _, uid -> mapOf("userId" to uid, "returnCode" to 0, "getMiles" to 0) } - val getUserFriendCheck = BaseHandler { mapOf("returnCode" to 0) } - val userFriendRegist = BaseHandler { mapOf("returnCode1" to 0, "returnCode2" to 0) } - val getUserIntimate = UserReqHandler { _, uid -> mapOf("userId" to uid, "length" to 0, "userIntimateList" to empty) } - val getTransferFriend = UserReqHandler { _, uid -> mapOf("userId" to uid, "transferFriendList" to empty) } - val getGameNgMusicId = BaseHandler { mapOf("length" to 0, "musicIdList" to empty) } - val getGameTournamentInfo = BaseHandler { mapOf("length" to 0, "gameTournamentInfoList" to empty) } - val getGameKaleidxScope = BaseHandler { mapOf("gameKaleidxScopeList" to empty) } - val getUserKaleidxScope = UserReqHandler { _, uid -> mapOf("userId" to uid, "userKaleidxScopeList" to empty) } - val getUserNewItem = UserReqHandler { _, uid -> mapOf("userId" to uid, "itemKind" to 0, "itemId" to 0) } - val getUserNewItemList = UserReqHandler { _, uid -> mapOf("userId" to uid, "userItemList" to empty) } - - val getGameSetting = BaseHandler { - // The client-side implementation for reboot time is extremely cursed. - // Only hour and minute are used, date is discarded and second is set to 0. - // The time is adjusted to the next day if it's 12 hours or more from now. - // And it's using local timezone instead of treating it as UTC. - // The official maimai cabs will reboot every day, but we don't want that - // So, we need to return the hour and minute 5 hours ago - // val rebootStart = Instant.now().atZone(ZoneId.of("Asia/Tokyo")).minusHours(5) - // val rebootEnd = rebootStart.plusSeconds(60) - // Nope that didn't work - - mapOf( - "isAouAccession" to true, - "gameSetting" to mapOf( -// "rebootStartTime" to GAME_SETTING_DATE_FMT.format(rebootStart), -// "rebootEndTime" to GAME_SETTING_DATE_FMT.format(rebootEnd), - "rebootStartTime" to "2020-01-01 23:59:00.0", - "rebootEndTime" to "2020-01-01 23:59:00.0", - "rebootInterval" to 0, - - // Fields below doesn't seem to be used by the client at all - "isMaintenance" to false, - "requestInterval" to 10, - "movieUploadLimit" to 0, - "movieStatus" to 0, - "movieServerUri" to "", - "deliverServerUri" to "", - "oldServerUri" to "", - "usbDlServerUri" to "", - - // Fields below are SDGB-specific settings (not present in SDEZ) - "pingDisable" to true, - "packetTimeout" to 20_000, - "packetTimeoutLong" to 60_000, - "packetRetryCount" to 5, - "userDataDlErrTimeout" to 300_000, - "userDataDlErrRetryCount" to 5, - "userDataDlErrSamePacketRetryCount" to 5, - "userDataUpSkipTimeout" to 0, - "userDataUpSkipRetryCount" to 0, - "iconPhotoDisable" to true, - "uploadPhotoDisable" to false, - "maxCountMusic" to 0, - "maxCountItem" to 0 - ) - ) - } - - val getGameWeeklyData = BaseHandler { mapOf( - "gameWeeklyData" to mapOf( - "missionCategory" to 0, - "updateDate" to "2024-01-01 00:00:00.0", - "beforeDate" to "2077-01-01 00:00:00.0" - ) - ) } - - val getUserMissionData = UserReqHandler { _, uid -> mapOf( - "userId" to uid, - "userWeeklyData" to mapOf ( - "lastLoginWeek" to "", - "beforeLoginWeek" to "", - "friendBonusFlag" to false - ), - "userMissionDataList" to empty - ) } - - val getGameMusicScore = BaseHandler { mapOf( - "gameMusicScore" to mapOf( - "musicId" to 0, - "level" to 0, - "type" to 0, - "scoreData" to "" - ) - ) } - - val endpointList = setOf("GetGameEventApi", "GetGameRankingApi", "GetGameSettingApi", "GetGameTournamentInfoApi", "GetGameWeeklyDataApi", - "GetTransferFriendApi", "GetUserActivityApi", "GetUserCardApi", "GetUserCharacterApi", "GetUserDataApi", - "GetUserExtendApi", "GetUserFavoriteApi", "GetUserGhostApi", "GetUserItemApi", "GetUserLoginBonusApi", - "GetUserMapApi", "GetUserMusicApi", "GetUserOptionApi", "GetUserPortraitApi", "GetUserPreviewApi", - "GetUserFriendBonusApi", "GetUserFriendCheckApi", "UserFriendRegistApi", "GetUserMissionDataApi", "GetUserIntimateApi", "GetUserShopStockApi", - "GetUserRatingApi", "GetUserRegionApi", "UploadUserPhotoApi", "UploadUserPlaylogApi", "UploadUserPortraitApi", - "UserLoginApi", "UserLogoutApi", "UpsertUserAllApi", "GetGameChargeApi", "GetUserChargeApi", - "GetUserCourseApi", "GetGameNgMusicIdApi", "GetUserFriendSeasonRankingApi", "CreateTokenApi", - "GetUserRecommendRateMusicApi", "GetUserRecommendSelectMusicApi", "CMGetSellingCardApi", - "CMGetUserCardApi", "CMGetUserCardPrintErrorApi", "CMGetUserCharacterApi", "CMGetUserDataApi", "CMGetUserItemApi", - "CMGetUserPreviewApi", "CMUpsertUserPrintApi", - "CMUpsertUserPrintlogApi", "GetUserFavoriteItemApi", "GetUserRivalDataApi", "GetUserRivalMusicApi", - "GetUserScoreRankingApi", "UpsertClientBookkeepingApi", "UpsertClientSettingApi", - "UpsertClientTestmodeApi", "UpsertClientUploadApi", "Ping", "RemoveTokenApi", "CMLoginApi", "CMLogoutApi", - "CMUpsertBuyCardApi", "GetGameSettingApi", "GetGameKaleidxScopeApi", "GetGameMusicScoreApi", - "GetUserKaleidxScopeApi", "GetUserNewItemApi", "GetUserNewItemListApi").mut - val noopEndpoint = setOf("GetUserScoreRankingApi", "UpsertClientBookkeepingApi", "UpsertClientSettingApi", "UpsertClientTestmodeApi", "UpsertClientUploadApi", "Ping", "RemoveTokenApi", "CMLoginApi", "CMLogoutApi", "CMUpsertBuyCardApi", "UserLogoutApi", "GetGameMapAreaConditionApi", "UpsertUserChargelogApi").also { endpointList.removeAll(it) } - val staticEndpoint = mapOf( - "CreateTokenApi" to """{"Bearer":"meow"}""", - "CMUpsertUserPrintlogApi" to """{"returnCode":1,"orderId":"0","serialId":"FAKECARDIMAG12345678"}""", - ).also { endpointList.removeAll(it.keys.toSet()) } - val members = this::class.declaredMemberProperties - val handlers: Map = endpointList.associateWith { api -> + val handlers: Map = endpointList.associateWith { api -> val name = api.replace("Api", "").lowercase() (members.find { it.name.lowercase() == name } ?: members.find { it.name.lowercase() == name.replace("cm", "") }) - ?.let { it.call(this) as BaseHandler } + ?.let { (it.call(this) as BaseHandler).toSpecial() } + ?: initH[api] ?: initH[api.replace("CM", "")] ?: throw IllegalArgumentException("Mai2: No handler found for $api") } @API("/{api}") - fun handle(@PathVariable api: String, @RequestBody request: Map): Any { - logger.info("Mai2 < $api : ${request.toJson()}") // TODO: Optimize logging - if (api !in noopEndpoint && api !in staticEndpoint && !handlers.containsKey(api)) { + fun handle(@PathVariable api: String, @RequestBody data: Map, req: HttpServletRequest): Any { + logger.info("Mai2 < $api : ${data.toJson()}") // TODO: Optimize logging + if (api !in noopEndpoint && !handlers.containsKey(api)) { logger.warn("Mai2 > $api not found") return """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}""" } @@ -355,16 +85,11 @@ class Maimai2ServletController( return """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}""" } - if (api in staticEndpoint) { - logger.info("Mai2 > $api static") - return staticEndpoint[api]!! - } - return try { Metrics.timer("aquadx_maimai2_api_latency", "api" to api).recordCallable { - handlers[api]!!.handle(request).let { if (it is String) it else it.toJson() }.also { - if (api !in setOf("GetUserItemApi", "GetGameEventApi", "GetUserPortraitApi")) - logger.info("Mai2 > $api : $it") + val ctx = RequestContext(req, data.mut) + serialize(api, handlers[api]!!(ctx)).also { + logger.info("Mai2 > $api : ${it.truncate(1000)}") } } } catch (e: Exception) { @@ -375,7 +100,7 @@ class Maimai2ServletController( if (e is ApiException) { // It's a bad practice to return 200 ok on error, but this is what maimai does so we have to follow - return ResponseEntity.ok().body("""{"returnCode":0,"apiName":"com.sega.maimai2servlet.api.$api","message":"${e.message?.replace("\"", "\\\"")} - ${e.code}"}""") + return mapOf("returnCode" to 0, "apiName" to "com.sega.maimai2servlet.api.$api", "message" to "${e.message} - ${e.code}").toJson() } else throw e } }