diff --git a/AquaNet/src/App.svelte b/AquaNet/src/App.svelte index 0d51d2d2..16c01b4b 100644 --- a/AquaNet/src/App.svelte +++ b/AquaNet/src/App.svelte @@ -11,6 +11,7 @@ import { pfp, tooltip } from "./libs/ui" import { ANNOUNCEMENT } from "./libs/config"; import { t } from "./libs/i18n"; + import Transfer from "./pages/Transfer/Transfer.svelte"; console.log(`%c ┏━┓ ┳━┓━┓┏━ @@ -76,6 +77,7 @@ + diff --git a/AquaNet/src/libs/generalTypes.ts b/AquaNet/src/libs/generalTypes.ts index f32b0a04..5dd4cd12 100644 --- a/AquaNet/src/libs/generalTypes.ts +++ b/AquaNet/src/libs/generalTypes.ts @@ -1,3 +1,5 @@ +export type Dict = Record + export interface TrendEntry { date: string rating: number @@ -48,7 +50,7 @@ export interface CardSummary { export interface ConfirmProps { title: string message: string - confirm: () => void + confirm?: () => void cancel?: () => void dangerous?: boolean } diff --git a/AquaNet/src/libs/sdk.ts b/AquaNet/src/libs/sdk.ts index 8b71ef23..d07d0a22 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -8,13 +8,14 @@ import type { TrendEntry, AquaNetUser, GameOption, UserBox, - UserItem + UserItem, + Dict } from './generalTypes' import type { GameName } from './scoring' -interface RequestInitWithParams extends RequestInit { +interface ExtReqInit extends RequestInit { params?: { [index: string]: string } - localCache?: boolean + json?: any } /** @@ -37,188 +38,113 @@ export function reconstructUrl(input: URL | RequestInfo, callback: (url: URL) => /** * Fetch with url parameters */ -export function fetchWithParams(input: URL | RequestInfo, init?: RequestInitWithParams): Promise { +export function fetchWithParams(input: URL | RequestInfo, init?: ExtReqInit): Promise { return fetch(reconstructUrl(input, u => { u.search = new URLSearchParams(init?.params ?? {}).toString() }), init) } -const cache: { [index: string]: any } = {} +/** + * Do something with the response when it's not ok + * + * @param res Response object + */ +async function ensureOk(res: Response) { + if (!res.ok) { + const text = await res.text() + console.error(`${res.status}: ${text}`) -export async function post(endpoint: string, params: Record = {}, init?: RequestInitWithParams): Promise { + // If 400 invalid token is caught, should invalidate the token and redirect to signin + if (text === 'Invalid token') { + localStorage.removeItem('token') + window.location.href = '/' + } + + // Try to parse as json + let json + try { + json = JSON.parse(text) + } catch (e) { + throw new Error(text) + } + if (json.error) throw new Error(json.error) + } +} + +/** + * Post to an endpoint and return the response in JSON while doing error checks + * and handling token (and token expiry) automatically. + * + * @param endpoint The endpoint to post to (e.g., '/pull') + * @param params An object containing the request body or any necessary parameters + * @param init Additional fetch/init configuration + * @returns The JSON response from the server + */ +export async function post(endpoint: string, params: Dict = {}, init?: ExtReqInit): Promise { + return postHelper(endpoint, params, init).then(it => it.json()) +} + +/** + * Actual impl of post(). This does not return JSON but returns response object. + */ +async function postHelper(endpoint: string, params: Dict = {}, init?: ExtReqInit): Promise { // Add token if exists const token = localStorage.getItem('token') if (token && !('token' in params)) params = { ...(params ?? {}), token } - if (init?.localCache) { - const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] - if (cached) return cached + if (init?.json) { + init.body = JSON.stringify(init.json) + init.headers = { 'Content-Type': 'application/json', ...init.headers } + init.json = undefined } - const res = await fetchWithParams(AQUA_HOST + endpoint, { - method: 'POST', - params, - ...init - }).catch(e => { - console.error(e) - throw new Error('Network error') - }) + const res = await fetchWithParams(AQUA_HOST + endpoint, { method: 'POST', params, ...init }) + .catch(e => { console.error(e); throw new Error("Network error") }) + await ensureOk(res) - if (!res.ok) { - const text = await res.text() - console.error(`${res.status}: ${text}`) - - // If 400 invalid token is caught, should invalidate the token and redirect to signin - if (text === 'Invalid token') { - localStorage.removeItem('token') - window.location.href = '/' - } - - // Try to parse as json - let json - try { - json = JSON.parse(text) - } catch (e) { - throw new Error(text) - } - if (json.error) throw new Error(json.error) - } - - const ret = res.json() - cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret - - return ret + return res } -export async function get(endpoint: string, params:any,init?: RequestInitWithParams): Promise { - // Add token if exists - const token = localStorage.getItem('token') - if (token && !('token' in params)) params = { ...(params ?? {}), token } +const decoder = new TextDecoder() - if (init?.localCache) { - const cached = cache[endpoint + JSON.stringify(init)] - if (cached) return cached +/** + * Post with a stream response. Similar to post(), but the response will stream messages to onChunk. + */ +export async function postStream(endpoint: string, params: Dict = {}, onChunk: (data: any) => void, init?: ExtReqInit): Promise { + const res = await postHelper(endpoint, params, init) + if (!res.body) { + console.error('Response body is not a stream') + return } - const res = await fetchWithParams(AQUA_HOST + endpoint, { - method: 'GET', - params, - ...init - }).catch(e => { - console.error(e) - throw new Error('Network error') - }) + // The response body is a ReadableStream. We'll read chunks as they arrive. + const reader = res.body?.getReader() + if (!reader) return + let buffer = '' - if (!res.ok) { - const text = await res.text() - console.error(`${res.status}: ${text}`) + try { + while (true) { + const { done, value } = await reader.read() + if (done) break - // If 400 invalid token is caught, should invalidate the token and redirect to signin - if (text === 'Invalid token') { - localStorage.removeItem('token') - window.location.href = '/' + // Decode any new data, parse full lines, keep the rest in buffer + buffer += decoder.decode(value, { stream: true }) + let fullLines = buffer.split('\n') + buffer = fullLines.pop() ?? '' + + for (const line of fullLines) { + if (!line.trim()) continue // skip empty lines + onChunk(JSON.parse(line)) + } } - // Try to parse as json - let json - try { - json = JSON.parse(text) - } catch (e) { - throw new Error(text) - } - if (json.error) throw new Error(json.error) + // If there's leftover data in 'buffer' after stream ends, parse + if (buffer.trim()) + onChunk(JSON.parse(buffer.trim())) + + } finally { + reader.releaseLock() } - - const ret = res.json() - cache[endpoint + JSON.stringify(init)] = ret - - return ret -} - -export async function put(endpoint: string, params: any, init?: RequestInitWithParams): Promise { - // Add token if exists - const token = localStorage.getItem('token') - if (token && !('token' in params)) params = { ...(params ?? {}), token } - - if (init?.localCache) { - const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] - if (cached) return cached - } - - const res = await fetchWithParams(AQUA_HOST + endpoint, { - method: 'PUT', - body: JSON.stringify(params), - headers:{ - 'Content-Type':'application/json', - ...init?.headers - }, - ...init - }).catch(e => { - console.error(e) - throw new Error('Network error') - }) - - if (!res.ok) { - const text = await res.text() - console.error(`${res.status}: ${text}`) - - // If 400 invalid token is caught, should invalidate the token and redirect to signin - if (text === 'Invalid token') { - localStorage.removeItem('token') - window.location.href = '/' - } - - // Try to parse as json - let json - try { - json = JSON.parse(text) - } catch (e) { - throw new Error(text) - } - if (json.error) throw new Error(json.error) - } - - const ret = res.json() - cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret - - return ret -} - -export async function realPost(endpoint: string, params: any, init?: RequestInitWithParams): Promise { - const res = await fetchWithParams(AQUA_HOST + endpoint, { - method: 'POST', - body: JSON.stringify(params), - headers:{ - 'Content-Type':'application/json', - ...init?.headers - }, - ...init - }).catch(e => { - console.error(e) - throw new Error('Network error') - }) - - if (!res.ok) { - const text = await res.text() - console.error(`${res.status}: ${text}`) - - // If 400 invalid token is caught, should invalidate the token and redirect to signin - if (text === 'Invalid token') { - localStorage.removeItem('token') - window.location.href = '/' - } - - // Try to parse as json - let json - try { - json = JSON.parse(text) - } catch (e) { - throw new Error(text) - } - if (json.error) throw new Error(json.error) - } - - return res.json() } /** @@ -229,6 +155,7 @@ export async function realPost(endpoint: string, params: any, init?: RequestInit async function register(user: { username: string, email: string, password: string, turnstile: string }) { return await post('/api/v2/user/register', user) } + async function login(user: { email: string, password: string, turnstile: string }) { const data = await post('/api/v2/user/login', user) @@ -263,11 +190,11 @@ export const USER = { export const USERBOX = { getProfile: (): Promise<{ user: UserBox, items: UserItem[] }> => - get('/api/v2/game/chu3/user-box', {}), + post('/api/v2/game/chu3/user-box', {}), setUserBox: (d: { field: string, value: number | string }) => post(`/api/v2/game/chu3/user-detail-set`, d), getUserProfile: (username: string): Promise => - get(`/api/v2/game/chu3/user-detail`, {username}) + post(`/api/v2/game/chu3/user-detail`, {username}) } export const CARD = { @@ -295,9 +222,9 @@ export const GAME = { export: (game: GameName): Promise> => post(`/api/v2/game/${game}/export`), import: (game: GameName, data: any): Promise> => - post(`/api/v2/game/${game}/import`, {}, { body: JSON.stringify(data) }), + post(`/api/v2/game/${game}/import`, {}, { json: data }), importMusicDetail: (game: GameName, data: any): Promise> => - post(`/api/v2/game/${game}/import-music-detail`, {}, {body: JSON.stringify(data), headers: {'Content-Type': 'application/json'}}), + post(`/api/v2/game/${game}/import-music-detail`, {}, { json: data }), setRival: (game: GameName, rivalUserName: string, isAdd: boolean) => post(`/api/v2/game/${game}/set-rival`, { rivalUserName, isAdd }), } @@ -317,3 +244,12 @@ export const SETTING = { detailSet: (game: string, field: string, value: any) => post(`/api/v2/game/${game}/user-detail-set`, { field, value }), } + +export const TRANSFER = { + check: (d: AllNetClient): Promise => + post('/api/v2/transfer/check', {}, { json: d }), + pull: (d: AllNetClient, callback: (data: TrStreamMessage) => void) => + postStream('/api/v2/transfer/pull', {}, callback, { json: d }), + push: (d: AllNetClient, data: string) => + post('/api/v2/transfer/push', {}, { json: { client: d, data } }), +} diff --git a/AquaNet/src/libs/ui.ts b/AquaNet/src/libs/ui.ts index fe2b4ca9..8eabcc82 100644 --- a/AquaNet/src/libs/ui.ts +++ b/AquaNet/src/libs/ui.ts @@ -212,3 +212,53 @@ export function pfp(node: HTMLImageElement, me?: AquaNetUser) { node.src = me?.profilePicture ? `${AQUA_HOST}/uploads/net/portrait/${me.profilePicture}` : DEFAULT_PFP node.onerror = e => pfpNotFound(e as Event) } + +export function download(data: string, filename: string) { + const blob = new Blob([data]); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); +} + +export async function selectJsonFile(): Promise { + return new Promise((resolve, reject) => { + // Create a hidden file input element + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json,application/json'; + input.style.display = 'none'; + + // Listen for when the user selects a file + input.addEventListener('change', (event: Event) => { + const target = event.target as HTMLInputElement; + if (!target.files || target.files.length === 0) { + return reject(new Error("No file selected")); + } + const file = target.files[0]; + const reader = new FileReader(); + + reader.onload = () => { + try { + const jsonData = JSON.parse(reader.result as string); + resolve(jsonData); + } catch (error) { + reject(new Error("Error parsing JSON: " + error)); + } + }; + + reader.onerror = () => { + reject(new Error("Error reading file")); + }; + + reader.readAsText(file); + }); + + // Append the input to the DOM, trigger click, and then remove it + document.body.appendChild(input); + input.click(); + document.body.removeChild(input); + }); +} + diff --git a/AquaNet/src/pages/Transfer/InputTextShort.svelte b/AquaNet/src/pages/Transfer/InputTextShort.svelte new file mode 100644 index 00000000..ba4930af --- /dev/null +++ b/AquaNet/src/pages/Transfer/InputTextShort.svelte @@ -0,0 +1,26 @@ + + +
+ + +
+ + diff --git a/AquaNet/src/pages/Transfer/Transfer.svelte b/AquaNet/src/pages/Transfer/Transfer.svelte new file mode 100644 index 00000000..29b7d8b5 --- /dev/null +++ b/AquaNet/src/pages/Transfer/Transfer.svelte @@ -0,0 +1,129 @@ + + + + +
+
+

🏳️‍⚧️ AquaTrans™ Data Transfer?

+ +
+ +
+

👋 Welcome to the AquaTrans™ server data transfer tool!

+

You can use this to export data from any server, and input data into any server using the connection credentials (card number, server address, and keychip id).

+

This tool will simulate a game client and pull your data from the source server, and push your data to the destination server.

+

Please fill out the info below to get started!

+
+ + + +
+ arrow +
+ + +
+ + + + + diff --git a/AquaNet/src/pages/Transfer/TransferLib.ts b/AquaNet/src/pages/Transfer/TransferLib.ts new file mode 100644 index 00000000..41e5dbbd --- /dev/null +++ b/AquaNet/src/pages/Transfer/TransferLib.ts @@ -0,0 +1,21 @@ + +interface AllNetSrc { + card: string + dns: string + keychip: string +} + +interface AllNetGame { + game: string + version: string +} + +interface AllNetClient extends AllNetSrc, AllNetGame {} + +interface TrCheckGood { + gameUrl: string + userId: number +} + +type TrStreamMessage = { message: string } | { error: string } | { data: string } + diff --git a/AquaNet/src/pages/Transfer/TransferServer.svelte b/AquaNet/src/pages/Transfer/TransferServer.svelte new file mode 100644 index 00000000..cef20607 --- /dev/null +++ b/AquaNet/src/pages/Transfer/TransferServer.svelte @@ -0,0 +1,198 @@ + + + + +
+

{isSrc ? "Source" : "Target"} Server

+ + {#if expectedError} +
{expectedError}
+ {/if} + + +
+ /^https?:\/\/[a-z0-9.-]+(:\d+)?$/i.test(v)} disabled={tested} /> + /^([A-Z0-9]{11}|[A-Z0-9]{4}-[A-Z0-9]{11})$/.test(v)} disabled={tested} /> +
+ + +
+
+ + +
+ +
+ + + {#if messages.length > 0} +
+ {#each messages.slice(Math.max(messages.length - 5, 0), undefined) as msg} +

{msg}

+ {/each} +
+ {/if} + + +
+ {#if !tested} + + {:else} + + + {/if} +
+
+ + diff --git a/build.gradle.kts b/build.gradle.kts index 41d937d7..510a4674 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter plugins { - val ktVer = "2.1.0" + val ktVer = "2.1.10" java kotlin("plugin.lombok") version ktVer @@ -117,6 +117,10 @@ springBoot { mainClass.set("icu.samnyan.aqua.EntryKt") } +application { + mainClass = "icu.samnyan.aqua.EntryKt" +} + hibernate { enhancement { enableLazyInitialization = true diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 83522f9d..057eec5c 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -14,7 +14,7 @@ and change `allnet.server.host` to your LAN IP address (e.g. 192.168.0.?). You c > [!NOTE] > The guide above will create a new MariaDB database. -> If you were using SQLite Aqua before, it is not supported in AquaDX. Please export your data and import it to MariaDB. +> If you were using SQLite Aqua before, it is not supported in AquaDX. Please export your data and import it to your new instance. > If you were using MySQL Aqua before, you can migrate to MariaDB using [this guide here](docs/mysql_to_mariadb.md). ### Configuration @@ -42,3 +42,9 @@ docker compose up ### Building You need to install JDK 21 on your system, then run `./gradlew clean build`. The jar file will be built into the `build/libs` folder. + +## Why drop SQLite support? + +If you wonder why I dropped SQLite support, ask SQLite devs why they still haven't supported adding a single constraint to a table without all the hassle of creating a new one and migrating all data over and finally deleting the original. + +![](sqlite-sucks.png) \ No newline at end of file diff --git a/docs/sqlite-sucks.png b/docs/sqlite-sucks.png new file mode 100644 index 00000000..0d32dcd8 Binary files /dev/null and b/docs/sqlite-sucks.png differ diff --git a/src/main/java/ext/Ext.kt b/src/main/java/ext/Ext.kt index c7278dcc..9f6e1ca2 100644 --- a/src/main/java/ext/Ext.kt +++ b/src/main/java/ext/Ext.kt @@ -192,8 +192,7 @@ val Any?.str get() = toString() // Collections fun ls(vararg args: T) = args.toList() inline fun arr(vararg args: T) = arrayOf(*args) -operator fun Map.plus(map: Map) = - (if (this is MutableMap) this else mut).apply { putAll(map) } +operator fun Map.plus(map: Map) = mut.apply { putAll(map) } operator fun MutableMap.plusAssign(map: Map) { putAll(map) } fun Map.vNotNull(): Map = filterValues { it != null }.mapValues { it.value!! } fun MutableList.popAll(list: List) = list.also { removeAll(it) } @@ -238,6 +237,7 @@ fun Str.path() = Path.of(this) operator fun Path.div(part: Str) = resolve(part) operator fun File.div(fileName: Str) = File(this, fileName) fun Str.ensureEndingSlash() = if (endsWith('/')) this else "$this/" +fun Str.ensureNoEndingSlash() = if (endsWith('/')) dropLast(1) else this fun T.logger() = LoggerFactory.getLogger(this::class.java) diff --git a/src/main/java/ext/Http.kt b/src/main/java/ext/Http.kt new file mode 100644 index 00000000..d0c73fe7 --- /dev/null +++ b/src/main/java/ext/Http.kt @@ -0,0 +1,34 @@ +package ext + +import icu.samnyan.aqua.sega.util.ZLib +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +val client = HttpClient.newBuilder().build() + +fun HttpRequest.Builder.send() = client.send(this.build(), HttpResponse.BodyHandlers.ofByteArray()) +fun HttpRequest.Builder.header(pair: Pair) = this.header(pair.first.toString(), pair.second.toString()) +fun String.request() = HttpRequest.newBuilder(URI.create(this)) + +fun HttpRequest.Builder.post(body: Any? = null) = this.POST(when (body) { + is ByteArray -> HttpRequest.BodyPublishers.ofByteArray(body) + is String -> HttpRequest.BodyPublishers.ofString(body) + is HttpRequest.BodyPublisher -> body + else -> throw Exception("Unsupported body type") +}).send() + + +inline fun HttpResponse.json(): T? = body()?.json() + +fun HttpRequest.Builder.postZ(body: String) = run { + header("Content-Type" to "application/json") + header("Content-Encoding" to "deflate") + post(ZLib.compress(body.toByteArray())) +} + +fun HttpResponse.header(key: String) = headers().firstValue(key).orElse(null) +fun HttpResponse.bodyString() = body()?.toString(Charsets.UTF_8) +fun HttpResponse.bodyZ() = body()?.let { ZLib.decompress(it)?.decodeToString() } +fun HttpResponse.bodyMaybeZ() = if (header("Content-Encoding") == "deflate") bodyZ() else bodyString() diff --git a/src/main/java/ext/Json.kt b/src/main/java/ext/Json.kt index 964b3b38..61cd6545 100644 --- a/src/main/java/ext/Json.kt +++ b/src/main/java/ext/Json.kt @@ -43,16 +43,18 @@ else JACKSON.readValue(this, cls) fun T.toJson() = JACKSON.writeValueAsString(this) inline fun String.json() = try { - JACKSON.readValue(this, T::class.java) + if (isEmpty() || this == "null") null + else JACKSON.readValue(this, T::class.java) } catch (e: Exception) { println("Failed to parse JSON: $this") throw e } -fun String.jsonMap(): Map = json() -fun String.jsonArray(): List> = json() - +fun String.jsonMap(): Map = json() ?: emptyMap() +fun String.jsonArray(): List> = json() ?: emptyList() +fun String.jsonMaybeMap(): Map? = json() +fun String.jsonMaybeArray(): List>? = json() // KotlinX Serialization @OptIn(ExperimentalSerializationApi::class) diff --git a/src/main/java/icu/samnyan/aqua/Entry.kt b/src/main/java/icu/samnyan/aqua/Entry.kt index 1c09881d..d08cccee 100644 --- a/src/main/java/icu/samnyan/aqua/Entry.kt +++ b/src/main/java/icu/samnyan/aqua/Entry.kt @@ -1,9 +1,9 @@ package icu.samnyan.aqua import icu.samnyan.aqua.sega.aimedb.AimeDbServer -import icu.samnyan.aqua.sega.maimai2.worldslink.MaimaiFutari import icu.samnyan.aqua.spring.AutoChecker import org.springframework.boot.SpringApplication +import org.springframework.boot.ansi.AnsiOutput import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.scheduling.annotation.EnableScheduling import java.io.File @@ -13,10 +13,7 @@ import java.io.File class Entry fun main(args: Array) { - if (args.getOrNull(0) == "futari") { - // Run futari main - return MaimaiFutari().start() - } + AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS) // If data/ is not found, create it File("data").mkdirs() diff --git a/src/main/java/icu/samnyan/aqua/api/model/resp/sega/chuni/v2/external/Chu3DataExport.kt b/src/main/java/icu/samnyan/aqua/api/model/resp/sega/chuni/v2/external/Chu3DataExport.kt index 6a88ae0e..cdde0ac4 100644 --- a/src/main/java/icu/samnyan/aqua/api/model/resp/sega/chuni/v2/external/Chu3DataExport.kt +++ b/src/main/java/icu/samnyan/aqua/api/model/resp/sega/chuni/v2/external/Chu3DataExport.kt @@ -7,12 +7,12 @@ data class Chu3DataExport( override var gameId: String = "SDHD", override var userData: Chu3UserData, var userGameOption: UserGameOption, - var userActivityList: List, + var userActivityList: List, var userCharacterList: List, var userChargeList: List, var userCourseList: List, var userDuelList: List, - var userItemList: List, + var userItemList: List, var userMapList: List, var userMusicDetailList: List, var userPlaylogList: List, diff --git a/src/main/java/icu/samnyan/aqua/api/model/resp/sega/chuni/v2/external/ChuniDataImport.java b/src/main/java/icu/samnyan/aqua/api/model/resp/sega/chuni/v2/external/ChuniDataImport.java index dfe2db53..83e7b21f 100644 --- a/src/main/java/icu/samnyan/aqua/api/model/resp/sega/chuni/v2/external/ChuniDataImport.java +++ b/src/main/java/icu/samnyan/aqua/api/model/resp/sega/chuni/v2/external/ChuniDataImport.java @@ -17,13 +17,13 @@ import java.util.List; public class ChuniDataImport { private String gameId; private ExternalUserData userData; - private List userActivityList; + private List userActivityList; private List userCharacterList; private List userChargeList; private List userCourseList; private List userDuelList; private UserGameOption userGameOption; - private List userItemList; + private List userItemList; private List userMapList; private List userMusicDetailList; private List userPlaylogList; diff --git a/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt b/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt index 29a20edf..107f78ce 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/chu3/Chu3Import.kt @@ -31,10 +31,10 @@ class Chu3Import( artemisRenames = mapOf( "chuni_item_character" to ImportClass(UserCharacter::class), "chuni_item_duel" to ImportClass(UserDuel::class), - "chuni_item_item" to ImportClass(UserItem::class, mapOf("isValid" to "valid")), + "chuni_item_item" to ImportClass(Chu3UserItem::class, mapOf("isValid" to "valid")), // "chuni_item_login_bonus" to ImportClass(UserLoginBonus::class, mapOf("isWatched" to "watched")), "chuni_item_map_area" to ImportClass(UserMap::class), - "chuni_profile_activity" to ImportClass(UserActivity::class, mapOf("activityId" to "id")), + "chuni_profile_activity" to ImportClass(Chu3UserActivity::class, mapOf("activityId" to "id")), "chuni_profile_charge" to ImportClass(UserCharge::class), "chuni_profile_data" to ImportClass(Chu3UserData::class, mapOf("user" to null, "version" to null, "isNetMember" to null)), "chuni_profile_option" to ImportClass(UserGameOption::class, mapOf("version" to null)), diff --git a/src/main/java/icu/samnyan/aqua/net/transfer/AllNetClient.kt b/src/main/java/icu/samnyan/aqua/net/transfer/AllNetClient.kt new file mode 100644 index 00000000..ad4d5559 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/transfer/AllNetClient.kt @@ -0,0 +1,56 @@ +package icu.samnyan.aqua.net.transfer + +import ext.header +import ext.post +import ext.request +import icu.samnyan.aqua.sega.aimedb.AimeDbClient +import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder +import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder.decodeAllNetResp + +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() = "AllNetClient($dns, $keychip, $game, $version, $card)" + + val keychipShort by lazy { + // A123-45678901337 -> A1234567890 + if (keychip.length == 11) keychip + else keychip.substring(0, 4) + keychip.substring(5, 12) + } + val aime by lazy { AimeDbClient(game, keychipShort, dns.substringAfter("://").substringBefore(":").substringBefore("/")) } + + // Send AllNet PowerOn request to obtain game URL + val gameUrl by lazy { + "$dns/sys/servlet/PowerOn".request() + .header("User-Agent" to "AquaTrans/1.0") + .header("Content-Type" to "application/x-www-form-urlencoded") + .header("Pragma" to "DFI") + .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" + ))) + ?.also { + println(it) + } + ?.decodeAllNetResp()?.get("uri") + ?: throw Exception("PowerOn Failed: No game URL returned") + } + + val userId by lazy { aime.execLookupOrRegister(card) } + + fun findDataBroker(log: (String) -> Unit) = when (game) { + "SDHD" -> ChusanDataBroker(this, log) + "SDEZ", "SDGA" -> MaimaiDataBroker(this, log) + "SDDT" -> OngekiDataBroker(this, log) + else -> throw IllegalArgumentException("Unsupported game: $game") + } +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/transfer/DataBroker.kt b/src/main/java/icu/samnyan/aqua/net/transfer/DataBroker.kt new file mode 100644 index 00000000..87564ba9 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/transfer/DataBroker.kt @@ -0,0 +1,169 @@ +package icu.samnyan.aqua.net.transfer + +import ext.* +import icu.samnyan.aqua.sega.chusan.model.request.Chu3UserAll +import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserActivity +import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserItem +import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail +import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UserAll +import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserFavorite +import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserItem +import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserMusicDetail +import icu.samnyan.aqua.sega.ongeki.model.request.UpsertUserAll +import icu.samnyan.aqua.sega.ongeki.model.userdata.UserItem +import icu.samnyan.aqua.sega.util.jackson.BasicMapper +import icu.samnyan.aqua.sega.util.jackson.IMapper +import icu.samnyan.aqua.sega.util.jackson.StringMapper + + +abstract class DataBroker( + val allNet: AllNetClient, + val log: (String) -> Unit, +) { + abstract val mapper: IMapper + abstract val url: String + + inline fun String.getNullable(key: String, data: JDict): T? = "$url/$this".request() + .postZ(mapper.write(data)) + .bodyMaybeZ() + ?.jsonMaybeMap()?.get(key) + ?.let { mapper.convert(it) } + ?.also { + if (it is List<*>) log("✅ $this: ${it.size}") + else log("✅ $this") + } + + inline fun String.get(key: String, data: JDict): T = getNullable(key, data) ?: run { + log("❌ $this") + if (this == "GetUserDataApi") 404 - "Failed to get $this (User not found?)" + else 417 - "Failed to get $this" + } + + fun prePull(): Pair, MutableMap> { + 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 userId to paged + } + + abstract fun pull(): String + fun push(data: String) { + log("Pushing data") + "$url/UpsertUserAllApi".request().postZ(mapper.write(mapOf( + "userId" to allNet.userId, + "upsertUserAll" to data.jsonMap() + ))).bodyMaybeZ()?.also { log(it) } + } +} + + +class ChusanDataBroker(allNet: AllNetClient, log: (String) -> Unit): DataBroker(allNet, log) { + override val mapper = StringMapper() + override val url by lazy { "${allNet.gameUrl.ensureEndingSlash()}ChuniServlet" } + + class UserMusicWrapper(var userMusicDetailList: List) + + override fun pull(): String { + val (userId, paged) = prePull() + + return mapper.write(Chu3UserAll().apply { + userData = ls("GetUserDataApi".get("userData", userId)) + userGameOption = ls("GetUserOptionApi".get("userGameOption", userId)) + userCharacterList = "GetUserCharacterApi".get("userCharacterList", paged) + userActivityList = (1..5).flatMap { + "GetUserActivityApi".get>("userActivityList", userId + mapOf("kind" to it)) + } + userItemList = (1..12).flatMap { + "GetUserItemApi".get>("userItemList", paged + mapOf("nextIndex" to 10000000000 * it)) + } + userRecentRatingList = "GetUserRecentRatingApi".get("userRecentRatingList", userId) + userMusicDetailList = "GetUserMusicApi".get>("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) + }) + } +} + + +class MaimaiDataBroker(allNet: AllNetClient, log: (String) -> Unit): DataBroker(allNet, log) { + override val mapper = BasicMapper() + override val url by lazy { "${allNet.gameUrl.ensureEndingSlash()}Maimai2Servlet" } + + class UserMusicWrapper(var userMusicDetailList: List) + + override fun pull(): String { + val (userId, paged) = prePull() + + return Mai2UserAll().apply { + userData = ls("GetUserDataApi".get("userData", userId)) + userOption = ls("GetUserOptionApi".get("userOption", userId)) + userExtend = ls("GetUserExtendApi".get("userExtend", userId)) + userRatingList = ls("GetUserRatingApi".get("userRating", userId)) + userActivityList = ls("GetUserActivityApi".get("userActivity", userId)) + + userMusicDetailList = "GetUserMusicApi".get>("userMusicList", paged) + .flatMap { it.userMusicDetailList } + userFriendSeasonRankingList = "GetUserFriendSeasonRankingApi".get("userFriendSeasonRankingList", paged) + userCharacterList = "GetUserCharacterApi".get("userCharacterList", paged) + userItemList = (1..12).flatMap { + "GetUserItemApi".get>("userItemList", paged + mapOf("nextIndex" to 10000000000 * it)) + } + userCourseList = "GetUserCourseApi".get("userCourseList", paged) + userFavoriteList = (1..5).mapNotNull { + "GetUserFavoriteApi".getNullable("userFavorite", userId + mapOf("itemKind" to it)) + } + userGhost = "GetUserGhostApi".get("userGhostList", userId) + userMapList = "GetUserMapApi".get("userMapList", paged) + userLoginBonusList = "GetUserLoginBonusApi".get("userLoginBonusList", userId) + + // TODO: userFavoriteMusicList + }.toJson() + } +} + +class OngekiDataBroker(allNet: AllNetClient, log: (String) -> Unit): DataBroker(allNet, log) { + override val mapper = BasicMapper() + override val url by lazy { allNet.gameUrl.ensureNoEndingSlash() } + + override fun pull(): String { + val (userId, paged) = prePull() + + return UpsertUserAll().apply { + userData = ls("GetUserDataApi".get("userData", userId)) + userOption = ls("GetUserOptionApi".get("userOption", userId)) + userMusicItemList = "GetUserMusicItemApi".get("userMusicItemList", paged) + userBossList = "GetUserBossApi".get("userBossList", userId) + userMusicDetailList = "GetUserMusicApi".get("userMusicList", paged) + userTechCountList = "GetUserTechCountApi".get("userTechCountList", userId) + userCardList = "GetUserCardApi".get("userCardList", paged) + userCharacterList = "GetUserCharacterApi".get("userCharacterList", paged) + userStoryList = "GetUserStoryApi".get("userStoryList", userId) + userChapterList = "GetUserChapterApi".get("userChapterList", userId) + userMemoryChapterList = "GetUserMemoryChapterApi".get("userMemoryChapterList", userId) + userDeckList = "GetUserDeckByKeyApi".get("userDeckList", userId + mapOf("authKey" to "")) + userTrainingRoomList = "GetUserTrainingRoomByKeyApi".get("userTrainingRoomList", userId + mapOf("authKey" to "")) + userActivityList = "GetUserActivityApi".get("userActivityList", userId + mapOf("kind" to 1)) + userRatinglogList = "GetUserRatinglogApi".get("userRatinglogList", userId) + userRecentRatingList = "GetUserRecentRatingApi".get("userRecentRatingList", userId) + userItemList = ls(2, 3, 4, 8, 9, 11, 12, 13, 14, 15, 16, 17, 19, 20).flatMap { + "GetUserItemApi".get>("userItemList", paged + mapOf("nextIndex" to 10000000000 * it)) + } + userEventPointList = "GetUserEventPointApi".get("userEventPointList", userId) + userMissionPointList = "GetUserMissionPointApi".get("userMissionPointList", userId) + userLoginBonusList = "GetUserLoginBonusApi".get("userLoginBonusList", userId) + userScenarioList = "GetUserScenarioApi".get("userScenarioList", userId) + userTradeItemList = "GetUserTradeItemApi".get("userTradeItemList", userId + mapOf("startChapterId" to 0, "endChapterId" to 99999)) + userEventMusicList = "GetUserEventMusicApi".get("userEventMusicList", userId) + userTechEventList = "GetUserTechEventRankingApi".get("userTechEventRankingList", userId) + userKopList = "GetUserKopApi".get("userKopList", userId) + }.toJson() + } +} + +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/transfer/TransferApis.kt b/src/main/java/icu/samnyan/aqua/net/transfer/TransferApis.kt new file mode 100644 index 00000000..b1f4dd02 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/transfer/TransferApis.kt @@ -0,0 +1,65 @@ +package icu.samnyan.aqua.net.transfer + +import ext.* +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) { + 400 - "Transfer check failed. Please check your host and keychip.\n${e.message}" + } + + fun HttpServletResponse.initStream(): PrintWriter { + contentType = "text/event-stream; charset=utf-8" + characterEncoding = "UTF-8" + return writer + } + + fun PrintWriter.sendJson(m: Any) { + println(m.toJson()) + flush() + } + + fun PrintWriter.log(m: String) = sendJson(mapOf("message" to m)) + + @API("/pull") + fun pull(@RB allNet: AllNetClient, response: HttpServletResponse) { + val stream = response.initStream() + try { + log.info("Transfer pull: $allNet") + stream.log("Starting pull...") + val broker = allNet.findDataBroker { stream.log(it) } + val out = broker.pull() + stream.log("Pull complete") + stream.sendJson(mapOf("data" to out)) + } catch (e: Exception) { + log.error("Transfer pull error", e) + stream.sendJson(mapOf("error" to e.message)) + } finally { + stream.close() + } + } + + class PushReq(val client: AllNetClient, val data: String) + + @API("/push") + fun push(@RB obj: PushReq) = try { + val allNet = obj.client + log.info("Transfer push: $allNet") + val broker = allNet.findDataBroker { log.info(it) } + broker.push(obj.data) + mapOf("status" to "ok") + } catch (e: Exception) { + log.error("Transfer push error", e) + 400 - "Transfer push error: ${e.message}" + } +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/aimedb/AimeDB.kt b/src/main/java/icu/samnyan/aqua/sega/aimedb/AimeDB.kt index 1dded556..887138a2 100644 --- a/src/main/java/icu/samnyan/aqua/sega/aimedb/AimeDB.kt +++ b/src/main/java/icu/samnyan/aqua/sega/aimedb/AimeDB.kt @@ -12,10 +12,8 @@ import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandler import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInboundHandlerAdapter -import org.slf4j.Logger -import org.slf4j.LoggerFactory import org.springframework.stereotype.Component -import java.nio.charset.StandardCharsets +import java.nio.charset.StandardCharsets.US_ASCII import java.time.LocalDateTime import kotlin.jvm.optionals.getOrNull @@ -31,13 +29,24 @@ class AimeDB( ): ChannelInboundHandlerAdapter() { val logger = logger() - data class AimeBaseInfo(val gameId: String, val keychipId: String) - - fun getBaseInfo(input: ByteBuf) = AimeBaseInfo( - gameId = input.toString(0x0a, 0x0e - 0x0a, StandardCharsets.US_ASCII), - keychipId = input.toString(0x14, 0x1f - 0x14, StandardCharsets.US_ASCII) + data class AimeBaseInfo( + val magic: UInt, val version: UInt, val responseCode: UInt, val length: UInt, + val status: UInt, val gameId: String, val storeId: UInt, val keychipId: String ) + fun ByteBuf.decodeHeader() = AimeBaseInfo( + magic = readShortLE().toUInt(), // 00 2b + version = readShortLE().toUInt(), // 02 2b + responseCode = readShortLE().toUInt(), // 04 2b + length = readShortLE().toUInt(), // 06 2b + status = readShortLE().toUInt(), // 08 2b + gameId = readPaddedString(6u), // 0a 6b + storeId = readIntLE().toUInt(), // 10 4b + keychipId = readPaddedString(12u) // 14 12b + ) + + fun ByteBuf.readPaddedString(maxLen: UInt) = readBytes(maxLen.toInt()).toString(US_ASCII).trimEnd('\u0000') + data class Handler(val name: String, val fn: (ByteBuf) -> ByteBuf?) final val handlers = mapOf( @@ -62,10 +71,10 @@ class AimeDB( try { val type = msg["type"] as Int val data = msg["data"] as ByteBuf - val base = getBaseInfo(data) + val base = data.decodeHeader() val handler = handlers[type] ?: return logger.error("AimeDB: Unknown request type 0x${type.toString(16)}") - logger.info("AimeDB /${handler.name} : (game ${base.gameId}, keychip ${base.keychipId})") + logger.info("AimeDB /${handler.name} : $base") // Check keychip // We do not check for type 0x13 because of a bug in duolinguo.dll @@ -127,8 +136,7 @@ class AimeDB( */ fun doFelicaLookupV2(msg: ByteBuf): ByteBuf { val idm = msg.slice(0x30, 0x38 - 0x30).getLong(0) - val dfc = msg.slice(0x38, 0x40 - 0x38).getLong(0) - logger.info("> Felica Lookup v2 (idm $idm, dfc $dfc)") + logger.info("> Felica Lookup v2 (idm $idm)") // Get the decimal represent of the hex value, same from minime val accessCode = idm.toString().replace("-", "").padStart(20, '0') diff --git a/src/main/java/icu/samnyan/aqua/sega/aimedb/AimeDbClient.kt b/src/main/java/icu/samnyan/aqua/sega/aimedb/AimeDbClient.kt new file mode 100644 index 00000000..28c55704 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/aimedb/AimeDbClient.kt @@ -0,0 +1,97 @@ +package icu.samnyan.aqua.sega.aimedb + +import ext.minus +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufUtil +import io.netty.buffer.Unpooled +import java.net.Socket +import java.net.SocketTimeoutException + +class AimeDbClient(val gameId: String, val keychipShort: String, val server: String) { + // https://sega.bsnk.me/allnet/aimedb/common/#packet-header + fun createRequest(type: UShort, writer: ByteBuf.() -> Unit) = + AimeDbEncryption.encrypt(Unpooled.buffer(1024).clear().run { + writeShortLE(0xa13e) // 00 2b: Magic + writeShortLE(0x3087) // 02 2b: Version + writeShortLE(type.toInt()) // 04 2b: Type + writeShortLE(0) // 06 2b: Length + writeShortLE(0) // 08 2b: Result + writeAscii(gameId, 6) // 0A 6b: Game ID + writeIntLE(299) // 10 4b: Store ID (Place ID) + writeAscii(keychipShort, 12) // 14 12b: Keychip ID + writer() // Write Payload + setShortLE(6, writerIndex()) // Update Length + copy(0, writerIndex()) // Trim unused bytes + }) + + private fun ByteBuf.writeAscii(value: String, length: Int) = + writeBytes(value.toByteArray(Charsets.US_ASCII).copyOf(length)) + + fun createReqLookupV2(accessCode: String) = + createRequest(0x0fu) { + // Access code is a 20-digit number, should be converted to a 10-byte array + writeBytes(ByteBufUtil.decodeHexDump(accessCode.padStart(20, '0'))) + writeByte(0) // 0A 1b: Company code + writeByte(0) // 0B 1b: R/W Firmware version + writeIntLE(0) // 0C 4b: Serial number + } + + fun createReqFelicaLookupV2(felicaIdm: String) = + createRequest(0x11u) { + writeBytes(ByteArray(16)) // 00 16b: Random Challenge + // 10 8b: Felica IDm + writeBytes(ByteBufUtil.decodeHexDump(felicaIdm.padStart(16, '0'))) + writeBytes(ByteArray(8)) // 18 8b: Felica PMm + writeBytes(ByteArray(16)) // 20 16b: Card key version + writeBytes(ByteArray(16)) // 30 16b: Write count + writeBytes(ByteArray(8)) // 40 8b: MACA + writeByte(0) // 48 1b: Company code + writeByte(0) // 49 1b: R/W Firmware version + writeShortLE(0) // 4A 2b: DFC + writeIntLE(0) // 4C 4b: Unknown padding + } + + fun createReqRegister(accessCode: String) = + createRequest(0x05u) { + // Access code is a 20-digit number, should be converted to a 10-byte array + writeBytes(ByteBufUtil.decodeHexDump(accessCode.padStart(20, '0'))) + writeByte(0) // 0A 1b: Company code + writeByte(0) // 0B 1b: R/W Firmware version + writeIntLE(0) // 0C 4b: Serial number + } + + fun send(buf: ByteBuf): ByteBuf = Socket(server, 22345).use { + it.soTimeout = 3000 + it.getOutputStream().write(buf.array()) + it.getInputStream().use { r -> + Unpooled.buffer().apply { + val buffer = ByteArray(1024) + try { + while (r.read(buffer) != -1) writeBytes(buffer) + } catch (_: SocketTimeoutException) { } + } + } + }.let { AimeDbEncryption.decrypt(it) } + + fun execLookup(card: String) = + send(when (card.length) { + 20 -> createReqLookupV2(card) + 16 -> createReqFelicaLookupV2(card) + else -> 400 - "Invalid card. Please input either 20-digit numeric access code (e.g. 5010000...0) or 16-digit hex Felica ID (e.g. 012E123456789ABC)." + }).getUnsignedIntLE(0x20).let { + if (it == 0xffffffff) -1L else it + } + + fun execRegister(card: String) = + when (card.length) { + 20 -> card + 16 -> ByteBufUtil.hexDump(send(createReqFelicaLookupV2(card)).slice(0x2c, 10)) + else -> 400 - "Invalid card. Please input a 20-digit numeric access code (e.g. 5010000...0)." + }.let { send(createReqRegister(it)).getUnsignedIntLE(0x20) } + + fun execLookupOrRegister(card: String) = + execLookup(card).let { + if (it == -1L) execRegister(card) + else it + } +} diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt index c243c320..25400285 100644 --- a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt @@ -2,7 +2,7 @@ package icu.samnyan.aqua.sega.allnet import ext.* import icu.samnyan.aqua.net.db.AquaNetUserRepo -import icu.samnyan.aqua.sega.util.AllNetBillingDecoder.decodeAllNet +import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder.decodeAllNet import icu.samnyan.aqua.sega.util.AquaConst import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNetBillingDecoder.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNetBillingDecoder.kt new file mode 100644 index 00000000..a24c5087 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNetBillingDecoder.kt @@ -0,0 +1,50 @@ +package icu.samnyan.aqua.sega.allnet + +import ext.bodyString +import ext.header +import icu.samnyan.aqua.sega.util.ZLib +import java.net.http.HttpResponse +import java.util.* +import kotlin.text.Charsets.UTF_8 + +object AllNetBillingDecoder { + fun String.urlToMap() = split("&").map { it.split("=") }.filter { it.size == 2 }.associate { it[0] to it[1] } + + /** + * Decode the input byte array from Base64 MIME encoding and decompress the decoded byte array + */ + fun decode(src: ByteArray, base64: Boolean, nowrap: Boolean): Map { + // Decode the input byte array from Base64 MIME encoding + val bytes = if (!base64) src else Base64.getMimeDecoder().decode(src) + + // Decompress the decoded byte array + val output = ZLib.decompress(bytes, nowrap).toString(UTF_8).trim() + + // Split the string by '&' symbol to separate key-value pairs + return output.urlToMap() + } + + fun encode(src: Map, base64: Boolean): ByteArray { + // Join the key-value pairs with '&' symbol + val output = src.map { "${it.key}=${it.value}" }.joinToString("&") + + // Compress the joined string + val bytes = ZLib.compress(output.toByteArray(UTF_8)) + + // Encode the compressed byte array to Base64 MIME encoding + return if (!base64) bytes else Base64.getEncoder().encode(bytes) + } + + @JvmStatic + fun decodeAllNet(src: ByteArray) = decode(src, base64 = true, nowrap = false) + fun encodeAllNet(src: Map) = encode(src, base64 = true) + + @JvmStatic + fun decodeBilling(src: ByteArray) = decode(src, base64 = false, nowrap = true) + + fun HttpResponse.decodeAllNetResp() = + if (header("Pragma") == "DFI") decodeAllNet(body()) + else bodyString()?.urlToMap() +} + + diff --git a/src/main/java/icu/samnyan/aqua/sega/billing/Billing.kt b/src/main/java/icu/samnyan/aqua/sega/billing/Billing.kt index ddfc84b5..bf12e1cc 100644 --- a/src/main/java/icu/samnyan/aqua/sega/billing/Billing.kt +++ b/src/main/java/icu/samnyan/aqua/sega/billing/Billing.kt @@ -2,7 +2,7 @@ package icu.samnyan.aqua.sega.billing import ext.logger import ext.toUrl -import icu.samnyan.aqua.sega.util.AllNetBillingDecoder.decodeBilling +import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder.decodeBilling import jakarta.annotation.PostConstruct import jakarta.servlet.http.HttpServletRequest import org.eclipse.jetty.http.HttpVersion diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt index 03444005..4e592135 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt @@ -62,21 +62,22 @@ class ChusanController( val token = TokenChecker.getCurrentSession()?.token?.substring(0, 6) ?: "NO-TOKEN" log.info("Chu3 < $api : ${data.toJson()} : [$token]") + val noop = """{"returnCode":"1","apiName":"$api"}""" if (api !in noopEndpoint && !handlers.containsKey(api)) { log.warn("Chu3 > $api not found") - return """{"returnCode":"1","apiName":"$api"}""" + return noop } // Only record the counter metrics if the API is known. Metrics.counter("aquadx_chusan_api_call", "api" to api).increment() if (api in noopEndpoint) { log.info("Chu3 > $api no-op") - return """{"returnCode":"1"}""" + return noop } return try { Metrics.timer("aquadx_chusan_api_latency", "api" to api).recordCallable { - serialize(api, handlers[api]!!(ctx)).also { + serialize(api, handlers[api]!!(ctx) ?: noop).also { if (api !in setOf("GetUserItemApi", "GetGameEventApi")) log.info("Chu3 > $api : $it") } diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt index 73f4e354..b4c755bf 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt @@ -5,7 +5,7 @@ import icu.samnyan.aqua.sega.allnet.TokenChecker import icu.samnyan.aqua.sega.chusan.ChusanController import icu.samnyan.aqua.sega.chusan.ChusanData import icu.samnyan.aqua.sega.chusan.model.request.UserCMissionResp -import icu.samnyan.aqua.sega.chusan.model.userdata.UserItem +import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserItem import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail import icu.samnyan.aqua.sega.general.model.response.UserRecentRating import java.time.format.DateTimeFormatter @@ -179,7 +179,7 @@ fun ChusanController.chusanInit() { db.userData.findByCard_ExtId(uid)()?.card?.aquaUser?.gameOptions?.let { if (it.chusanInfinitePenguins && kind == 5) { items.removeAll { it.itemId in penguins } - items.addAll(penguins.map { UserItem(kind, it, 999, true) }) + items.addAll(penguins.map { Chu3UserItem(kind, it, 999, true) }) } } diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanCMApis.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanCMApis.kt index ad983f78..6116ed6b 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanCMApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanCMApis.kt @@ -1,12 +1,11 @@ package icu.samnyan.aqua.sega.chusan.handler -import com.fasterxml.jackson.core.type.TypeReference import ext.* import icu.samnyan.aqua.sega.chusan.ChusanController import icu.samnyan.aqua.sega.chusan.model.request.UpsertUserGacha import icu.samnyan.aqua.sega.chusan.model.request.UserEmoney import icu.samnyan.aqua.sega.chusan.model.userdata.UserCardPrintState -import icu.samnyan.aqua.sega.chusan.model.userdata.UserItem +import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserItem import java.time.LocalDateTime fun ChusanController.cmApiInit() { @@ -29,7 +28,7 @@ fun ChusanController.cmApiInit() { val (gachaId, placeId) = parsing { data["gachaId"]!!.int to data["placeId"]!!.int } val u = db.userData.findByCard_ExtId(uid)() ?: return@api null - val upsertUserGacha = parsing { mapper.convert(data["cmUpsertUserGacha"], UpsertUserGacha::class.java) } + val upsertUserGacha = parsing { mapper.convert(data["cmUpsertUserGacha"]!!) } upsertUserGacha.gameGachaCardList?.let { lst -> db.userCardPrintState.saveAll(lst.map { @@ -63,7 +62,7 @@ fun ChusanController.cmApiInit() { } "CMUpsertUserPrintCancel" { - val orderIdList: List = cmMapper.convert(data["orderIdList"], object : TypeReference>() {}) + val orderIdList: List = cmMapper.convert>(parsing { data["orderIdList"]!! }) db.userCardPrintState.saveAll(orderIdList.mapNotNull { // TODO: The original code by Eori writes findById but I don't think that is correct... @@ -76,8 +75,8 @@ fun ChusanController.cmApiInit() { } "CMUpsertUserPrintSubtract" api@ { - val userCardPrintState = cmMapper.convert(data["userCardPrintState"], UserCardPrintState::class.java) - val userItemList = cmMapper.convert(data["userItemList"], object : TypeReference>() {}) + val userCardPrintState = cmMapper.convert(parsing { data["userCardPrintState"]!! }) + val userItemList = cmMapper.convert>(parsing { data["userItemList"]!! }) val u = db.userData.findByCard_ExtId(uid)() ?: return@api null diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanUpsertApis.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanUpsertApis.kt index 36425071..ccbe9f45 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanUpsertApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanUpsertApis.kt @@ -2,7 +2,7 @@ package icu.samnyan.aqua.sega.chusan.handler import ext.* import icu.samnyan.aqua.sega.chusan.ChusanController -import icu.samnyan.aqua.sega.chusan.model.request.UpsertUserAll +import icu.samnyan.aqua.sega.chusan.model.request.Chu3UserAll import icu.samnyan.aqua.sega.chusan.model.userdata.* import icu.samnyan.aqua.sega.general.model.response.UserRecentRating @@ -17,7 +17,7 @@ fun ChusanController.upsertApiInit() { } "UpsertUserAll" api@ { - val req = mapper.convert(data["upsertUserAll"], UpsertUserAll::class.java) + val req = parsing { mapper.convert(data["upsertUserAll"]!!) } req.run { // UserData diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt index 280978f5..936c316f 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt @@ -40,11 +40,11 @@ interface Chu3UserLoginBonusRepo : JpaRepository { fun findLoginBonus(userId: Int, version: Int, presetId: Long): Optional } -interface Chu3UserActivityRepo : Chu3UserLinked { - fun findTopByUserAndActivityIdAndKindOrderByIdDesc(user: Chu3UserData, activityId: Int, kind: Int): Optional - fun findByUserAndActivityIdAndKind(user: Chu3UserData, activityId: Int, kind: Int): UserActivity? +interface Chu3UserActivityRepo : Chu3UserLinked { + fun findTopByUserAndActivityIdAndKindOrderByIdDesc(user: Chu3UserData, activityId: Int, kind: Int): Optional + fun findByUserAndActivityIdAndKind(user: Chu3UserData, activityId: Int, kind: Int): Chu3UserActivity? - fun findAllByUser_Card_ExtIdAndKind(extId: Long, kind: Int): List + fun findAllByUser_Card_ExtIdAndKind(extId: Long, kind: Int): List } interface Chu3UserCardPrintStateRepo : Chu3UserLinked { @@ -89,14 +89,14 @@ interface Chu3UserGeneralDataRepo : Chu3UserLinked { fun findByUser_Card_ExtIdAndPropertyKey(extId: Long, key: String): Optional } -interface Chu3UserItemRepo : Chu3UserLinked { - fun findAllByUser(user: Chu3UserData): List - fun findTopByUserAndItemIdAndItemKindOrderByIdDesc(user: Chu3UserData, itemId: Int, itemKind: Int): Optional - fun findByUserAndItemIdAndItemKind(user: Chu3UserData, itemId: Int, itemKind: Int): UserItem? +interface Chu3UserItemRepo : Chu3UserLinked { + fun findAllByUser(user: Chu3UserData): List + fun findTopByUserAndItemIdAndItemKindOrderByIdDesc(user: Chu3UserData, itemId: Int, itemKind: Int): Optional + fun findByUserAndItemIdAndItemKind(user: Chu3UserData, itemId: Int, itemKind: Int): Chu3UserItem? - fun findAllByUser_Card_ExtIdAndItemKind(extId: Long, itemKind: Int, pageable: Pageable): Page + fun findAllByUser_Card_ExtIdAndItemKind(extId: Long, itemKind: Int, pageable: Pageable): Page - fun findAllByUser_Card_ExtIdAndItemKind(extId: Long, itemKind: Int): List + fun findAllByUser_Card_ExtIdAndItemKind(extId: Long, itemKind: Int): List } interface Chu3UserMapRepo : Chu3UserLinked { diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/request/UpsertUserAll.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/request/Chu3UserAll.kt similarity index 94% rename from src/main/java/icu/samnyan/aqua/sega/chusan/model/request/UpsertUserAll.kt rename to src/main/java/icu/samnyan/aqua/sega/chusan/model/request/Chu3UserAll.kt index 2c195609..95e29052 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/request/UpsertUserAll.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/request/Chu3UserAll.kt @@ -37,13 +37,13 @@ data class MusicIdWrapper( val musicId: Int = 0, ) -class UpsertUserAll( +class Chu3UserAll( var userData: List? = null, var userGameOption: List? = null, var userCharacterList: List? = null, - var userItemList: List? = null, + var userItemList: List? = null, var userMusicDetailList: List? = null, - var userActivityList: List? = null, + var userActivityList: List? = null, var userRecentRatingList: List? = null, var userPlaylogList: List? = null, var userChargeList: List? = null, diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/request/UpsertUserGacha.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/request/UpsertUserGacha.kt index 92c3ab39..ad264c3b 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/request/UpsertUserGacha.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/request/UpsertUserGacha.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import icu.samnyan.aqua.sega.chusan.model.GameGachaCard import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserData import icu.samnyan.aqua.sega.chusan.model.userdata.UserGacha -import icu.samnyan.aqua.sega.chusan.model.userdata.UserItem +import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserItem import java.io.Serializable class UpsertUserGacha : Serializable { @@ -13,7 +13,7 @@ class UpsertUserGacha : Serializable { var userCharacterList: List? = null var userCardList: List? = null var gameGachaCardList: List? = null - var userItemList: List? = null + var userItemList: List? = null @JsonProperty("isNewCharacterList") var isNewCharacterList: String? = null diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserActivity.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserActivity.kt similarity index 93% rename from src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserActivity.kt rename to src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserActivity.kt index ef5d527b..301f3fb1 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserActivity.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserActivity.kt @@ -8,7 +8,7 @@ import jakarta.persistence.UniqueConstraint @Entity(name = "ChusanUserActivity") @Table(name = "chusan_user_activity", uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "kind", "activity_id"])]) -class UserActivity : Chu3UserEntity() { +class Chu3UserActivity : Chu3UserEntity() { var kind = 0 @JsonProperty("id") @Column(name = "activity_id") diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserItem.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserItem.kt similarity index 96% rename from src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserItem.kt rename to src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserItem.kt index 94fd554f..8419c0a9 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserItem.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/Chu3UserItem.kt @@ -7,7 +7,7 @@ import jakarta.persistence.UniqueConstraint @Entity(name = "ChusanUserItem") @Table(name = "chusan_user_item", uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "item_id", "item_kind"])]) -class UserItem( +class Chu3UserItem( var itemKind: Int = 0, var itemId: Int = 0, var stock: Int = 1, diff --git a/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt b/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt index 1b0d6e88..0a11905a 100644 --- a/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt +++ b/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt @@ -30,7 +30,7 @@ typealias PagePost = (MutJDict) -> Unit data class PagedProcessor(val add: JDict?, val fn: PagedHandler, var post: PagePost? = null) // A very :3 way of declaring APIs -abstract class MeowApi(val serialize: (String, Any?) -> String) { +abstract class MeowApi(val serialize: (String, Any) -> String) { val initH = mutableMapOf() infix operator fun String.invoke(fn: SpecialHandler) = initH.set("${this}Api", fn) infix fun String.static(fn: () -> Any) = serialize(this, fn()).let { resp -> this { resp } } diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt index 95324ac9..8eaa8b24 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt @@ -4,12 +4,10 @@ package icu.samnyan.aqua.sega.maimai2 import ext.* import icu.samnyan.aqua.sega.general.PagedHandler -import icu.samnyan.aqua.sega.maimai2.model.response.data.UserRivalMusic -import icu.samnyan.aqua.sega.maimai2.model.response.data.UserRivalMusicDetail -import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserIntimate +import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusic +import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusicDetail import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserKaleidx import java.time.LocalDate -import java.util.* fun Maimai2ServletController.initApis() { // Used because maimai does not actually require paging implementation @@ -157,7 +155,7 @@ fun Maimai2ServletController.initApis() { val rivalId = parsing { data["rivalId"]!!.long } val lst = db.userMusicDetail.findByUserId(rivalId) - val res = lst.associate { it.musicId to UserRivalMusic(it.musicId, LinkedList()) } + val res = lst.associate { it.musicId to UserRivalMusic(it.musicId) } lst.forEach { res[it.musicId]!!.userRivalMusicDetailList.add( @@ -195,6 +193,7 @@ fun Maimai2ServletController.initApis() { } // Kaleidoscope, added on 1.50 + // [{gateId, phaseId}] "GetGameKaleidxScope" { mapOf("gameKaleidxScopeList" to ls( mapOf("gateId" to 1, "phaseId" to findPhase(LocalDate.of(2025, 1, 18))), mapOf("gateId" to 2, "phaseId" to 2), @@ -203,6 +202,8 @@ fun Maimai2ServletController.initApis() { mapOf("gateId" to 5, "phaseId" to 2), mapOf("gateId" to 6, "phaseId" to 2), )) } + // Request: {userId} + // Response: {userId, userKaleidxScopeList} "GetUserKaleidxScope".unpaged { val u = db.userData.findByCardExtId(uid)() ?: (404 - "User not found") val lst = db.userKaleidx.findByUser(u) @@ -213,6 +214,8 @@ fun Maimai2ServletController.initApis() { lst } + // Request: {userId, version, userData: [UserDetail], userPlaylogList: [UserPlaylog]} + // Response: {userId, userItemList: [UserItem]} // Added on 1.50 "GetUserNewItemList" { mapOf("userId" to uid, "userItemList" to empty) } 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 e88bc438..5ec2afc0 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt @@ -68,9 +68,10 @@ class Maimai2ServletController( @API("/{api}") fun handle(@PathVariable api: String, @RequestBody data: Map, req: HttpServletRequest): Any { logger.info("Mai2 < $api : ${data.toJson()}") // TODO: Optimize logging + val noop = """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}""" if (api !in noopEndpoint && !handlers.containsKey(api)) { logger.warn("Mai2 > $api not found") - return """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}""" + return noop } // Only record the counter metrics if the API is known. @@ -78,13 +79,13 @@ class Maimai2ServletController( if (api in noopEndpoint) { logger.info("Mai2 > $api no-op") - return """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}""" + return noop } return try { Metrics.timer("aquadx_maimai2_api_latency", "api" to api).recordCallable { val ctx = RequestContext(req, data.mut) - serialize(api, handlers[api]!!(ctx)).also { + serialize(api, handlers[api]!!(ctx) ?: noop).also { logger.info("Mai2 > $api : ${it.truncate(1000)}") } } diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetUserPortraitHandler.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetUserPortraitHandler.java deleted file mode 100644 index 668f768f..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetUserPortraitHandler.java +++ /dev/null @@ -1,98 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.handler; - -import com.fasterxml.jackson.core.JsonProcessingException; -import icu.samnyan.aqua.net.db.AquaNetUser; -import icu.samnyan.aqua.net.utils.PathProps; -import icu.samnyan.aqua.sega.general.BaseHandler; -import icu.samnyan.aqua.sega.general.dao.CardRepository; -import icu.samnyan.aqua.sega.general.model.Card; -import icu.samnyan.aqua.sega.maimai2.model.request.data.UserPortrait; -import icu.samnyan.aqua.sega.util.jackson.BasicMapper; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.crypto.codec.Utf8; -import org.springframework.stereotype.Component; - -import java.io.FileInputStream; -import java.nio.file.Paths; -import java.util.*; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Component("Maimai2GetUserPortraitHandler") -public class GetUserPortraitHandler implements BaseHandler { - private static final Logger logger = LoggerFactory.getLogger(GetUserPortraitHandler.class); - - private final BasicMapper mapper; - private final boolean enable; - private final CardRepository cardRepo; - private final String portraitPath; - - public GetUserPortraitHandler(BasicMapper mapper, - @Value("${game.maimai2.userPhoto.enable:true}") boolean enable, - CardRepository cardRepo, - PathProps paths) { - this.mapper = mapper; - this.enable = enable; - this.cardRepo = cardRepo; - this.portraitPath = paths.getAquaNetPortrait(); - } - - @Override - public String handle(Map request) throws JsonProcessingException { - if (enable) { - var userId = ((Number) request.get("userId")).longValue(); - var list = new ArrayList(); - var card = cardRepo.findByExtId(userId); - var user = card.map(Card::getAquaUser); - var profilePicture = user.map(AquaNetUser::getProfilePicture).orElse(null); - - try { - if (!StringUtils.isEmpty(profilePicture)) { - var filePath = Paths.get(portraitPath, profilePicture); - var buffer = new byte[10240]; - - var stream = new FileInputStream(filePath.toFile()); - while (stream.available() > 0) { - var read = stream.read(buffer, 0, 10240); - - var encodeBuffer = read == 10240 ? buffer : Arrays.copyOfRange(buffer, 0, read); - - var userPortrait = new UserPortrait(); - - userPortrait.setFileName("portrait.jpg"); - userPortrait.setPlaceId(0); - userPortrait.setUserId(userId); - userPortrait.setClientId(""); - userPortrait.setUploadDate("1970-01-01 09:00:00.0"); - userPortrait.setDivData(Utf8.decode(Base64.getEncoder().encode(encodeBuffer))); - - userPortrait.setDivNumber(list.size()); - - list.add(userPortrait); - } - - stream.close(); - for (var i = 0; i < list.size(); i++) { - var userPortrait = list.get(i); - userPortrait.setDivLength(list.size()); - } - - var map = new HashMap(); - map.put("length", list.size()); - map.put("userPortraitList", list); - - var respJson = mapper.write(map); - return respJson; - } - } catch (Exception e) { - logger.error("Result: User photo save failed", e); - } - } - - return "{\"length\":0,\"userPortraitList\":[]}"; - } -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetUserPortraitHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetUserPortraitHandler.kt new file mode 100644 index 00000000..d36ed191 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetUserPortraitHandler.kt @@ -0,0 +1,63 @@ +package icu.samnyan.aqua.sega.maimai2.handler + +import ext.invoke +import ext.logger +import icu.samnyan.aqua.net.utils.PathProps +import icu.samnyan.aqua.sega.general.BaseHandler +import icu.samnyan.aqua.sega.general.dao.CardRepository +import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UserPortrait +import org.springframework.beans.factory.annotation.Value +import org.springframework.security.crypto.codec.Utf8 +import org.springframework.stereotype.Component +import java.io.FileInputStream +import java.nio.file.Paths +import java.util.* + +@Component("Maimai2GetUserPortraitHandler") +class GetUserPortraitHandler( + val cardRepo: CardRepository, + + @param:Value("\${game.maimai2.userPhoto.enable:true}") val enable: Boolean, + + paths: PathProps +) : BaseHandler { + val portraitPath = paths.aquaNetPortrait + val log = logger() + + override fun handle(request: Map): Any? { + if (!enable) return """{"length":0,"userPortraitList":[]}""" + + val uid = (request["userId"] as Number).toLong() + val list = ArrayList() + val profilePicture = cardRepo.findByExtId(uid)()?.aquaUser?.profilePicture?.ifBlank { null } + ?: return """{"length":0,"userPortraitList":[]}""" + + try { + val filePath = Paths.get(portraitPath, profilePicture) + val buffer = ByteArray(10240) + + FileInputStream(filePath.toFile()).use { stream -> + while (stream.available() > 0) { + val read = stream.read(buffer, 0, 10240) + val buf = if (read == 10240) buffer else Arrays.copyOfRange(buffer, 0, read) + + list.add(Mai2UserPortrait().apply { + userId = uid + divData = Utf8.decode(Base64.getEncoder().encode(buf)) + divNumber = list.size + }) + } + } + + list.forEach { it.divLength = list.size } + + return mapOf( + "length" to list.size, + "userPortraitList" to list + ) + } catch (e: Exception) { + log.error("Result: User photo get failed", e) + return """{"length":0,"userPortraitList":[]}""" + } + } +} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetUserRatingHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetUserRatingHandler.kt index 0e1bd922..c9a6813d 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetUserRatingHandler.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/GetUserRatingHandler.kt @@ -3,7 +3,7 @@ package icu.samnyan.aqua.sega.maimai2.handler import ext.invoke import icu.samnyan.aqua.sega.general.BaseHandler import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos -import icu.samnyan.aqua.sega.maimai2.model.response.data.UserRating +import icu.samnyan.aqua.sega.maimai2.model.UserRating import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserRate import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserUdemae import icu.samnyan.aqua.sega.util.jackson.BasicMapper diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPhotoHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPhotoHandler.kt index 58396b46..e3942a67 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPhotoHandler.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPhotoHandler.kt @@ -1,11 +1,8 @@ package icu.samnyan.aqua.sega.maimai2.handler -import ext.div -import ext.isoDateTime -import ext.logger -import ext.path +import ext.* import icu.samnyan.aqua.sega.general.BaseHandler -import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPhoto +import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UserPhoto import icu.samnyan.aqua.sega.util.jackson.BasicMapper import org.springframework.stereotype.Component import java.io.IOException @@ -23,9 +20,7 @@ class UploadUserPhotoHandler(private val mapper: BasicMapper) : // Maimai DX sends split base64 data for one jpeg image. // So, make a temp file and keep append bytes until last part received. // If finished, rename it to other name so user can keep save multiple scorecards in a single day. - - val uploadUserPhoto = mapper.convert(request, UploadUserPhoto::class.java) - val up = uploadUserPhoto.userPhoto + val up = parsing { mapper.convert(request["userPhoto"]!!, Mai2UserPhoto::class.java) } try { val tmpFile = tmpDir / "${up.userId}-${up.trackNo}.tmp" diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPlaylogHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPlaylogHandler.kt index 44d9d8fa..b5824cb5 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPlaylogHandler.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPlaylogHandler.kt @@ -1,12 +1,13 @@ package icu.samnyan.aqua.sega.maimai2.handler import ext.logger +import ext.long import ext.millis +import ext.parsing import icu.samnyan.aqua.sega.allnet.TokenChecker import icu.samnyan.aqua.sega.general.BaseHandler import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPlaylogRepo -import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPlaylog import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPlaylog import icu.samnyan.aqua.sega.util.jackson.BasicMapper import icu.samnyan.aqua.spring.Metrics @@ -33,9 +34,10 @@ class UploadUserPlaylogHandler( } override fun handle(request: Map): String { - val req = mapper.convert(request, UploadUserPlaylog::class.java) + val uid = parsing { request["userId"]!!.long } + val playlog = parsing { mapper.convert(request["userPlaylog"]!!, Mai2UserPlaylog::class.java) } - val version = tryParseGameVersion(req.userPlaylog.version) + val version = tryParseGameVersion(playlog.version) if (version != null) { val session = TokenChecker.getCurrentSession() val gameId = if (session?.gameId in VALID_GAME_IDS) session!!.gameId else "" @@ -47,9 +49,9 @@ class UploadUserPlaylogHandler( // Check duplicate val isDup = playlogRepo.findByUser_Card_ExtIdAndMusicIdAndUserPlayDate( - req.userId, - req.userPlaylog.musicId, - req.userPlaylog.userPlayDate + uid, + playlog.musicId, + playlog.userPlayDate ).size > 0 if (isDup) { log.info("Duplicate playlog detected") @@ -57,14 +59,14 @@ class UploadUserPlaylogHandler( } // Save if the user is registered - val u = userDataRepository.findByCardExtId(req.userId).getOrNull() - if (u != null) playlogRepo.save(req.userPlaylog.apply { user = u }) + val u = userDataRepository.findByCardExtId(uid).getOrNull() + if (u != null) playlogRepo.save(playlog.apply { user = u }) // If the user hasn't registered (first play), save the playlog to a backlog else { - playBacklog.putIfAbsent(req.userId, mutableListOf()) - playBacklog[req.userId]?.apply { - add(BacklogEntry(millis(), req.userPlaylog)) + playBacklog.putIfAbsent(uid, mutableListOf()) + playBacklog[uid]?.apply { + add(BacklogEntry(millis(), playlog)) if (size > 6) clear() // Prevent abuse } } diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPortraitHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPortraitHandler.kt index 4df81ff6..1301bcb4 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPortraitHandler.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UploadUserPortraitHandler.kt @@ -2,10 +2,11 @@ package icu.samnyan.aqua.sega.maimai2.handler import ext.div import ext.logger +import ext.parsing import ext.path import icu.samnyan.aqua.net.utils.PathProps import icu.samnyan.aqua.sega.general.BaseHandler -import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPortrait +import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UserPortrait import icu.samnyan.aqua.sega.util.jackson.BasicMapper import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component @@ -34,8 +35,7 @@ class UploadUserPortraitHandler( // Maimai DX sends split base64 data for one jpeg image. // So, make a temp file and keep append bytes until last part received. // If finished, rename it to other name so user can keep save multiple scorecards in a single day. - - val up = mapper.convert(request, UploadUserPortrait::class.java).userPortrait + val up = parsing { mapper.convert(request["userPortrait"]!!, Mai2UserPortrait::class.java) } val id = up.userId val num = up.divNumber diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UpsertUserAllHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UpsertUserAllHandler.kt index df78d046..e2ec4b99 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UpsertUserAllHandler.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UpsertUserAllHandler.kt @@ -3,13 +3,12 @@ package icu.samnyan.aqua.sega.maimai2.handler import com.fasterxml.jackson.core.JsonProcessingException import ext.invoke import ext.mapApply -import ext.minus import ext.unique import icu.samnyan.aqua.sega.general.BaseHandler import icu.samnyan.aqua.sega.general.service.CardService import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler.Companion.playBacklog import icu.samnyan.aqua.sega.maimai2.model.* -import icu.samnyan.aqua.sega.maimai2.model.request.UpsertUserAll +import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UpsertUserAll import icu.samnyan.aqua.sega.maimai2.model.userdata.* import icu.samnyan.aqua.sega.util.jackson.BasicMapper import lombok.AllArgsConstructor @@ -31,16 +30,13 @@ class UpsertUserAllHandler( @Throws(JsonProcessingException::class) override fun handle(request: Map): Any? { - val upsertUserAll = mapper.convert(request, UpsertUserAll::class.java) + val upsertUserAll = mapper.convert(request, Mai2UpsertUserAll::class.java) val userId = upsertUserAll.userId val req = upsertUserAll.upsertUserAll // If user is guest, just return OK response. if ((userId and 281474976710657L) == 281474976710657L) return SUCCESS - // UserData - if (req.userData == null) 400 - "Invalid Request" - val userData = repos.userData.findByCardExtId(userId)() val u = repos.userData.saveAndFlush(req.userData[0].apply { id = userData?.id ?: 0 diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UpsertUserPrintHandler.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UpsertUserPrintHandler.java deleted file mode 100644 index bb40e166..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UpsertUserPrintHandler.java +++ /dev/null @@ -1,107 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.handler; - -import com.fasterxml.jackson.core.JsonProcessingException; - -import icu.samnyan.aqua.sega.maimai2.model.Mai2UserCardRepo; -import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo; -import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPrintDetailRepo; -import icu.samnyan.aqua.sega.general.BaseHandler; -import icu.samnyan.aqua.sega.maimai2.model.request.UpsertUserPrint; -import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserCard; -import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserDetail; -import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail; -import icu.samnyan.aqua.sega.util.jackson.BasicMapper; -import lombok.AllArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ThreadLocalRandom; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Component("Maimai2UpsertUserPrintHandler") -public class UpsertUserPrintHandler implements BaseHandler { - - private static final Logger logger = LoggerFactory.getLogger(UpsertUserPrintHandler.class); - private final BasicMapper mapper; - - private final Mai2UserCardRepo userCardRepository; - private final Mai2UserPrintDetailRepo userPrintDetailRepository; - private final Mai2UserDataRepo userDataRepository; - - private long expirationTime; - - public UpsertUserPrintHandler(BasicMapper mapper, - @Value("${game.cardmaker.card.expiration:15}") long expirationTime, - Mai2UserCardRepo userCardRepository, - Mai2UserPrintDetailRepo userPrintDetailRepository, - Mai2UserDataRepo userDataRepository - ) { - this.mapper = mapper; - this.expirationTime = expirationTime; - this.userCardRepository = userCardRepository; - this.userPrintDetailRepository = userPrintDetailRepository; - this.userDataRepository = userDataRepository; - } - - @Override - public String handle(Map request) throws JsonProcessingException { - long userId = ((Number) request.get("userId")).longValue(); - - Mai2UserDetail userData; - - Optional userOptional = userDataRepository.findByCardExtId(userId); - if (userOptional.isPresent()) { - userData = userOptional.get(); - } else { - logger.error("User not found. userId: {}", userId); - return null; - } - - UpsertUserPrint upsertUserPrint = mapper.convert(request, UpsertUserPrint.class); - - Mai2UserPrintDetail userPrintDetail = upsertUserPrint.getUserPrintDetail(); - Mai2UserCard newUserCard = userPrintDetail.getUserCard(); - - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS"); - String currentDateTime = LocalDateTime.now().format(formatter); - String expirationDateTime = LocalDateTime.now().plusDays(expirationTime).format(formatter); - String randomSerialId = - String.format("%010d", ThreadLocalRandom.current().nextLong(0L, 9999999999L)) + - String.format("%010d", ThreadLocalRandom.current().nextLong(0L, 9999999999L)); - - newUserCard.setUser(userData); - userPrintDetail.setUser(userData); - - newUserCard.setStartDate(currentDateTime); - newUserCard.setEndDate(expirationDateTime); - userPrintDetail.setSerialId(randomSerialId); - - Optional userCardOptional = userCardRepository.findByUserAndCardId(newUserCard.getUser(), newUserCard.getCardId()); - if (userCardOptional.isPresent()) { - Mai2UserCard userCard = userCardOptional.get(); - newUserCard.setId(userCard.getId()); - } - - userCardRepository.save(newUserCard); - userPrintDetailRepository.save(userPrintDetail); - - Map resultMap = new LinkedHashMap<>(); - resultMap.put("returnCode", 1); - resultMap.put("orderId", 0); - resultMap.put("serialId", randomSerialId); - resultMap.put("startDate", currentDateTime); - resultMap.put("endDate", expirationDateTime); - - return mapper.write(resultMap); - } -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UpsertUserPrintHandler.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UpsertUserPrintHandler.kt new file mode 100644 index 00000000..6db707aa --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/handler/UpsertUserPrintHandler.kt @@ -0,0 +1,54 @@ +package icu.samnyan.aqua.sega.maimai2.handler + +import ext.invoke +import ext.logger +import ext.long +import ext.parsing +import icu.samnyan.aqua.sega.general.BaseHandler +import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos +import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail +import icu.samnyan.aqua.sega.util.jackson.BasicMapper +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.concurrent.ThreadLocalRandom + +@Component("Maimai2UpsertUserPrintHandler") +class UpsertUserPrintHandler( + val mapper: BasicMapper, + val db: Mai2Repos, + @param:Value("\${game.cardmaker.card.expiration:15}") val expirationTime: Long, +) : BaseHandler { + val log = logger() + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS") + + override fun handle(request: Map): Any? { + val userId = parsing { request["userId"]!!.long } + val userData = db.userData.findByCardExtId(userId)() ?: return null + + val userPrint = parsing { mapper.convert(request["userPrintDetail"]!!, Mai2UserPrintDetail::class.java) } + val newCard = userPrint.userCard ?: return null + + newCard.user = userData + newCard.startDate = LocalDateTime.now().format(formatter) + newCard.endDate = LocalDateTime.now().plusDays(expirationTime).format(formatter) + newCard.id = db.userCard.findByUserAndCardId(newCard.user, newCard.cardId)()?.id ?: 0 + db.userCard.save(newCard) + + userPrint.user = userData + userPrint.serialId = buildString { + append(String.format("%010d", ThreadLocalRandom.current().nextLong(0L, 9999999999L))) + append(String.format("%010d", ThreadLocalRandom.current().nextLong(0L, 9999999999L))) + } + db.userPrintDetail.save(userPrint) + + return mapOf( + "returnCode" to 1, + "orderId" to 0, + "serialId" to userPrint.serialId, + "startDate" to newCard.startDate, + "endDate" to newCard.endDate + ) + } +} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/UserRating.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/UserRating.kt new file mode 100644 index 00000000..5639e410 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/UserRating.kt @@ -0,0 +1,13 @@ +package icu.samnyan.aqua.sega.maimai2.model + +import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserRate +import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserUdemae + +class UserRating { + var rating = 0 + var ratingList: List = emptyList() + var newRatingList: List = emptyList() + var nextRatingList: List = emptyList() + var nextNewRatingList: List = emptyList() + var udemae: Mai2UserUdemae = Mai2UserUdemae() +} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/UserRivalMusic.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/UserRivalMusic.kt new file mode 100644 index 00000000..cdadd70b --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/UserRivalMusic.kt @@ -0,0 +1,12 @@ +package icu.samnyan.aqua.sega.maimai2.model + +class UserRivalMusic( + var musicId: Int, + var userRivalMusicDetailList: MutableList = mutableListOf() +) + +class UserRivalMusicDetail( + var level: Int, + var achievement: Int, + var deluxscoreMax: Int +) \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/Mai2UpsertUserAll.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/Mai2UpsertUserAll.kt new file mode 100644 index 00000000..2ad0a8fe --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/Mai2UpsertUserAll.kt @@ -0,0 +1,53 @@ +package icu.samnyan.aqua.sega.maimai2.model.request + +import icu.samnyan.aqua.sega.maimai2.model.UserRating +import icu.samnyan.aqua.sega.maimai2.model.userdata.* + +class Mai2UpsertUserAll( + var userId: Long, + var upsertUserAll: Mai2UserAll +) + +class Mai2UserAll { + var userData: List = emptyList() + var userOption: List? = null + var userExtend: List? = null + var userCharacterList: List? = null + var userGhost: List? = null + var userMapList: List? = null + var userLoginBonusList: List? = null + var userRatingList: List? = null + var userItemList: List? = null + var userMusicDetailList: List? = null + var userCourseList: List? = null + var userFriendSeasonRankingList: List? = null + var userChargeList: List? = null + var userFavoriteList: List? = null + var userActivityList: List? = null + var userGamePlaylogList: List>? = null + var userFavoritemusicList: List? = null + var userKaleidxScopeList: List? = null + var userIntimateList: List? = null + var isNewCharacterList: String? = null + var isNewMapList: String? = null + var isNewLoginBonusList: String? = null + var isNewItemList: String? = null + var isNewMusicDetailList: String? = null + var isNewCourseList: String? = null + var isNewFavoriteList: String? = null + var isNewFriendSeasonRankingList: String? = null + var isNewFavoritemusicList: String? = null + var isNewKaleidxScopeList: String? = null +} + +class Mai2UserFavoriteItem { + var orderId = 0 + var id = 0 +} + +class Mai2UserActivity { + var playList: List = emptyList() + var musicList: List = emptyList() +} + + diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/Mai2UserPhoto.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/Mai2UserPhoto.kt new file mode 100644 index 00000000..8ace5b2b --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/Mai2UserPhoto.kt @@ -0,0 +1,25 @@ +package icu.samnyan.aqua.sega.maimai2.model.request + +class Mai2UserPhoto { + var orderId = 0 + var userId: Long = 0 + var divNumber = 0 + var divLength = 0 + var divData: String? = null + var placeId = 0 + var clientId: String? = null + var uploadDate: String? = null + var playlogId: Long = 0 + var trackNo = 0 +} + +class Mai2UserPortrait { + var userId: Long = 0 + var divNumber = 0 + var divLength = 0 + var divData: String? = null + var placeId = 0 + var clientId: String = "" + var uploadDate: String = "1970-01-01 09:00:00.0" + var fileName: String = "portrait.jpg" +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UploadUserPhoto.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UploadUserPhoto.java deleted file mode 100644 index 196a2d39..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UploadUserPhoto.java +++ /dev/null @@ -1,18 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.request; - -import icu.samnyan.aqua.sega.maimai2.model.request.data.UserPhoto; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UploadUserPhoto implements Serializable { - private UserPhoto userPhoto; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UploadUserPlaylog.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UploadUserPlaylog.java deleted file mode 100644 index 7748e4cf..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UploadUserPlaylog.java +++ /dev/null @@ -1,20 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.request; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPlaylog; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UploadUserPlaylog implements Serializable { - private long userId; - private Mai2UserPlaylog userPlaylog; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UploadUserPortrait.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UploadUserPortrait.java deleted file mode 100644 index f1810d67..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UploadUserPortrait.java +++ /dev/null @@ -1,18 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.request; - -import icu.samnyan.aqua.sega.maimai2.model.request.data.UserPortrait; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UploadUserPortrait implements Serializable { - private UserPortrait userPortrait; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UpsertUserAll.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UpsertUserAll.java deleted file mode 100644 index 4ff893de..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UpsertUserAll.java +++ /dev/null @@ -1,25 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.request; - -import com.fasterxml.jackson.annotation.JsonProperty; -import icu.samnyan.aqua.sega.maimai2.model.request.data.UserAll; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UpsertUserAll implements Serializable { - private long userId; - private long playlogId; - @JsonProperty("isEventMode") - private boolean isEventMode; - @JsonProperty("isFreePlay") - private boolean isFreePlay; - private UserAll upsertUserAll; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UpsertUserPrint.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UpsertUserPrint.java deleted file mode 100644 index 62b41ee6..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/UpsertUserPrint.java +++ /dev/null @@ -1,22 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.request; - -import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; -import java.util.Map; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UpsertUserPrint implements Serializable { - private long userId; - private long orderId; - private Map userPrintReserve; - private Mai2UserPrintDetail userPrintDetail; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserAll.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserAll.java deleted file mode 100644 index 88c38e9f..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserAll.java +++ /dev/null @@ -1,50 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.request.data; - -import icu.samnyan.aqua.sega.maimai2.model.response.data.Mai2UserActivity; -import icu.samnyan.aqua.sega.maimai2.model.response.data.UserRating; -import icu.samnyan.aqua.sega.maimai2.model.userdata.*; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; -import java.util.List; -import java.util.Map; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UserAll implements Serializable { - private List userData; - private List userExtend; - private List userOption; - private List userCharacterList; - private List userGhost; - private List userMapList; - private List userLoginBonusList; - private List userRatingList; - private List userItemList; - private List userMusicDetailList; - private List userCourseList; - private List userFriendSeasonRankingList; - private List userChargeList; - private List userFavoriteList; - private List userActivityList; - private List> userGamePlaylogList; - private List userFavoritemusicList; - private List userKaleidxScopeList; - private List userIntimateList; - private String isNewCharacterList; - private String isNewMapList; - private String isNewLoginBonusList; - private String isNewItemList; - private String isNewMusicDetailList; - private String isNewCourseList; - private String isNewFavoriteList; - private String isNewFriendSeasonRankingList; - private String isNewFavoritemusicList; - private String isNewKaleidxScopeList; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserFavoriteItem.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserFavoriteItem.java deleted file mode 100644 index 7b03799d..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserFavoriteItem.java +++ /dev/null @@ -1,15 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.request.data; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UserFavoriteItem implements Serializable { - private int orderId; - private int id; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserPhoto.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserPhoto.java deleted file mode 100644 index 192a9c37..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserPhoto.java +++ /dev/null @@ -1,26 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.request.data; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UserPhoto implements Serializable { - private int orderId; - private long userId; - private int divNumber; - private int divLength; - private String divData; - private int placeId; - private String clientId; - private String uploadDate; - private long playlogId; - private int trackNo; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserPortrait.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserPortrait.java deleted file mode 100644 index ac7be464..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/request/data/UserPortrait.java +++ /dev/null @@ -1,24 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.request.data; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UserPortrait implements Serializable { - private long userId; - private int divNumber; - private int divLength; - private String divData; - private int placeId; - private String clientId; - private String uploadDate; - private String fileName; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/Mai2UserActivity.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/Mai2UserActivity.java deleted file mode 100644 index fc59ddb8..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/Mai2UserActivity.java +++ /dev/null @@ -1,19 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.response.data; - -import java.util.List; - -import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserAct; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class Mai2UserActivity { - private List playList; - private List musicList; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/UserRating.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/UserRating.java deleted file mode 100644 index dac23350..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/UserRating.java +++ /dev/null @@ -1,24 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.response.data; - -import java.util.List; - -import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserRate; -import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserUdemae; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UserRating { - private int rating; - private List ratingList; - private List newRatingList; - private List nextRatingList; - private List nextNewRatingList; - private Mai2UserUdemae udemae; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/UserRivalMusic.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/UserRivalMusic.java deleted file mode 100644 index 51f26449..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/UserRivalMusic.java +++ /dev/null @@ -1,15 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.response.data; - -import java.util.List; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UserRivalMusic { - private int musicId; - private List userRivalMusicDetailList; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/UserRivalMusicDetail.java b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/UserRivalMusicDetail.java deleted file mode 100644 index f05a8824..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/response/data/UserRivalMusicDetail.java +++ /dev/null @@ -1,14 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.model.response.data; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UserRivalMusicDetail { - private int level; - private int achievement; - private int deluxscoreMax; -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariLobby.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariLobby.kt deleted file mode 100644 index d9fb2b01..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariLobby.kt +++ /dev/null @@ -1,96 +0,0 @@ - -package icu.samnyan.aqua.sega.maimai2.worldslink - -import ext.* -import icu.samnyan.aqua.net.utils.PathProps -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import java.io.BufferedWriter -import java.io.File -import java.io.FileOutputStream -import java.time.LocalDateTime -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock -import kotlin.io.path.readText - - -// KotlinX Serialization -@OptIn(ExperimentalSerializationApi::class) -private val KJson = Json { - ignoreUnknownKeys = true - isLenient = true - explicitNulls = false - coerceInputValues = true -} - -// Maximum time to live for a recruit record -const val MAX_TTL = 30 * 1000 - -@RestController -@RequestMapping(path = ["/mai2-futari"]) -class FutariLobby(val paths: PathProps) { - // - val recruits = mutableMapOf() - // Append writer - lateinit var writer: BufferedWriter - val mutex = ReentrantLock() - val log = logger() - - init { - paths.init() - writer = FileOutputStream(File(paths.futariRecruitLog), true).bufferedWriter() - } - - fun log(data: String) = mutex.withLock { - log.info(data) - writer.write(data) - writer.newLine() - writer.flush() - } - - fun log(data: RecruitRecord, msg: String) = - log("${LocalDateTime.now().isoDateTime()}: $msg: ${KJson.encodeToString(data)}") - - val RecruitRecord.ip get() = RecruitInfo.MechaInfo.IpAddress - - @API("recruit/start") - fun startRecruit(@RB data: String) { - val d = parsing { KJson.decodeFromString(data) }.apply { Time = millis() } - val exists = d.ip in recruits - recruits[d.ip] = d - - if (!exists) log(d, "StartRecruit") - d.RecruitInfo.MechaInfo.UserIDs = d.RecruitInfo.MechaInfo.UserIDs.map { it.str.hashToUInt().toLong() } - } - - @API("recruit/finish") - fun finishRecruit(@RB data: String) { - val d = parsing { KJson.decodeFromString(data) } - if (d.ip !in recruits) 400 - "Recruit not found" -// if (d.Keychip != recruits[d.ip]!!.Keychip) 400 - "Keychip mismatch" - recruits.remove(d.ip) - log(d, "EndRecruit") - } - - @API("recruit/list") - fun listRecruit(): String { - val time = millis() - recruits.filterValues { time - it.Time > MAX_TTL }.keys.forEach { recruits.remove(it) } - return recruits.values.toList().joinToString("\n") { KJson.encodeToString(it) } - } - - @API("server-list") - fun serverList() = paths.futariRelayInfo.path().readText().trim() -} - -fun main(args: Array) { - val json = """{"RecruitInfo":{"MechaInfo":{"IsJoin":true,"IpAddress":1820162433,"MusicID":11692,"Entrys":[true,false],"UserIDs":[281474976710657,281474976710657],"UserNames":["GUEST","GUEST"],"IconIDs":[1,1],"FumenDifs":[0,-1],"Rateing":[0,0],"ClassValue":[0,0],"MaxClassValue":[0,0],"UserType":[0,0]},"MusicID":11692,"GroupID":0,"EventModeID":false,"JoinNumber":1,"PartyStance":0,"_startTimeTicks":638725464510308001,"_recvTimeTicks":0}}""" - println(json.jsonMap().toJson()) - val data = KJson.decodeFromString(json) - println(json) - println(KJson.encodeToString(data)) - println(data) -} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariRelay.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariRelay.kt deleted file mode 100644 index f037d285..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariRelay.kt +++ /dev/null @@ -1,187 +0,0 @@ -package icu.samnyan.aqua.sega.maimai2.worldslink - -import ext.logger -import ext.md5 -import ext.millis -import ext.thread -import java.io.BufferedReader -import java.io.BufferedWriter -import java.io.InputStreamReader -import java.io.OutputStreamWriter -import java.net.ServerSocket -import java.net.Socket -import java.net.SocketTimeoutException -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.locks.ReentrantLock -import kotlin.collections.set -import kotlin.concurrent.withLock - -const val PROTO_VERSION = 1 -const val MAX_STREAMS = 10 -const val SO_TIMEOUT = 20000 -//const val SO_TIMEOUT = 10000000 - -fun ctlMsg(cmd: UInt, data: String? = null) = Msg(cmd, data = data) - -data class ActiveClient( - val clientKey: String, - val socket: Socket, - val reader: BufferedReader, - val writer: BufferedWriter, - val thread: Thread = Thread.currentThread(), - // - val tcpStreams: MutableMap = mutableMapOf(), - val pendingStreams: MutableSet = mutableSetOf(), -) { - val log = logger() - val stubIp = keychipToStubIp(clientKey) - val writeMutex = ReentrantLock() - - var lastHeartbeat = millis() - - fun send(msg: Msg) { - writeMutex.withLock { - try { - writer.write(msg.toString()) - writer.newLine() - writer.flush() - } - catch (e: Exception) { - log.error("Error sending message", e) - socket.close() - thread.interrupt() - } - } - } -} - -fun ActiveClient.handle(msg: Msg) { - // Find target by dst IP address or TCP stream ID - val target = (msg.sid?.let { tcpStreams[it] } ?: msg.dst)?.let { clients[it] } - - when (msg.cmd) { - Command.CTL_HEARTBEAT -> { - lastHeartbeat = millis() - send(ctlMsg(Command.CTL_HEARTBEAT)) - } - Command.DATA_BROADCAST -> { - // Broadcast to all clients. This is only used in UDP so SID is always 0 - if (msg.proto != Proto.UDP) return log.warn("TCP Broadcast received, something is wrong.") - clients.values.forEach { it.send(msg.copy(src = stubIp)) } - } - Command.DATA_SEND -> { - target ?: return log.warn("Send: Target not found: ${msg.dst}") - - if (msg.proto == Proto.TCP && msg.sid !in tcpStreams) - return log.warn("Stream ID not found: ${msg.sid}") - - target.send(msg.copy(src = stubIp, dst = target.stubIp)) - } - Command.CTL_TCP_CONNECT -> { - target ?: return log.warn("Connect: Target not found: ${msg.dst}") - val sid = msg.sid ?: return log.warn("Connect: Stream ID not found") - - if (sid in tcpStreams || sid in pendingStreams) - return log.warn("Stream ID already in use: $sid") - - // Add the stream to the pending list - pendingStreams.add(sid) - if (pendingStreams.size > MAX_STREAMS) { - log.warn("Too many pending streams, closing connection") - return socket.close() - } - - target.send(msg.copy(src = stubIp, dst = target.stubIp)) - } - Command.CTL_TCP_ACCEPT -> { - target ?: return log.warn("Accept: Target not found: ${msg.dst}") - val sid = msg.sid ?: return log.warn("Accept: Stream ID not found") - - if (sid !in target.pendingStreams) - return log.warn("Stream ID not found in pending list: $sid") - - // Add the stream to the active list - target.pendingStreams.remove(sid) - target.tcpStreams[sid] = stubIp - tcpStreams[sid] = target.stubIp - - target.send(msg.copy(src = stubIp, dst = target.stubIp)) - } - } -} - -fun String.hashToUInt() = md5().let { - ((it[0].toUInt() and 0xFFu) shl 24) or - ((it[1].toUInt() and 0xFFu) shl 16) or - ((it[2].toUInt() and 0xFFu) shl 8) or - (it[3].toUInt() and 0xFFu) -} - -fun keychipToStubIp(keychip: String) = keychip.hashToUInt() - -// Keychip ID to Socket -val clients = ConcurrentHashMap() - -/** - * Service for the party linker for AquaMai - */ -class MaimaiFutari(private val port: Int = 20101) { - val log = logger() - - fun start() { - val serverSocket = ServerSocket(port) - log.info("Server started on port $port") - - while (true) { - val clientSocket = serverSocket.accept().apply { - soTimeout = SO_TIMEOUT - log.info("[+] Client connected: $remoteSocketAddress") - } - thread { handleClient(clientSocket) } - } - } - - fun handleClient(socket: Socket) { - val reader = BufferedReader(InputStreamReader(socket.getInputStream())) - val writer = BufferedWriter(OutputStreamWriter(socket.getOutputStream())) - var handler: ActiveClient? = null - - try { - while (!Thread.interrupted() && !socket.isClosed) { - val input = (reader.readLine() ?: break).trim('\uFEFF') - if (input != "1,3") log.info("${socket.remoteSocketAddress} (${handler?.clientKey}) <<< $input") - val message = Msg.fromString(input) - - when (message.cmd) { - // Start: Register the client. Payload is the keychip - Command.CTL_START -> { - val id = message.data as String - val client = ActiveClient(id, socket, reader, writer) - clients[client.stubIp]?.socket?.close() - clients[client.stubIp] = client - handler = clients[client.stubIp] - log.info("[+] Client registered: ${socket.remoteSocketAddress} -> $id") - - // Send back the version - handler?.send(ctlMsg(Command.CTL_START, "version=$PROTO_VERSION")) - } - - // Handle any other command using the handler - else -> { - (handler ?: throw Exception("Client not registered")).handle(message) - } - } - } - } catch (e: Exception) { - if (e.message != "Connection reset" && e !is SocketTimeoutException) - log.error("Error in client handler", e) - } finally { - // Remove client - handler?.stubIp?.let { clients.remove(it) } - socket.close() - log.info("[-] Client disconnected: ${handler?.clientKey}") - } - } -} - -fun main() = MaimaiFutari().start() diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariTypes.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariTypes.kt deleted file mode 100644 index 4544b412..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariTypes.kt +++ /dev/null @@ -1,100 +0,0 @@ -@file:Suppress("PropertyName") - -package icu.samnyan.aqua.sega.maimai2.worldslink - -import ext.* -import kotlinx.serialization.Serializable - -object Command { - // Control plane - const val CTL_START = 1u - const val CTL_BIND = 2u - const val CTL_HEARTBEAT = 3u - const val CTL_TCP_CONNECT = 4u // Accept a new multiplexed TCP stream - const val CTL_TCP_ACCEPT = 5u - const val CTL_TCP_ACCEPT_ACK = 6u - const val CTL_TCP_CLOSE = 7u - - // Data plane - const val DATA_SEND = 21u - const val DATA_BROADCAST = 22u -} - -object Proto { - const val TCP = 6u - const val UDP = 17u -} - -data class Msg( - var cmd: UInt, - var proto: UInt? = null, - var sid: UInt? = null, - var src: UInt? = null, - var sPort: UInt? = null, - var dst: UInt? = null, - var dPort: UInt? = null, - var data: String? = null -) { - override fun toString() = ls( - 1, cmd, proto, sid, src, sPort, dst, dPort, - null, null, null, null, null, null, null, null, // reserved for future use - data - ).joinToString(",") { it?.str ?: "" }.trimEnd(',') - - companion object { - val fields = arr(Msg::proto, Msg::sid, Msg::src, Msg::sPort, Msg::dst, Msg::dPort) - - fun fromString(str: String): Msg { - val sp = str.split(',') - return Msg(0u).apply { - cmd = sp[1].toUInt() - fields.forEachIndexed { i, f -> f.set(this, sp.getOrNull(i + 2)?.some?.toUIntOrNull()) } - data = sp.drop(16).joinToString(",") - } - } - } -} - -@Serializable -data class MechaInfo( - val IsJoin: Bool, - val IpAddress: UInt, - val MusicID: Int, - val Entrys: List, - var UserIDs: List, - val UserNames: List, - val IconIDs: List, - val FumenDifs: List, - val Rateing: List, - val ClassValue: List, - val MaxClassValue: List, - val UserType: List -) - -@Serializable -data class RecruitInfo( - val MechaInfo: MechaInfo, - val MusicID: Int, - val GroupID: Int, - val EventModeID: Boolean, - val JoinNumber: Int, - val PartyStance: Int, - val _startTimeTicks: Long, - val _recvTimeTicks: Long -) - -@Serializable -data class RecruitRecord( - val RecruitInfo: RecruitInfo, - val Keychip: String, - var Server: RelayServerInfo? = null, - var Time: Long = 0, -) - -@Serializable -data class RelayServerInfo( - val name: String, - val addr: String, - val port: Int = 20101, - val official: Bool = true -) diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/model/userdata/UserRival.java b/src/main/java/icu/samnyan/aqua/sega/ongeki/model/userdata/UserRival.java index ca0f9d16..ff469bf0 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/model/userdata/UserRival.java +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/model/userdata/UserRival.java @@ -2,8 +2,6 @@ package icu.samnyan.aqua.sega.ongeki.model.userdata; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import icu.samnyan.aqua.sega.util.jackson.UserIdSerializer; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/src/main/java/icu/samnyan/aqua/sega/util/AllNetBillingDecoder.kt b/src/main/java/icu/samnyan/aqua/sega/util/AllNetBillingDecoder.kt deleted file mode 100644 index 765dcf9f..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/util/AllNetBillingDecoder.kt +++ /dev/null @@ -1,29 +0,0 @@ -package icu.samnyan.aqua.sega.util - -import java.util.* -import kotlin.text.Charsets.UTF_8 - -object AllNetBillingDecoder { - /** - * Decode the input byte array from Base64 MIME encoding and decompress the decoded byte array - */ - fun decode(src: ByteArray, base64: Boolean, nowrap: Boolean): Map { - // Decode the input byte array from Base64 MIME encoding - val bytes = if (!base64) src else Base64.getMimeDecoder().decode(src) - - // Decompress the decoded byte array - val output = ZLib.decompress(bytes, nowrap).toString(UTF_8).trim() - - // Split the string by '&' symbol to separate key-value pairs - return output.split("&").associate { - val (key, value) = it.split("=") - key to value - } - } - - @JvmStatic - fun decodeAllNet(src: ByteArray) = decode(src, base64 = true, nowrap = false) - - @JvmStatic - fun decodeBilling(src: ByteArray) = decode(src, base64 = false, nowrap = true) -} diff --git a/src/main/java/icu/samnyan/aqua/sega/util/jackson/AccessCodeSerializer.java b/src/main/java/icu/samnyan/aqua/sega/util/jackson/AccessCodeSerializer.java deleted file mode 100644 index f031ccd2..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/util/jackson/AccessCodeSerializer.java +++ /dev/null @@ -1,27 +0,0 @@ -package icu.samnyan.aqua.sega.util.jackson; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import icu.samnyan.aqua.sega.general.model.Card; - -import java.io.IOException; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -public class AccessCodeSerializer extends StdSerializer { - - public AccessCodeSerializer() { - this(null); - } - - public AccessCodeSerializer(Class t) { - super(t); - } - - @Override - public void serialize(Card value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeString(value.getLuid()); - } -} diff --git a/src/main/java/icu/samnyan/aqua/sega/util/jackson/BasicMapper.kt b/src/main/java/icu/samnyan/aqua/sega/util/jackson/BasicMapper.kt index 1b7aaf42..758e6c03 100644 --- a/src/main/java/icu/samnyan/aqua/sega/util/jackson/BasicMapper.kt +++ b/src/main/java/icu/samnyan/aqua/sega/util/jackson/BasicMapper.kt @@ -1,55 +1,96 @@ package icu.samnyan.aqua.sega.util.jackson +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.json.JsonWriteFeature import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import ext.jsonArray import org.springframework.stereotype.Component import java.time.LocalDateTime import java.time.format.DateTimeFormatter -interface IMapper { - fun write(o: Any?): String +open class IMapper(val mapper: ObjectMapper) { + fun write(o: Any): String = mapper.writeValueAsString(o) + fun convert(map: Any, to: Class) = mapper.convertValue(map, to) + fun convert(map: Any, to: TypeReference): T = mapper.convertValue(map, to) + fun read(json: String, to: Class) = mapper.readValue(json, to) + fun read(json: String, to: TypeReference) = mapper.readValue(json, to) + + inline fun convert(map: Any): T = convert(map, object : TypeReference() {}) + inline fun read(json: String): T = read(json, object : TypeReference() {}) +} + +val BASIC_MAPPER = jacksonObjectMapper().apply { + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + configure(SerializationFeature.WRITE_ENUMS_USING_INDEX, true) + findAndRegisterModules() + registerModule(SimpleModule().apply { + addSerializer( + LocalDateTime::class.java, + LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")) + ) + addDeserializer( + LocalDateTime::class.java, + LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")) + ) + }) } @Component -class BasicMapper: IMapper { - companion object { - val BASIC_MAPPER = jacksonObjectMapper().apply { - configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - configure(SerializationFeature.WRITE_ENUMS_USING_INDEX, true) - findAndRegisterModules() - registerModule(SimpleModule().apply { - addSerializer( - LocalDateTime::class.java, - LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")) - ) - addDeserializer( - LocalDateTime::class.java, - LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")) - ) - }) - } +class BasicMapper: IMapper(BASIC_MAPPER) + + + +val BOOLEAN_SERIALIZER = object : StdSerializer(Boolean::class.java) { + override fun serialize(v: Boolean, gen: JsonGenerator, p: SerializerProvider) { + gen.writeString(v.toString()) } +} - override fun write(o: Any?) = - BASIC_MAPPER.writeValueAsString(o) +var STRING_MAPPER = jacksonObjectMapper().apply { + enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS.mappedFeature()) + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + configure(SerializationFeature.WRITE_ENUMS_USING_INDEX, true) + findAndRegisterModules() + registerModule(SimpleModule().apply { + addSerializer( + LocalDateTime::class.java, + LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + ) + addDeserializer( + LocalDateTime::class.java, + LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + ) + addSerializer(Boolean::class.javaObjectType, BOOLEAN_SERIALIZER) + addSerializer(Boolean::class.javaPrimitiveType, BOOLEAN_SERIALIZER) + }) +} - fun read(jsonStr: String?, toClass: Class?) = - BASIC_MAPPER.readValue(jsonStr, toClass) +@Component +class StringMapper: IMapper(STRING_MAPPER) - fun read(jsonStr: String?, toValueTypeRef: TypeReference?) = - BASIC_MAPPER.readValue(jsonStr, toValueTypeRef) - fun convert(map: Any?, toClass: Class?) = - BASIC_MAPPER.convertValue(map, toClass) +// Testing code +private class A( + var cat: String = "" +) - fun convert(map: Any?, toValueTypeRef: TypeReference?) = - BASIC_MAPPER.convertValue(map, toValueTypeRef) - - fun toMap(obj: Any?): LinkedHashMap = - BASIC_MAPPER.convertValue(obj, object : TypeReference>() {}) +fun main(args: Array) { + val json = """{"cat":"meow"}""" + val a = BasicMapper().read(json, A::class.java) + println(a.cat) + val lst = """[{"cat":"meow"}, {"cat":"meow"}]""" + val b = BasicMapper().convert(lst.jsonArray(), object : TypeReference>() {}) + println(b[0].cat) + println(b.size) + val c = BasicMapper().convert>(lst.jsonArray()) + println(c[0].cat) } diff --git a/src/main/java/icu/samnyan/aqua/sega/util/jackson/BooleanNumberDeserializer.java b/src/main/java/icu/samnyan/aqua/sega/util/jackson/BooleanNumberDeserializer.java deleted file mode 100644 index aafbc91a..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/util/jackson/BooleanNumberDeserializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package icu.samnyan.aqua.sega.util.jackson; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; - -import java.io.IOException; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -public class BooleanNumberDeserializer extends JsonDeserializer { - - @Override - public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return !"0".equals(p.getText()); - } - -} diff --git a/src/main/java/icu/samnyan/aqua/sega/util/jackson/BooleanNumberSerializer.java b/src/main/java/icu/samnyan/aqua/sega/util/jackson/BooleanNumberSerializer.java deleted file mode 100644 index 66532c6b..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/util/jackson/BooleanNumberSerializer.java +++ /dev/null @@ -1,26 +0,0 @@ -package icu.samnyan.aqua.sega.util.jackson; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; - -import java.io.IOException; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -public class BooleanNumberSerializer extends StdSerializer { - - public BooleanNumberSerializer() { - this(null); - } - - public BooleanNumberSerializer(Class t) { - super(t); - } - - @Override - public void serialize(Boolean value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeNumber(value ? 1 : 0); - } -} diff --git a/src/main/java/icu/samnyan/aqua/sega/util/jackson/BooleanToIntegerDeserializer.kt b/src/main/java/icu/samnyan/aqua/sega/util/jackson/BooleanToIntegerDeserializer.kt deleted file mode 100644 index cb34a5b1..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/util/jackson/BooleanToIntegerDeserializer.kt +++ /dev/null @@ -1,23 +0,0 @@ -package icu.samnyan.aqua.sega.util.jackson - -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.JsonToken -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import ext.int - -class BooleanToIntegerDeserializer : JsonDeserializer() { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Int { - return when (p.currentToken) { - JsonToken.VALUE_STRING -> when (val str = p.valueAsString.lowercase()) { - "true" -> 1 - "false" -> 0 - else -> str.int - } - JsonToken.VALUE_NUMBER_INT -> p.intValue - JsonToken.VALUE_TRUE -> 1 - JsonToken.VALUE_FALSE -> 0 - else -> throw UnsupportedOperationException("Cannot deserialize to boolean int") - } - } -} diff --git a/src/main/java/icu/samnyan/aqua/sega/util/jackson/ByteBufSerializer.java b/src/main/java/icu/samnyan/aqua/sega/util/jackson/ByteBufSerializer.java deleted file mode 100644 index 88b62122..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/util/jackson/ByteBufSerializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package icu.samnyan.aqua.sega.util.jackson; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; - -import java.io.IOException; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -public class ByteBufSerializer extends StdSerializer { - - public ByteBufSerializer() { - this(null); - } - - public ByteBufSerializer(Class t) { - super(t); - } - - @Override - public void serialize(ByteBuf value, JsonGenerator gen, SerializerProvider provider) throws IOException { - - gen.writeString(ByteBufUtil.hexDump(value)); - - } -} diff --git a/src/main/java/icu/samnyan/aqua/sega/util/jackson/Serializers.kt b/src/main/java/icu/samnyan/aqua/sega/util/jackson/Serializers.kt new file mode 100644 index 00000000..369591a5 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/util/jackson/Serializers.kt @@ -0,0 +1,47 @@ +package icu.samnyan.aqua.sega.util.jackson + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import ext.int +import icu.samnyan.aqua.sega.general.model.Card +import java.time.ZonedDateTime + +class AccessCodeSerializer @JvmOverloads constructor(t: Class? = null) : StdSerializer(t) { + override fun serialize(value: Card, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(value.luid) + } +} + +class BooleanToIntegerDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext) = when (p.currentToken) { + JsonToken.VALUE_STRING -> when (val str = p.valueAsString.lowercase()) { + "true" -> 1 + "false" -> 0 + else -> str.int + } + JsonToken.VALUE_NUMBER_INT -> p.intValue + JsonToken.VALUE_TRUE -> 1 + JsonToken.VALUE_FALSE -> 0 + else -> throw UnsupportedOperationException("Cannot deserialize to boolean int") + } +} + +class BooleanNumberSerializer @JvmOverloads constructor(t: Class? = null) : StdSerializer(t) { + override fun serialize(value: Boolean, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeNumber(if (value) 1 else 0) + } +} + +class BooleanNumberDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext) = "0" != p.text +} + +class ZonedDateTimeDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext) = ZonedDateTime.parse(p.text) +} + diff --git a/src/main/java/icu/samnyan/aqua/sega/util/jackson/StringMapper.kt b/src/main/java/icu/samnyan/aqua/sega/util/jackson/StringMapper.kt deleted file mode 100644 index d72559f0..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/util/jackson/StringMapper.kt +++ /dev/null @@ -1,51 +0,0 @@ -package icu.samnyan.aqua.sega.util.jackson - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.json.JsonWriteFeature -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.databind.ser.std.StdSerializer -import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer -import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import org.springframework.stereotype.Component -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - - -@Component -class StringMapper: IMapper { - override fun write(o: Any?) = STRING_MAPPER.writeValueAsString(o) - fun convert(map: Any?, toClass: Class?) = STRING_MAPPER.convertValue(map, toClass) - final inline fun convert(map: Any?) = convert(map, T::class.java) - fun toMap(obj: Any?) = STRING_MAPPER.convertValue(obj, object : TypeReference>() {}) - - companion object { - val BOOLEAN_SERIALIZER = object : StdSerializer(Boolean::class.java) { - override fun serialize(v: Boolean, gen: JsonGenerator, p: SerializerProvider) { - gen.writeString(v.toString()) - } - } - var STRING_MAPPER = jacksonObjectMapper().apply { - enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS.mappedFeature()) - configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - configure(SerializationFeature.WRITE_ENUMS_USING_INDEX, true) - findAndRegisterModules() - registerModule(SimpleModule().apply { - addSerializer( - LocalDateTime::class.java, - LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - ) - addDeserializer( - LocalDateTime::class.java, - LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - ) - addSerializer(Boolean::class.javaObjectType, BOOLEAN_SERIALIZER) - addSerializer(Boolean::class.javaPrimitiveType, BOOLEAN_SERIALIZER) - }) - } - } -} diff --git a/src/main/java/icu/samnyan/aqua/sega/util/jackson/UserIdSerializer.java b/src/main/java/icu/samnyan/aqua/sega/util/jackson/UserIdSerializer.java deleted file mode 100644 index c17a1841..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/util/jackson/UserIdSerializer.java +++ /dev/null @@ -1,27 +0,0 @@ -package icu.samnyan.aqua.sega.util.jackson; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import icu.samnyan.aqua.sega.ongeki.model.userdata.UserData; - -import java.io.IOException; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -public class UserIdSerializer extends StdSerializer { - - public UserIdSerializer() { - this(null); - } - - public UserIdSerializer(Class t) { - super(t); - } - - @Override - public void serialize(UserData value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeNumber(value.getCard().getExtId()); - } -} diff --git a/src/main/java/icu/samnyan/aqua/sega/util/jackson/ZonedDateTimeDeserializer.java b/src/main/java/icu/samnyan/aqua/sega/util/jackson/ZonedDateTimeDeserializer.java deleted file mode 100644 index fcdaa23c..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/util/jackson/ZonedDateTimeDeserializer.java +++ /dev/null @@ -1,19 +0,0 @@ -package icu.samnyan.aqua.sega.util.jackson; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; - -import java.io.IOException; -import java.time.ZonedDateTime; - -/** - * @author samnyan (privateamusement@protonmail.com) - */ -public class ZonedDateTimeDeserializer extends JsonDeserializer { - - @Override - public ZonedDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return ZonedDateTime.parse(p.getText()); - } -}