mirror of https://github.com/hykilpikonna/AquaDX
[+] Data broker api
parent
3b2199127b
commit
6252cbbefe
|
@ -17,7 +17,6 @@ fun main(args: Array<String>) {
|
|||
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS)
|
||||
when (args.getOrNull(0)) {
|
||||
"futari" -> return MaimaiFutari().start()
|
||||
"data" -> return icu.samnyan.aqua.sega.chusan.main(args)
|
||||
}
|
||||
|
||||
// If data/ is not found, create it
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package icu.samnyan.aqua.net.transfer
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.sega.aimedb.AimeDbClient
|
||||
import icu.samnyan.aqua.sega.aimedb.AimeDbClient.Companion.sendAimePacket
|
||||
import icu.samnyan.aqua.sega.util.AllNetBillingDecoder
|
||||
|
||||
val keychipPattern = Regex("([A-Z\\d]{4}-[A-Z\\d]{11}|[A-Z\\d]{11})")
|
||||
|
||||
class AllNetClient(val dns: String, val keychip: String, val game: String, val version: String, val card: String) {
|
||||
init {
|
||||
// Check if keychip is valid
|
||||
// TODO : Use a more appropriate exception
|
||||
if (!keychipPattern.matches(keychip)) throw Exception("Invalid keychip")
|
||||
}
|
||||
|
||||
override fun toString() = toJson()
|
||||
|
||||
val keychipShort by lazy {
|
||||
// A123-45678901337 -> A1234567890
|
||||
if (keychip.length == 11) keychip
|
||||
else keychip.substring(0, 4) + keychip.substring(5, 11)
|
||||
}
|
||||
val aime by lazy { AimeDbClient(game, keychipShort) }
|
||||
|
||||
// Send AllNet PowerOn request to obtain game URL
|
||||
val gameUrl by lazy {
|
||||
"$dns/sys/servlet/PowerOn".request()
|
||||
.header("Content-Type" to "application/x-www-form-urlencoded")
|
||||
.post(
|
||||
AllNetBillingDecoder.encodeAllNet(mapOf(
|
||||
"game_id" to game,
|
||||
"ver" to version,
|
||||
"serial" to keychipShort,
|
||||
"ip" to "127.0.0.1", "firm_ver" to "60001", "boot_ver" to "0000",
|
||||
"encode" to "UTF-8", "format_ver" to "3", "hops" to "1", "token" to "2864179931"
|
||||
)))
|
||||
.bodyString()
|
||||
.split("&")
|
||||
.map { it.split("=") }
|
||||
.filter { it.size == 2 }
|
||||
.associate { it[0] to it[1] }["uri"]
|
||||
?: throw Exception("PowerOn Failed: No game URL returned")
|
||||
}
|
||||
|
||||
val userId by lazy {
|
||||
when (card.length) {
|
||||
20 -> aime.createReqLookupV2(card)
|
||||
16 -> aime.createReqFelicaLookupV2(card)
|
||||
else -> throw Exception("Invalid card. Please input either 20-digit numeric access code (e.g. 5010000...0) or 16-digit hex Felica ID (e.g. 012E123456789ABC).")
|
||||
}.sendAimePacket(dns.substringAfter("://")).getLongLE(0x20)
|
||||
}
|
||||
|
||||
fun findDataBroker(log: (String) -> Unit) = when (game) {
|
||||
"SDHD" -> ChusanDataBroker(this, log)
|
||||
else -> throw IllegalArgumentException("Unsupported game: $game")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package icu.samnyan.aqua.net.transfer
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.sega.chusan.model.request.UpsertUserAll
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserActivity
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserItem
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail
|
||||
import icu.samnyan.aqua.sega.util.jackson.StringMapper
|
||||
|
||||
|
||||
interface DataBroker {
|
||||
fun pull(): String
|
||||
}
|
||||
|
||||
|
||||
class ChusanDataBroker(val allNet: AllNetClient, val log: (String) -> Unit): DataBroker {
|
||||
val mapper = StringMapper()
|
||||
val url by lazy { "${allNet.gameUrl.ensureEndingSlash()}ChuniServlet" }
|
||||
|
||||
inline fun <reified T> String.get(key: String, data: JDict) = "$url/$this".request()
|
||||
.postZ(mapper.write(data))
|
||||
.bodyMaybeZ()
|
||||
.jsonMap()[key]
|
||||
.let { mapper.convert<T>(it) }
|
||||
.also {
|
||||
if (it is List<*>) log("✅ $this: ${it.size}")
|
||||
else log("✅ $this")
|
||||
}
|
||||
|
||||
class UserMusicWrapper(var userMusicDetailList: List<UserMusicDetail>)
|
||||
|
||||
override fun pull(): String {
|
||||
log("Game URL: ${allNet.gameUrl}")
|
||||
log("User ID: ${allNet.userId}")
|
||||
|
||||
val userId = mapOf("userId" to allNet.userId)
|
||||
val paged = userId + mapOf("nextIndex" to 0, "maxCount" to 10000000)
|
||||
|
||||
return mapper.write(UpsertUserAll().apply {
|
||||
userData = ls("GetUserDataApi".get("userData", userId))
|
||||
userGameOption = ls("GetUserOptionApi".get("userGameOption", userId))
|
||||
userCharacterList = "GetUserCharacterApi".get("userCharacterList", paged)
|
||||
userActivityList = (1..5).flatMap {
|
||||
"GetUserActivityApi".get<List<UserActivity>>("userActivityList", userId + mapOf("kind" to it))
|
||||
}
|
||||
userItemList = (1..12).flatMap {
|
||||
"GetUserItemApi".get<List<UserItem>>(
|
||||
"userItemList",
|
||||
userId + mapOf("nextIndex" to 10000000000 * it, "maxCount" to 10000000)
|
||||
)
|
||||
}
|
||||
userRecentRatingList = "GetUserRecentRatingApi".get("userRecentRatingList", userId)
|
||||
userMusicDetailList = "GetUserMusicApi".get<List<UserMusicWrapper>>("userMusicList", paged)
|
||||
.flatMap { it.userMusicDetailList }
|
||||
userCourseList = "GetUserCourseApi".get("userCourseList", paged)
|
||||
userFavoriteMusicList = "GetUserFavoriteItemApi".get("userFavoriteItemList", paged + mapOf("kind" to 1))
|
||||
// TODO userMapAreaList = "GetUserMapAreaApi"
|
||||
// TODO userNetBattleData = ls("GetUserNetBattleDataApi".get("userNetBattleData", userId))
|
||||
userUnlockChallengeList = "GetUserUCApi".get("userUnlockChallengeList", userId)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package icu.samnyan.aqua.net.transfer
|
||||
|
||||
import ext.API
|
||||
import ext.RB
|
||||
import ext.logger
|
||||
import ext.toJson
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.io.PrintWriter
|
||||
|
||||
@RestController
|
||||
@API("/api/v2/transfer")
|
||||
class TransferApis {
|
||||
val log = logger()
|
||||
|
||||
@API("/check")
|
||||
fun check(@RB allNet: AllNetClient) = try {
|
||||
log.info("Transfer check: $allNet")
|
||||
mapOf("gameUrl" to allNet.gameUrl, "userId" to allNet.userId)
|
||||
} catch (e: Exception) {
|
||||
log.error("Transfer check error", e)
|
||||
mapOf("error" to e.message)
|
||||
}
|
||||
|
||||
fun HttpServletResponse.initStream(): PrintWriter {
|
||||
contentType = "text/event-stream; charset=utf-8"
|
||||
characterEncoding = "UTF-8"
|
||||
return writer
|
||||
}
|
||||
|
||||
fun PrintWriter.sendJson(m: Any) {
|
||||
println("data: ${m.toJson()}\n")
|
||||
flush()
|
||||
}
|
||||
|
||||
fun PrintWriter.log(m: String) = sendJson(mapOf("message" to m))
|
||||
|
||||
@API("/export")
|
||||
fun export(@RB allNet: AllNetClient, response: HttpServletResponse) {
|
||||
val stream = response.initStream()
|
||||
try {
|
||||
log.info("Transfer export: $allNet")
|
||||
stream.log("Starting export...")
|
||||
val broker = allNet.findDataBroker { stream.log(it) }
|
||||
val out = broker.pull()
|
||||
stream.log("Export complete")
|
||||
stream.sendJson(mapOf("data" to out))
|
||||
} catch (e: Exception) {
|
||||
log.error("Transfer export error", e)
|
||||
stream.sendJson(mapOf("error" to e.message))
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
package icu.samnyan.aqua.sega.chusan
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.sega.aimedb.AimeDbClient
|
||||
import icu.samnyan.aqua.sega.aimedb.AimeDbClient.Companion.sendAimePacket
|
||||
import icu.samnyan.aqua.sega.chusan.model.request.UpsertUserAll
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserActivity
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserItem
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail
|
||||
import icu.samnyan.aqua.sega.util.AllNetBillingDecoder
|
||||
import icu.samnyan.aqua.sega.util.jackson.StringMapper
|
||||
import lombok.AllArgsConstructor
|
||||
import lombok.Data
|
||||
import lombok.NoArgsConstructor
|
||||
|
||||
|
||||
val keychipPattern = Regex("([A-Z\\d]{4}-[A-Z\\d]{11}|[A-Z\\d]{11})")
|
||||
|
||||
class AllNetHost(val dns: String, val keychip: String, val game: String, val version: String, val card: String) {
|
||||
init {
|
||||
// Check if keychip is valid
|
||||
// TODO : Use a more appropriate exception
|
||||
if (!keychipPattern.matches(keychip)) throw Exception("Invalid keychip")
|
||||
}
|
||||
|
||||
val keychipShort by lazy {
|
||||
// A123-45678901337 -> A1234567890
|
||||
if (keychip.length == 11) keychip
|
||||
else keychip.substring(0, 4) + keychip.substring(5, 11)
|
||||
}
|
||||
val aime by lazy { AimeDbClient(game, keychipShort) }
|
||||
|
||||
// Send AllNet PowerOn request to obtain game URL
|
||||
val gameUrl by lazy {
|
||||
"$dns/sys/servlet/PowerOn".request()
|
||||
.header("Content-Type" to "application/x-www-form-urlencoded")
|
||||
.post(AllNetBillingDecoder.encodeAllNet(mapOf(
|
||||
"game_id" to game,
|
||||
"ver" to version,
|
||||
"serial" to keychipShort,
|
||||
"ip" to "127.0.0.1", "firm_ver" to "60001", "boot_ver" to "0000",
|
||||
"encode" to "UTF-8", "format_ver" to "3", "hops" to "1", "token" to "2864179931"
|
||||
)))
|
||||
.bodyString()
|
||||
.split("&")
|
||||
.map { it.split("=") }
|
||||
.filter { it.size == 2 }
|
||||
.associate { it[0] to it[1] }["uri"]
|
||||
?: throw Exception("PowerOn Failed: No game URL returned")
|
||||
}
|
||||
|
||||
val userId by lazy {
|
||||
when (card.length) {
|
||||
20 -> aime.createReqLookupV2(card)
|
||||
16 -> aime.createReqFelicaLookupV2(card)
|
||||
else -> throw Exception("Invalid card. Please input either 20-digit numeric access code (e.g. 5010000...0) or 16-digit hex Felica ID (e.g. 012E123456789ABC).")
|
||||
}.sendAimePacket(dns.substringAfter("://")).getLongLE(0x20)
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserMusicWrapper {
|
||||
var userMusicDetailList: List<UserMusicDetail> = emptyList()
|
||||
}
|
||||
|
||||
class ChusanDataBroker(val host: AllNetHost) {
|
||||
val mapper = StringMapper()
|
||||
val url by lazy { "${host.gameUrl.ensureEndingSlash()}ChuniServlet" }
|
||||
val log = logger()
|
||||
|
||||
inline fun <reified T> String.get(key: String, data: JDict) = "$url/$this".request()
|
||||
.postZ(mapper.write(data))
|
||||
.bodyMaybeZ()
|
||||
.jsonMap()[key]
|
||||
.let { mapper.convert<T>(it) }
|
||||
.also {
|
||||
if (it is List<*>) log.info("✅ $this: ${it.size}")
|
||||
else log.info("✅ $this")
|
||||
}
|
||||
|
||||
fun pull(): String {
|
||||
log.info("Game URL: ${host.gameUrl}")
|
||||
log.info("User ID: ${host.userId}")
|
||||
|
||||
val userId = mapOf("userId" to host.userId)
|
||||
val paged = userId + mapOf("nextIndex" to 0, "maxCount" to 10000000)
|
||||
|
||||
return mapper.write(UpsertUserAll().apply {
|
||||
userData = ls("GetUserDataApi".get("userData", userId))
|
||||
userGameOption = ls("GetUserOptionApi".get("userGameOption", userId))
|
||||
userCharacterList = "GetUserCharacterApi".get("userCharacterList", paged)
|
||||
userActivityList = (1..5).flatMap {
|
||||
"GetUserActivityApi".get<List<UserActivity>>("userActivityList", userId + mapOf("kind" to it))
|
||||
}
|
||||
userItemList = (1..12).flatMap {
|
||||
"GetUserItemApi".get<List<UserItem>>(
|
||||
"userItemList",
|
||||
userId + mapOf("nextIndex" to 10000000000 * it, "maxCount" to 10000000)
|
||||
)
|
||||
}
|
||||
userRecentRatingList = "GetUserRecentRatingApi".get("userRecentRatingList", userId)
|
||||
userMusicDetailList = "GetUserMusicApi".get<List<UserMusicWrapper>>("userMusicList", paged)
|
||||
.flatMap { it.userMusicDetailList }
|
||||
userCourseList = "GetUserCourseApi".get("userCourseList", paged)
|
||||
userFavoriteMusicList = "GetUserFavoriteItemApi".get("userFavoriteItemList", paged + mapOf("kind" to 1))
|
||||
// TODO userMapAreaList = "GetUserMapAreaApi"
|
||||
// TODO userNetBattleData = ls("GetUserNetBattleDataApi".get("userNetBattleData", userId))
|
||||
userUnlockChallengeList = "GetUserUCApi".get("userUnlockChallengeList", userId)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue