forked from Cookies_Public/AquaDX
AquaTrans and stuff (#131)
This commit is contained in:
commit
7fb46441f4
@ -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 @@
|
||||
<Route path="/u/:username/:game" component={UserHome} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/pictures" component={MaiPhoto} />
|
||||
<Route path="/transfer" component={Transfer} />
|
||||
</Router>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@ -12,6 +12,11 @@
|
||||
export let confirm: ConfirmProps | null = null
|
||||
export let error: string | null
|
||||
export let loading: boolean = false
|
||||
|
||||
function doConfirm(fn?: () => void) {
|
||||
confirm = null
|
||||
fn && fn()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if confirm}
|
||||
@ -22,15 +27,9 @@
|
||||
|
||||
<div class="actions">
|
||||
{#if confirm.cancel}
|
||||
<!-- Svelte LSP is very annoying here -->
|
||||
<button on:click={() => {
|
||||
confirm && confirm.cancel && confirm.cancel()
|
||||
|
||||
// Set to null
|
||||
confirm = null
|
||||
}}>{t('action.cancel')}</button>
|
||||
<button on:click={() => doConfirm(confirm?.cancel)}>{t('action.cancel')}</button>
|
||||
{/if}
|
||||
<button on:click={() => confirm && confirm.confirm()} class:error={confirm.dangerous}>{t('action.confirm')}</button>
|
||||
<button on:click={() => doConfirm(confirm?.confirm)} class:error={confirm.dangerous}>{t('action.confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import StatusOverlays from "../StatusOverlays.svelte";
|
||||
import { GAME } from "../../libs/sdk";
|
||||
import GameSettingFields from "./GameSettingFields.svelte";
|
||||
import { download } from "../../libs/ui";
|
||||
|
||||
const profileFields = [
|
||||
['name', t('settings.mai2.name')],
|
||||
@ -42,15 +43,6 @@
|
||||
.catch(e => error = e.message)
|
||||
.finally(() => submitting = "")
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fields" out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
|
||||
@ -4,13 +4,16 @@
|
||||
import { DISCORD_INVITE } from "../../libs/config";
|
||||
|
||||
export let error: string;
|
||||
export let expected: boolean = false;
|
||||
</script>
|
||||
|
||||
<div class="overlay" transition:fade>
|
||||
<div>
|
||||
<h2 class="error">{t('status.error')}</h2>
|
||||
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
|
||||
<span>{t('status.detail', { detail: error })}</span>
|
||||
{#if !expected}
|
||||
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
|
||||
{/if}
|
||||
<span class="detail">{error}</span>
|
||||
|
||||
<div class="actions">
|
||||
<button on:click={() => location.reload()} class="error">
|
||||
@ -27,4 +30,10 @@
|
||||
|
||||
button
|
||||
width: 100%
|
||||
|
||||
.detail
|
||||
white-space: pre-line
|
||||
font-size: 0.9em
|
||||
line-height: 1.2
|
||||
opacity: 0.8
|
||||
</style>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
export type Dict = Record<string, any>
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -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<Response> {
|
||||
export function fetchWithParams(input: URL | RequestInfo, init?: ExtReqInit): Promise<Response> {
|
||||
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<string, any> = {}, init?: RequestInitWithParams): Promise<any> {
|
||||
// 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<any> {
|
||||
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<any> {
|
||||
// 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<any> {
|
||||
// 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<void> {
|
||||
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<any> {
|
||||
// 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<any> {
|
||||
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<UserBox> =>
|
||||
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<Record<string, any>> =>
|
||||
post(`/api/v2/game/${game}/export`),
|
||||
import: (game: GameName, data: any): Promise<Record<string, any>> =>
|
||||
post(`/api/v2/game/${game}/import`, {}, { body: JSON.stringify(data) }),
|
||||
post(`/api/v2/game/${game}/import`, {}, { json: data }),
|
||||
importMusicDetail: (game: GameName, data: any): Promise<Record<string, any>> =>
|
||||
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<TrCheckGood> =>
|
||||
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 } }),
|
||||
}
|
||||
|
||||
@ -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<any> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
26
AquaNet/src/pages/Transfer/InputTextShort.svelte
Normal file
26
AquaNet/src/pages/Transfer/InputTextShort.svelte
Normal file
@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
export let desc: string
|
||||
export let value: string
|
||||
export let placeholder: string
|
||||
export let flex: number = 60
|
||||
|
||||
export let disabled: boolean = false
|
||||
|
||||
export let validate: (value: string) => boolean = () => true
|
||||
</script>
|
||||
|
||||
<div class="field" style="flex: {flex}">
|
||||
<label for={desc}>{desc}</label>
|
||||
<input type="text" placeholder={placeholder} bind:value={value} id="{desc}" on:change
|
||||
class:error={value && !validate(value)} {disabled}/>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
.field
|
||||
display: inline-flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
label
|
||||
font-weight: bold
|
||||
</style>
|
||||
129
AquaNet/src/pages/Transfer/Transfer.svelte
Normal file
129
AquaNet/src/pages/Transfer/Transfer.svelte
Normal file
@ -0,0 +1,129 @@
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
import { t, ts } from "../../libs/i18n";
|
||||
import TransferServer from "./TransferServer.svelte";
|
||||
import { DATA_HOST } from "../../libs/config";
|
||||
import type { ConfirmProps } from "../../libs/generalTypes";
|
||||
import StatusOverlays from "../../components/StatusOverlays.svelte";
|
||||
|
||||
|
||||
let tabs = ['chu3', 'mai2', 'ongeki']
|
||||
let game: Record<string, { game: string, version: string }> = {
|
||||
'chu3': { game: "SDHD", version: "2.30" },
|
||||
'mai2': { game: "SDGA", version: "1.50" },
|
||||
'ongeki': { game: "SDDT", version: "1.45" }
|
||||
}
|
||||
let tab = 0
|
||||
|
||||
let src = JSON.parse(localStorage.getItem('src') ?? `{"dns": "", "card": "", "keychip": ""}`)
|
||||
let dst = JSON.parse(localStorage.getItem('dst') ?? `{"dns": "", "card": "", "keychip": ""}`)
|
||||
let [srcTested, dstTested] = [false, false]
|
||||
let gameInfo = JSON.parse(localStorage.getItem('gameInfo') ?? `{"game": "", "version": ""}`)
|
||||
|
||||
let srcEl: TransferServer, dstEl: TransferServer
|
||||
let srcExportedData: string
|
||||
let [error, loading] = ["", false]
|
||||
let confirm: ConfirmProps | null = null
|
||||
|
||||
function defaultGame() {
|
||||
gameInfo.game = game[tabs[tab]].game
|
||||
gameInfo.version = game[tabs[tab]].version
|
||||
}
|
||||
|
||||
function onChange() {
|
||||
localStorage.setItem('src', JSON.stringify(src))
|
||||
localStorage.setItem('dst', JSON.stringify(dst))
|
||||
localStorage.setItem('gameInfo', JSON.stringify(gameInfo))
|
||||
}
|
||||
|
||||
function actuallyStartTransfer() {
|
||||
srcEl.pull()
|
||||
.then(() => dstEl.push(srcExportedData))
|
||||
.then(() => confirm = {
|
||||
title: "Done!",
|
||||
message: `Transfer completed successfully! Your data on ${dst.dns} is overwritten with your data from ${src.dns}.`
|
||||
})
|
||||
.catch(e => error = e)
|
||||
.finally(() => loading = false)
|
||||
}
|
||||
|
||||
function startTransfer() {
|
||||
if (!(srcTested && dstTested)) return alert("Please test both servers first!")
|
||||
if (loading) return alert("Transfer already in progress!")
|
||||
console.log("Starting transfer...")
|
||||
loading = true
|
||||
|
||||
if (dstEl.exportedData) return actuallyStartTransfer()
|
||||
|
||||
// Ask user to make sure to backup their data
|
||||
confirm = {
|
||||
title: "Confirm transfer",
|
||||
message: "It seems like you haven't backed up your destination data. Are you sure you want to proceed? (This will overwrite your destination server's data)",
|
||||
dangerous: true,
|
||||
confirm: actuallyStartTransfer,
|
||||
cancel: () => { loading = false }
|
||||
}
|
||||
}
|
||||
|
||||
defaultGame()
|
||||
</script>
|
||||
|
||||
<StatusOverlays bind:confirm={confirm} {error} />
|
||||
|
||||
<main class="content">
|
||||
<div class="outer-title-options">
|
||||
<h2>🏳️⚧️ AquaTrans™ Data Transfer?</h2>
|
||||
<nav>
|
||||
{#each tabs as tabName, i}
|
||||
<div transition:slide={{axis: 'x'}} class:active={tab === i}
|
||||
on:click={() => tab = i} on:keydown={e => e.key === 'Enter' && (tab = i)}
|
||||
role="button" tabindex="0">
|
||||
{ts(`settings.tabs.${tabName}`)}
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="prompt">
|
||||
<p>👋 Welcome to the AquaTrans™ server data transfer tool!</p>
|
||||
<p>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).</p>
|
||||
<p>This tool will simulate a game client and pull your data from the source server, and push your data to the destination server.</p>
|
||||
<p>Please fill out the info below to get started!</p>
|
||||
</div>
|
||||
|
||||
<TransferServer bind:src={src} bind:gameInfo={gameInfo} on:change={onChange}
|
||||
bind:tested={srcTested} bind:this={srcEl} bind:exportedData={srcExportedData} />
|
||||
|
||||
<div class="arrow" class:disabled={!(srcTested && dstTested)}>
|
||||
<img src="{DATA_HOST}/d/DownArrow.png" alt="arrow" on:click={startTransfer}>
|
||||
</div>
|
||||
|
||||
<TransferServer bind:src={dst} bind:gameInfo={gameInfo} on:change={onChange}
|
||||
bind:tested={dstTested} bind:this={dstEl} isSrc={false} />
|
||||
</main>
|
||||
|
||||
|
||||
<style lang="sass">
|
||||
.arrow
|
||||
width: 100%
|
||||
display: flex
|
||||
justify-content: center
|
||||
margin-top: -40px
|
||||
margin-bottom: -40px
|
||||
z-index: 1
|
||||
|
||||
&.disabled
|
||||
filter: grayscale(1)
|
||||
|
||||
// CSS animation to let the image opacity breathe
|
||||
img
|
||||
animation: breathe 1s infinite alternate
|
||||
|
||||
@keyframes breathe
|
||||
0%
|
||||
opacity: 0.5
|
||||
100%
|
||||
opacity: 1
|
||||
</style>
|
||||
|
||||
|
||||
21
AquaNet/src/pages/Transfer/TransferLib.ts
Normal file
21
AquaNet/src/pages/Transfer/TransferLib.ts
Normal file
@ -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 }
|
||||
|
||||
198
AquaNet/src/pages/Transfer/TransferServer.svelte
Normal file
198
AquaNet/src/pages/Transfer/TransferServer.svelte
Normal file
@ -0,0 +1,198 @@
|
||||
<script lang="ts">
|
||||
import StatusOverlays from "../../components/StatusOverlays.svelte";
|
||||
import { TRANSFER } from "../../libs/sdk";
|
||||
import { download, selectJsonFile } from "../../libs/ui";
|
||||
import InputTextShort from "./InputTextShort.svelte";
|
||||
|
||||
export let src: AllNetSrc
|
||||
export let gameInfo: AllNetGame
|
||||
export let isSrc: boolean = true
|
||||
|
||||
export let tested: boolean = false
|
||||
let [loading, error, expectedError] = [false, "", ""]
|
||||
|
||||
function testConnection() {
|
||||
if (loading) return
|
||||
|
||||
// Preliminiary checks
|
||||
if (!src.dns || !src.keychip || !src.card || !gameInfo.game || !gameInfo.version) {
|
||||
error = "Please fill out all fields"
|
||||
return
|
||||
}
|
||||
|
||||
loading = true
|
||||
console.log("Testing connection...")
|
||||
TRANSFER.check({...src, ...gameInfo}).then(res => {
|
||||
console.log("Connection test result:", res)
|
||||
tested = true
|
||||
}).catch(err => expectedError = err.message).finally(() => loading = false)
|
||||
}
|
||||
|
||||
let messages: string[] = []
|
||||
export let exportedData: string = ""
|
||||
|
||||
export function pull(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (loading || !tested) return reject("Please test connection first")
|
||||
if (exportedData) return resolve(exportedData)
|
||||
console.log("Exporting data...")
|
||||
|
||||
TRANSFER.pull({...src, ...gameInfo}, (msg: TrStreamMessage) => {
|
||||
console.log("Export progress: ", JSON.stringify(msg))
|
||||
|
||||
if ('message' in msg) messages = [...messages, msg.message]
|
||||
|
||||
if ('error' in msg) {
|
||||
expectedError = msg.error
|
||||
reject(msg.error)
|
||||
}
|
||||
|
||||
if ('data' in msg) {
|
||||
// file name: Export YYYY-MM-DD {server host} {game} {card last 6}.json
|
||||
let date = new Date().toISOString().split('T')[0]
|
||||
let host = new URL(src.dns).hostname
|
||||
download(msg.data, `Export ${date} ${host} ${gameInfo.game} ${src.card.slice(-6)}.json`)
|
||||
exportedData = msg.data
|
||||
resolve(msg.data)
|
||||
}
|
||||
}).catch(err => { expectedError = err; reject(err) })
|
||||
})
|
||||
}
|
||||
|
||||
function pushBtn() {
|
||||
if (loading || !tested) return
|
||||
selectJsonFile().then(obj => push(JSON.stringify(obj)))
|
||||
}
|
||||
|
||||
export function push(data: string) {
|
||||
if (loading || !tested) return
|
||||
console.log("Import data...")
|
||||
loading = true
|
||||
|
||||
return TRANSFER.push({...src, ...gameInfo}, data).then(() => {
|
||||
console.log("Data imported successfully")
|
||||
messages = ["Data imported successfully"]
|
||||
}).catch(err => expectedError = err.message).finally(() => loading = false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<StatusOverlays {loading} {error} />
|
||||
|
||||
<div class="server source" class:src={isSrc} class:hasError={expectedError} class:tested={tested}>
|
||||
<h3>{isSrc ? "Source" : "Target"} Server</h3>
|
||||
|
||||
{#if expectedError}
|
||||
<blockquote class="error-msg">{expectedError}</blockquote>
|
||||
{/if}
|
||||
|
||||
<!-- First input line -->
|
||||
<div class="inputs">
|
||||
<InputTextShort desc="Server Address" placeholder="e.g. http://aquadx.hydev.org"
|
||||
bind:value={src.dns} on:change validate={v => /^https?:\/\/[a-z0-9.-]+(:\d+)?$/i.test(v)} disabled={tested} />
|
||||
<InputTextShort desc="Keychip ID" placeholder="e.g. A0299792458"
|
||||
bind:value={src.keychip} on:change validate={v => /^([A-Z0-9]{11}|[A-Z0-9]{4}-[A-Z0-9]{11})$/.test(v)} disabled={tested} />
|
||||
</div>
|
||||
|
||||
<!-- Second input line -->
|
||||
<div class="inputs">
|
||||
<div class="game-version">
|
||||
<InputTextShort desc="Game" placeholder="e.g. SDHD"
|
||||
bind:value={gameInfo.game} on:change disabled={tested} />
|
||||
<InputTextShort desc="Version" placeholder="e.g. 2.30"
|
||||
bind:value={gameInfo.version} on:change disabled={tested} />
|
||||
</div>
|
||||
<InputTextShort desc="Card Number" placeholder="e.g. 27182818284590452353"
|
||||
bind:value={src.card} on:change disabled={tested} />
|
||||
</div>
|
||||
|
||||
<!-- Streaming messages -->
|
||||
{#if messages.length > 0}
|
||||
<div class="stream-messages">
|
||||
{#each messages.slice(Math.max(messages.length - 5, 0), undefined) as msg}
|
||||
<p>{msg}</p>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="inputs buttons">
|
||||
{#if !tested}
|
||||
<button class="flex-1" on:click={testConnection} disabled={loading}>Test Connection</button>
|
||||
{:else}
|
||||
<button class="flex-1" on:click={pull}>Export Data</button>
|
||||
<button class="flex-1" on:click={pushBtn}>Import Data</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@use "../../vars"
|
||||
@use "sass:color"
|
||||
|
||||
.error-msg
|
||||
white-space: pre-wrap
|
||||
margin: 0
|
||||
|
||||
.server
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
// --c-src: 202, 168, 252
|
||||
--c-src: 179, 198, 255
|
||||
// animation: hue-rotate 10s infinite linear
|
||||
// &.src
|
||||
// --c-src: 173, 192, 247
|
||||
// animation: hue-rotate 10s infinite linear reverse
|
||||
|
||||
&.tested
|
||||
--c-src: 169, 255, 186
|
||||
|
||||
&.hasError
|
||||
--c-src: 255, 174, 174
|
||||
animation: none
|
||||
|
||||
padding: 1rem
|
||||
border-radius: vars.$border-radius
|
||||
// background-color: vars.$ov-light
|
||||
background: #252525
|
||||
|
||||
// Pink outline
|
||||
border: 1px solid rgba(var(--c-src), 0.5)
|
||||
box-shadow: 0 0 1rem 0 rgba(var(--c-src), 0.25)
|
||||
|
||||
h3
|
||||
margin: 0
|
||||
font-size: 1.5rem
|
||||
text-align: center
|
||||
|
||||
|
||||
// @keyframes hue-rotate
|
||||
// 0%
|
||||
// filter: hue-rotate(0deg)
|
||||
// 100%
|
||||
// filter: hue-rotate(360deg)
|
||||
|
||||
.inputs
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
gap: 1rem
|
||||
|
||||
.game-version
|
||||
flex: 60
|
||||
display: flex
|
||||
gap: 1rem
|
||||
|
||||
:global(> *)
|
||||
width: 100px
|
||||
|
||||
&.buttons
|
||||
margin-top: 0.5rem
|
||||
|
||||
.stream-messages
|
||||
font-size: 0.8rem
|
||||
opacity: 0.8
|
||||
|
||||
margin-top: 0.5rem
|
||||
padding: 0 0.5rem
|
||||
</style>
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||

|
||||
BIN
docs/sqlite-sucks.png
Normal file
BIN
docs/sqlite-sucks.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 972 KiB |
@ -192,8 +192,7 @@ val Any?.str get() = toString()
|
||||
// Collections
|
||||
fun <T> ls(vararg args: T) = args.toList()
|
||||
inline fun <reified T> arr(vararg args: T) = arrayOf(*args)
|
||||
operator fun <K, V> Map<K, V>.plus(map: Map<K, V>) =
|
||||
(if (this is MutableMap) this else mut).apply { putAll(map) }
|
||||
operator fun <K, V> Map<K, V>.plus(map: Map<K, V>) = mut.apply { putAll(map) }
|
||||
operator fun <K, V> MutableMap<K, V>.plusAssign(map: Map<K, V>) { putAll(map) }
|
||||
fun <K, V: Any> Map<K, V?>.vNotNull(): Map<K, V> = filterValues { it != null }.mapValues { it.value!! }
|
||||
fun <T> MutableList<T>.popAll(list: List<T>) = 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: Any> T.logger() = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
|
||||
34
src/main/java/ext/Http.kt
Normal file
34
src/main/java/ext/Http.kt
Normal file
@ -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<Any, Any>) = 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 <reified T> HttpResponse<String>.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 <T> HttpResponse<T>.header(key: String) = headers().firstValue(key).orElse(null)
|
||||
fun HttpResponse<ByteArray>.bodyString() = body()?.toString(Charsets.UTF_8)
|
||||
fun HttpResponse<ByteArray>.bodyZ() = body()?.let { ZLib.decompress(it)?.decodeToString() }
|
||||
fun HttpResponse<ByteArray>.bodyMaybeZ() = if (header("Content-Encoding") == "deflate") bodyZ() else bodyString()
|
||||
@ -43,16 +43,18 @@ else JACKSON.readValue(this, cls)
|
||||
fun <T> T.toJson() = JACKSON.writeValueAsString(this)
|
||||
|
||||
inline fun <reified T> 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<String, Any?> = json()
|
||||
fun String.jsonArray(): List<Map<String, Any?>> = json()
|
||||
|
||||
fun String.jsonMap(): Map<String, Any?> = json() ?: emptyMap()
|
||||
fun String.jsonArray(): List<Map<String, Any?>> = json() ?: emptyList()
|
||||
fun String.jsonMaybeMap(): Map<String, Any?>? = json()
|
||||
fun String.jsonMaybeArray(): List<Map<String, Any?>>? = json()
|
||||
|
||||
// KotlinX Serialization
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
|
||||
@ -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<String>) {
|
||||
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()
|
||||
|
||||
@ -7,12 +7,12 @@ data class Chu3DataExport(
|
||||
override var gameId: String = "SDHD",
|
||||
override var userData: Chu3UserData,
|
||||
var userGameOption: UserGameOption,
|
||||
var userActivityList: List<UserActivity>,
|
||||
var userActivityList: List<Chu3UserActivity>,
|
||||
var userCharacterList: List<UserCharacter>,
|
||||
var userChargeList: List<UserCharge>,
|
||||
var userCourseList: List<UserCourse>,
|
||||
var userDuelList: List<UserDuel>,
|
||||
var userItemList: List<UserItem>,
|
||||
var userItemList: List<Chu3UserItem>,
|
||||
var userMapList: List<UserMap>,
|
||||
var userMusicDetailList: List<UserMusicDetail>,
|
||||
var userPlaylogList: List<UserPlaylog>,
|
||||
|
||||
@ -17,13 +17,13 @@ import java.util.List;
|
||||
public class ChuniDataImport {
|
||||
private String gameId;
|
||||
private ExternalUserData userData;
|
||||
private List<UserActivity> userActivityList;
|
||||
private List<Chu3UserActivity> userActivityList;
|
||||
private List<UserCharacter> userCharacterList;
|
||||
private List<UserCharge> userChargeList;
|
||||
private List<UserCourse> userCourseList;
|
||||
private List<UserDuel> userDuelList;
|
||||
private UserGameOption userGameOption;
|
||||
private List<UserItem> userItemList;
|
||||
private List<Chu3UserItem> userItemList;
|
||||
private List<UserMap> userMapList;
|
||||
private List<UserMusicDetail> userMusicDetailList;
|
||||
private List<UserPlaylog> userPlaylogList;
|
||||
|
||||
@ -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)),
|
||||
|
||||
56
src/main/java/icu/samnyan/aqua/net/transfer/AllNetClient.kt
Normal file
56
src/main/java/icu/samnyan/aqua/net/transfer/AllNetClient.kt
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
169
src/main/java/icu/samnyan/aqua/net/transfer/DataBroker.kt
Normal file
169
src/main/java/icu/samnyan/aqua/net/transfer/DataBroker.kt
Normal file
@ -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 <reified T> String.getNullable(key: String, data: JDict): T? = "$url/$this".request()
|
||||
.postZ(mapper.write(data))
|
||||
.bodyMaybeZ()
|
||||
?.jsonMaybeMap()?.get(key)
|
||||
?.let { mapper.convert<T>(it) }
|
||||
?.also {
|
||||
if (it is List<*>) log("✅ $this: ${it.size}")
|
||||
else log("✅ $this")
|
||||
}
|
||||
|
||||
inline fun <reified T> 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<Map<String, Long>, MutableMap<String, Long>> {
|
||||
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<UserMusicDetail>)
|
||||
|
||||
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<List<Chu3UserActivity>>("userActivityList", userId + mapOf("kind" to it))
|
||||
}
|
||||
userItemList = (1..12).flatMap {
|
||||
"GetUserItemApi".get<List<Chu3UserItem>>("userItemList", paged + mapOf("nextIndex" to 10000000000 * it))
|
||||
}
|
||||
userRecentRatingList = "GetUserRecentRatingApi".get("userRecentRatingList", userId)
|
||||
userMusicDetailList = "GetUserMusicApi".get<List<UserMusicWrapper>>("userMusicList", paged)
|
||||
.flatMap { it.userMusicDetailList }
|
||||
userCourseList = "GetUserCourseApi".get("userCourseList", paged)
|
||||
userFavoriteMusicList = "GetUserFavoriteItemApi".get("userFavoriteItemList", paged + mapOf("kind" to 1))
|
||||
// TODO userMapAreaList = "GetUserMapAreaApi"
|
||||
// TODO userNetBattleData = ls("GetUserNetBattleDataApi".get("userNetBattleData", userId))
|
||||
userUnlockChallengeList = "GetUserUCApi".get("userUnlockChallengeList", userId)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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<Mai2UserMusicDetail>)
|
||||
|
||||
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<List<UserMusicWrapper>>("userMusicList", paged)
|
||||
.flatMap { it.userMusicDetailList }
|
||||
userFriendSeasonRankingList = "GetUserFriendSeasonRankingApi".get("userFriendSeasonRankingList", paged)
|
||||
userCharacterList = "GetUserCharacterApi".get("userCharacterList", paged)
|
||||
userItemList = (1..12).flatMap {
|
||||
"GetUserItemApi".get<List<Mai2UserItem>>("userItemList", paged + mapOf("nextIndex" to 10000000000 * it))
|
||||
}
|
||||
userCourseList = "GetUserCourseApi".get("userCourseList", paged)
|
||||
userFavoriteList = (1..5).mapNotNull {
|
||||
"GetUserFavoriteApi".getNullable<Mai2UserFavorite>("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<List<UserItem>>("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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
65
src/main/java/icu/samnyan/aqua/net/transfer/TransferApis.kt
Normal file
65
src/main/java/icu/samnyan/aqua/net/transfer/TransferApis.kt
Normal file
@ -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}"
|
||||
}
|
||||
}
|
||||
@ -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')
|
||||
|
||||
97
src/main/java/icu/samnyan/aqua/sega/aimedb/AimeDbClient.kt
Normal file
97
src/main/java/icu/samnyan/aqua/sega/aimedb/AimeDbClient.kt
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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<String, String> {
|
||||
// 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<String, String>, 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<String, String>) = encode(src, base64 = true)
|
||||
|
||||
@JvmStatic
|
||||
fun decodeBilling(src: ByteArray) = decode(src, base64 = false, nowrap = true)
|
||||
|
||||
fun HttpResponse<ByteArray>.decodeAllNetResp() =
|
||||
if (header("Pragma") == "DFI") decodeAllNet(body())
|
||||
else bodyString()?.urlToMap()
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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) })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<UpsertUserGacha>(data["cmUpsertUserGacha"]!!) }
|
||||
|
||||
upsertUserGacha.gameGachaCardList?.let { lst ->
|
||||
db.userCardPrintState.saveAll(lst.map {
|
||||
@ -63,7 +62,7 @@ fun ChusanController.cmApiInit() {
|
||||
}
|
||||
|
||||
"CMUpsertUserPrintCancel" {
|
||||
val orderIdList: List<Long> = cmMapper.convert(data["orderIdList"], object : TypeReference<List<Long>>() {})
|
||||
val orderIdList: List<Long> = cmMapper.convert<List<Long>>(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<List<UserItem>>() {})
|
||||
val userCardPrintState = cmMapper.convert<UserCardPrintState>(parsing { data["userCardPrintState"]!! })
|
||||
val userItemList = cmMapper.convert<List<Chu3UserItem>>(parsing { data["userItemList"]!! })
|
||||
|
||||
val u = db.userData.findByCard_ExtId(uid)() ?: return@api null
|
||||
|
||||
|
||||
@ -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<Chu3UserAll>(data["upsertUserAll"]!!) }
|
||||
|
||||
req.run {
|
||||
// UserData
|
||||
|
||||
@ -40,11 +40,11 @@ interface Chu3UserLoginBonusRepo : JpaRepository<UserLoginBonus, Long> {
|
||||
fun findLoginBonus(userId: Int, version: Int, presetId: Long): Optional<UserLoginBonus>
|
||||
}
|
||||
|
||||
interface Chu3UserActivityRepo : Chu3UserLinked<UserActivity> {
|
||||
fun findTopByUserAndActivityIdAndKindOrderByIdDesc(user: Chu3UserData, activityId: Int, kind: Int): Optional<UserActivity>
|
||||
fun findByUserAndActivityIdAndKind(user: Chu3UserData, activityId: Int, kind: Int): UserActivity?
|
||||
interface Chu3UserActivityRepo : Chu3UserLinked<Chu3UserActivity> {
|
||||
fun findTopByUserAndActivityIdAndKindOrderByIdDesc(user: Chu3UserData, activityId: Int, kind: Int): Optional<Chu3UserActivity>
|
||||
fun findByUserAndActivityIdAndKind(user: Chu3UserData, activityId: Int, kind: Int): Chu3UserActivity?
|
||||
|
||||
fun findAllByUser_Card_ExtIdAndKind(extId: Long, kind: Int): List<UserActivity>
|
||||
fun findAllByUser_Card_ExtIdAndKind(extId: Long, kind: Int): List<Chu3UserActivity>
|
||||
}
|
||||
|
||||
interface Chu3UserCardPrintStateRepo : Chu3UserLinked<UserCardPrintState> {
|
||||
@ -89,14 +89,14 @@ interface Chu3UserGeneralDataRepo : Chu3UserLinked<UserGeneralData> {
|
||||
fun findByUser_Card_ExtIdAndPropertyKey(extId: Long, key: String): Optional<UserGeneralData>
|
||||
}
|
||||
|
||||
interface Chu3UserItemRepo : Chu3UserLinked<UserItem> {
|
||||
fun findAllByUser(user: Chu3UserData): List<UserItem>
|
||||
fun findTopByUserAndItemIdAndItemKindOrderByIdDesc(user: Chu3UserData, itemId: Int, itemKind: Int): Optional<UserItem>
|
||||
fun findByUserAndItemIdAndItemKind(user: Chu3UserData, itemId: Int, itemKind: Int): UserItem?
|
||||
interface Chu3UserItemRepo : Chu3UserLinked<Chu3UserItem> {
|
||||
fun findAllByUser(user: Chu3UserData): List<Chu3UserItem>
|
||||
fun findTopByUserAndItemIdAndItemKindOrderByIdDesc(user: Chu3UserData, itemId: Int, itemKind: Int): Optional<Chu3UserItem>
|
||||
fun findByUserAndItemIdAndItemKind(user: Chu3UserData, itemId: Int, itemKind: Int): Chu3UserItem?
|
||||
|
||||
fun findAllByUser_Card_ExtIdAndItemKind(extId: Long, itemKind: Int, pageable: Pageable): Page<UserItem>
|
||||
fun findAllByUser_Card_ExtIdAndItemKind(extId: Long, itemKind: Int, pageable: Pageable): Page<Chu3UserItem>
|
||||
|
||||
fun findAllByUser_Card_ExtIdAndItemKind(extId: Long, itemKind: Int): List<UserItem>
|
||||
fun findAllByUser_Card_ExtIdAndItemKind(extId: Long, itemKind: Int): List<Chu3UserItem>
|
||||
}
|
||||
|
||||
interface Chu3UserMapRepo : Chu3UserLinked<UserMap> {
|
||||
|
||||
@ -37,13 +37,13 @@ data class MusicIdWrapper(
|
||||
val musicId: Int = 0,
|
||||
)
|
||||
|
||||
class UpsertUserAll(
|
||||
class Chu3UserAll(
|
||||
var userData: List<Chu3UserData>? = null,
|
||||
var userGameOption: List<UserGameOption>? = null,
|
||||
var userCharacterList: List<UserCharacter>? = null,
|
||||
var userItemList: List<UserItem>? = null,
|
||||
var userItemList: List<Chu3UserItem>? = null,
|
||||
var userMusicDetailList: List<UserMusicDetail>? = null,
|
||||
var userActivityList: List<UserActivity>? = null,
|
||||
var userActivityList: List<Chu3UserActivity>? = null,
|
||||
var userRecentRatingList: List<UserRecentRating>? = null,
|
||||
var userPlaylogList: List<UserPlaylog>? = null,
|
||||
var userChargeList: List<UserCharge>? = null,
|
||||
@ -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<Any>? = null
|
||||
var userCardList: List<Any>? = null
|
||||
var gameGachaCardList: List<GameGachaCard>? = null
|
||||
var userItemList: List<UserItem>? = null
|
||||
var userItemList: List<Chu3UserItem>? = null
|
||||
|
||||
@JsonProperty("isNewCharacterList")
|
||||
var isNewCharacterList: String? = null
|
||||
|
||||
@ -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")
|
||||
@ -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,
|
||||
@ -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<String, SpecialHandler>()
|
||||
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 } }
|
||||
|
||||
@ -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) }
|
||||
|
||||
|
||||
@ -68,9 +68,10 @@ class Maimai2ServletController(
|
||||
@API("/{api}")
|
||||
fun handle(@PathVariable api: String, @RequestBody data: Map<String, Any>, 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)}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String, ?> request) throws JsonProcessingException {
|
||||
if (enable) {
|
||||
var userId = ((Number) request.get("userId")).longValue();
|
||||
var list = new ArrayList<UserPortrait>();
|
||||
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<String, Object>();
|
||||
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\":[]}";
|
||||
}
|
||||
}
|
||||
@ -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<String, Any>): Any? {
|
||||
if (!enable) return """{"length":0,"userPortraitList":[]}"""
|
||||
|
||||
val uid = (request["userId"] as Number).toLong()
|
||||
val list = ArrayList<Mai2UserPortrait>()
|
||||
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":[]}"""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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, Any>): 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<String, Any>): 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
|
||||
|
||||
@ -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<String, ?> request) throws JsonProcessingException {
|
||||
long userId = ((Number) request.get("userId")).longValue();
|
||||
|
||||
Mai2UserDetail userData;
|
||||
|
||||
Optional<Mai2UserDetail> 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<Mai2UserCard> 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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
@ -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<String, Any>): 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
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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<Mai2UserRate> = emptyList()
|
||||
var newRatingList: List<Mai2UserRate> = emptyList()
|
||||
var nextRatingList: List<Mai2UserRate> = emptyList()
|
||||
var nextNewRatingList: List<Mai2UserRate> = emptyList()
|
||||
var udemae: Mai2UserUdemae = Mai2UserUdemae()
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package icu.samnyan.aqua.sega.maimai2.model
|
||||
|
||||
class UserRivalMusic(
|
||||
var musicId: Int,
|
||||
var userRivalMusicDetailList: MutableList<UserRivalMusicDetail> = mutableListOf()
|
||||
)
|
||||
|
||||
class UserRivalMusicDetail(
|
||||
var level: Int,
|
||||
var achievement: Int,
|
||||
var deluxscoreMax: Int
|
||||
)
|
||||
@ -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<Mai2UserDetail> = emptyList()
|
||||
var userOption: List<Mai2UserOption>? = null
|
||||
var userExtend: List<Mai2UserExtend>? = null
|
||||
var userCharacterList: List<Mai2UserCharacter>? = null
|
||||
var userGhost: List<Mai2UserGhost>? = null
|
||||
var userMapList: List<Mai2UserMap>? = null
|
||||
var userLoginBonusList: List<Mai2UserLoginBonus>? = null
|
||||
var userRatingList: List<UserRating>? = null
|
||||
var userItemList: List<Mai2UserItem>? = null
|
||||
var userMusicDetailList: List<Mai2UserMusicDetail>? = null
|
||||
var userCourseList: List<Mai2UserCourse>? = null
|
||||
var userFriendSeasonRankingList: List<Mai2UserFriendSeasonRanking>? = null
|
||||
var userChargeList: List<Mai2UserCharge>? = null
|
||||
var userFavoriteList: List<Mai2UserFavorite>? = null
|
||||
var userActivityList: List<Mai2UserActivity>? = null
|
||||
var userGamePlaylogList: List<Map<String, Any>>? = null
|
||||
var userFavoritemusicList: List<Mai2UserFavoriteItem>? = null
|
||||
var userKaleidxScopeList: List<Mai2UserKaleidx>? = null
|
||||
var userIntimateList: List<Mai2UserIntimate>? = 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<Mai2UserAct> = emptyList()
|
||||
var musicList: List<Mai2UserAct> = emptyList()
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<String, Object> userPrintReserve;
|
||||
private Mai2UserPrintDetail userPrintDetail;
|
||||
}
|
||||
@ -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<Mai2UserDetail> userData;
|
||||
private List<Mai2UserExtend> userExtend;
|
||||
private List<Mai2UserOption> userOption;
|
||||
private List<Mai2UserCharacter> userCharacterList;
|
||||
private List<Mai2UserGhost> userGhost;
|
||||
private List<Mai2UserMap> userMapList;
|
||||
private List<Mai2UserLoginBonus> userLoginBonusList;
|
||||
private List<UserRating> userRatingList;
|
||||
private List<Mai2UserItem> userItemList;
|
||||
private List<Mai2UserMusicDetail> userMusicDetailList;
|
||||
private List<Mai2UserCourse> userCourseList;
|
||||
private List<Mai2UserFriendSeasonRanking> userFriendSeasonRankingList;
|
||||
private List<Mai2UserCharge> userChargeList;
|
||||
private List<Mai2UserFavorite> userFavoriteList;
|
||||
private List<Mai2UserActivity> userActivityList;
|
||||
private List<Map<String, Object>> userGamePlaylogList;
|
||||
private List<UserFavoriteItem> userFavoritemusicList;
|
||||
private List<Mai2UserKaleidx> userKaleidxScopeList;
|
||||
private List<Mai2UserIntimate> 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<Mai2UserAct> playList;
|
||||
private List<Mai2UserAct> musicList;
|
||||
}
|
||||
@ -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<Mai2UserRate> ratingList;
|
||||
private List<Mai2UserRate> newRatingList;
|
||||
private List<Mai2UserRate> nextRatingList;
|
||||
private List<Mai2UserRate> nextNewRatingList;
|
||||
private Mai2UserUdemae udemae;
|
||||
}
|
||||
@ -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<UserRivalMusicDetail> userRivalMusicDetailList;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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) {
|
||||
// <IP Address, RecruitInfo>
|
||||
val recruits = mutableMapOf<UInt, RecruitRecord>()
|
||||
// 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<RecruitRecord>(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<RecruitRecord>(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<String>) {
|
||||
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<RecruitRecord>(json)
|
||||
println(json)
|
||||
println(KJson.encodeToString(data))
|
||||
println(data)
|
||||
}
|
||||
@ -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(),
|
||||
// <Stream ID, Destination client stub IP>
|
||||
val tcpStreams: MutableMap<UInt, UInt> = mutableMapOf(),
|
||||
val pendingStreams: MutableSet<UInt> = 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<UInt, ActiveClient>()
|
||||
|
||||
/**
|
||||
* 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()
|
||||
@ -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<Bool>,
|
||||
var UserIDs: List<Long>,
|
||||
val UserNames: List<String>,
|
||||
val IconIDs: List<Int>,
|
||||
val FumenDifs: List<Int>,
|
||||
val Rateing: List<Int>,
|
||||
val ClassValue: List<Int>,
|
||||
val MaxClassValue: List<Int>,
|
||||
val UserType: List<Int>
|
||||
)
|
||||
|
||||
@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
|
||||
)
|
||||
@ -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;
|
||||
|
||||
@ -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<String, String> {
|
||||
// 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)
|
||||
}
|
||||
@ -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<Card> {
|
||||
|
||||
public AccessCodeSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public AccessCodeSerializer(Class<Card> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Card value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
gen.writeString(value.getLuid());
|
||||
}
|
||||
}
|
||||
@ -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 <T> convert(map: Any, to: Class<T>) = mapper.convertValue(map, to)
|
||||
fun <T> convert(map: Any, to: TypeReference<T>): T = mapper.convertValue(map, to)
|
||||
fun <T> read(json: String, to: Class<T>) = mapper.readValue(json, to)
|
||||
fun <T> read(json: String, to: TypeReference<T>) = mapper.readValue(json, to)
|
||||
|
||||
inline fun <reified T> convert(map: Any): T = convert(map, object : TypeReference<T>() {})
|
||||
inline fun <reified T> read(json: String): T = read(json, object : TypeReference<T>() {})
|
||||
}
|
||||
|
||||
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>(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)
|
||||
|
||||
fun <T> read(jsonStr: String?, toClass: Class<T>?) =
|
||||
BASIC_MAPPER.readValue(jsonStr, toClass)
|
||||
|
||||
fun <T> read(jsonStr: String?, toValueTypeRef: TypeReference<T>?) =
|
||||
BASIC_MAPPER.readValue(jsonStr, toValueTypeRef)
|
||||
|
||||
fun <T> convert(map: Any?, toClass: Class<T>?) =
|
||||
BASIC_MAPPER.convertValue(map, toClass)
|
||||
|
||||
fun <T> convert(map: Any?, toValueTypeRef: TypeReference<T>?) =
|
||||
BASIC_MAPPER.convertValue(map, toValueTypeRef)
|
||||
|
||||
fun toMap(obj: Any?): LinkedHashMap<String, Any?> =
|
||||
BASIC_MAPPER.convertValue(obj, object : TypeReference<LinkedHashMap<String, Any?>>() {})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@Component
|
||||
class StringMapper: IMapper(STRING_MAPPER)
|
||||
|
||||
|
||||
// Testing code
|
||||
private class A(
|
||||
var cat: String = ""
|
||||
)
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
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<List<A>>() {})
|
||||
println(b[0].cat)
|
||||
println(b.size)
|
||||
val c = BasicMapper().convert<List<A>>(lst.jsonArray())
|
||||
println(c[0].cat)
|
||||
}
|
||||
|
||||
@ -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<Boolean> {
|
||||
|
||||
@Override
|
||||
public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
return !"0".equals(p.getText());
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<Boolean> {
|
||||
|
||||
public BooleanNumberSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public BooleanNumberSerializer(Class<Boolean> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Boolean value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
gen.writeNumber(value ? 1 : 0);
|
||||
}
|
||||
}
|
||||
@ -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<Int>() {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<ByteBuf> {
|
||||
|
||||
public ByteBufSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public ByteBufSerializer(Class<ByteBuf> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(ByteBuf value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
|
||||
gen.writeString(ByteBufUtil.hexDump(value));
|
||||
|
||||
}
|
||||
}
|
||||
@ -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<Card>? = null) : StdSerializer<Card>(t) {
|
||||
override fun serialize(value: Card, gen: JsonGenerator, provider: SerializerProvider) {
|
||||
gen.writeString(value.luid)
|
||||
}
|
||||
}
|
||||
|
||||
class BooleanToIntegerDeserializer : JsonDeserializer<Int>() {
|
||||
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<Boolean>? = null) : StdSerializer<Boolean>(t) {
|
||||
override fun serialize(value: Boolean, gen: JsonGenerator, provider: SerializerProvider) {
|
||||
gen.writeNumber(if (value) 1 else 0)
|
||||
}
|
||||
}
|
||||
|
||||
class BooleanNumberDeserializer : JsonDeserializer<Boolean>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext) = "0" != p.text
|
||||
}
|
||||
|
||||
class ZonedDateTimeDeserializer : JsonDeserializer<ZonedDateTime>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext) = ZonedDateTime.parse(p.text)
|
||||
}
|
||||
|
||||
@ -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 <T> convert(map: Any?, toClass: Class<T>?) = STRING_MAPPER.convertValue(map, toClass)
|
||||
final inline fun <reified T> convert(map: Any?) = convert(map, T::class.java)
|
||||
fun toMap(obj: Any?) = STRING_MAPPER.convertValue(obj, object : TypeReference<LinkedHashMap<String, Any>>() {})
|
||||
|
||||
companion object {
|
||||
val BOOLEAN_SERIALIZER = object : StdSerializer<Boolean>(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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<UserData> {
|
||||
|
||||
public UserIdSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public UserIdSerializer(Class<UserData> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(UserData value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
gen.writeNumber(value.getCard().getExtId());
|
||||
}
|
||||
}
|
||||
@ -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<ZonedDateTime> {
|
||||
|
||||
@Override
|
||||
public ZonedDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
return ZonedDateTime.parse(p.getText());
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user