3 Commits

Author SHA1 Message Date
986fbdda6b merge upstream 2025-05-29 13:36:12 +08:00
0453ebd1bb 更新 .github/workflows/build.yml 2025-05-27 09:01:31 +08:00
ee9f06ca86 添加 .github/workflows/build 2025-05-27 09:00:27 +08:00
544 changed files with 17577 additions and 9504 deletions

49
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Gradle Build
on:
pull_request:
branches: [ v1-dev ]
workflow_dispatch:
jobs:
build:
runs-on: macos-arm64
env:
GIT_SSL_NO_VERIFY: true
steps:
- name: Force Git to use HTTP/1.1 (Experimental)
run: |
git config --global http.version HTTP/1.1
git config --global http.sslVerify false
git config --global http.sslVerify false
git config --global https.sslVerify false
git config --global http.proxy 'http://127.0.0.1:7890'
git config --global https.proxy 'http://127.0.0.1:7890'
export NODE_TLS_REJECT_UNAUTHORIZED='0'
- name: Disable SSL verification (Temporary Fix)
run: git config --global http.sslVerify false
- name: Checkout repository
uses: https://gitee.com/github-actions/checkout@v4
- name: Set up JDK
uses: https://gitea.com/actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: Build with Gradle
run: |
mkdir data
bash ./src/main/resources/meta/update.sh
chmod +x gradlew
./gradlew build
- name: Upload artifact
uses: https://gitee.com/actions-mirror/upload-artifact@v3
with:
name: AquaDX-1.0.0.jar
path: build/libs

25
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,25 @@
image: gradle:alpine
before_script:
- GRADLE_USER_HOME="$(pwd)/.gradle"
- export GRADLE_USER_HOME
build:
stage: build
script: gradle --build-cache assemble
cache:
key: "$CI_COMMIT_REF_NAME"
policy: push
paths:
- build
- .gradle
test:
stage: test
script: gradle check
cache:
key: "$CI_COMMIT_REF_NAME"
policy: pull
paths:
- build
- .gradle

View File

@@ -79,8 +79,6 @@
<Router {url}> <Router {url}>
<Route path="/" component={Welcome} /> <Route path="/" component={Welcome} />
<Route path="/verify" component={Welcome} /> <!-- For email verification only, backwards compatibility with AquaNet2 in the future -->
<Route path="/reset-password" component={Welcome} />
<Route path="/home" component={Home} /> <Route path="/home" component={Home} />
<Route path="/ranking" component={Ranking} /> <Route path="/ranking" component={Ranking} />
<Route path="/ranking/:game" component={Ranking} /> <Route path="/ranking/:game" component={Ranking} />

View File

@@ -1,60 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let page: number
export let totalPages: number
const dispatch = createEventDispatcher()
let editing = false
let inputPage: number
function updatePage(newPage: number) {
if (newPage > 0 && newPage <= totalPages) dispatch('updatePage', newPage)
}
function startEditing() {
inputPage = page
editing = true
}
function finishEditing() {
editing = false
if (inputPage !== page) updatePage(inputPage)
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') finishEditing()
else if (event.key === 'Escape') editing = false
}
</script>
<div class="pagination">
<button on:click={() => updatePage(page - 1)} disabled={page <= 1}>Previous</button>
{#if editing}
<input bind:value={inputPage} on:blur={finishEditing} on:keydown={handleKeydown} min="1" max={totalPages} autofocus/>
{:else}
<span on:click={startEditing} role="button" tabindex="0" on:keydown={(e) => e.key === 'Enter' && startEditing()}>
Page {page} of {totalPages}
</span>
{/if}
<button on:click={() => updatePage(page + 1)} disabled={page >= totalPages}>Next</button>
</div>
<style lang="sass">
.pagination
display: flex
justify-content: center
align-items: center
margin: 1rem 0
gap: 1rem
input
width: 100px
text-align: center
span[role="button"]
cursor: pointer
</style>

View File

@@ -70,7 +70,7 @@
if (ubKey == 'namePlateId') ubKey = 'nameplateId' if (ubKey == 'namePlateId') ubKey = 'nameplateId'
if (ubKey == 'systemVoiceId') ubKey = 'voiceId' if (ubKey == 'systemVoiceId') ubKey = 'voiceId'
return [{ iKey, ubKey: ubKey as keyof UserBox, return [{ iKey, ubKey: ubKey as keyof UserBox,
items: profile.items.filter(x => x.itemKind === iKind || (iKey == "trophy" && x.itemKind == 3)) items: profile.items.filter(x => x.itemKind === iKind)
}] }]
} }
@@ -106,133 +106,6 @@
.finally(() => submitting = "") .finally(() => submitting = "")
} }
async function exportBatchManual() {
submitting = "batchExport"
const DIFFICULTY_MAP: Record<number, string> = {
0: "BASIC",
1: "ADVANCED",
2: "EXPERT",
3: "MASTER",
4: "ULTIMA"
} as const // WORLD'S END scores not supported by Tachi
const DAN_MAP: Record<number, string> = {
1: "DAN_I",
2: "DAN_II",
3: "DAN_III",
4: "DAN_IV",
5: "DAN_V",
6: "DAN_INFINITE"
} as const
const SKILL_IDS: Record<number, string> = {
100009: 'CATASTROPHY',
102009: 'CATASTROPHY',
103007: 'CATASTROPHY',
100008: 'ABSOLUTE',
101008: 'ABSOLUTE',
102008: 'ABSOLUTE',
103006: 'ABSOLUTE',
100007: 'BRAVE',
101007: 'BRAVE',
102007: 'BRAVE',
103005: 'BRAVE',
100005: 'HARD',
100006: 'HARD',
101004: 'HARD',
101005: 'HARD',
101006: 'HARD',
102004: 'HARD',
102005: 'HARD',
102006: 'HARD',
103002: 'HARD',
103003: 'HARD',
103004: 'HARD'
} as const
// Shamelessly stolen from https://github.com/beer-psi/saekawa/commit/b3bee13e126df2f4e2a449bdf971debb8c95ba40, needs to be updated every major version :(
let data: any
let output: any = {
"meta": {
"game": "chunithm",
"playtype": "Single",
"service": "AquaDX-Manual"
},
"scores": [],
"classes": {}
}
try {
data = await GAME.export('chu3')
}
catch (e) {
error = e.message
submitting = ""
return
}
if (data && "userPlaylogList" in data) {
for (let score of data.userPlaylogList) {
let clearLamp = null
let noteLamp = null
if (score.level in DIFFICULTY_MAP) {
if (score.isClear) {
clearLamp = score.skillId in SKILL_IDS ? SKILL_IDS[score.skillId] : "CLEAR"
}
else {
clearLamp = "FAILED"
}
if (score.score === 1010000) {
noteLamp = "ALL JUSTICE CRITICAL"
}
else if (score.isAllJustice) {
noteLamp = "ALL JUSTICE"
}
else if (score.isFullCombo) {
noteLamp = "FULL COMBO"
}
else {
noteLamp = "NONE"
}
output.scores.push({
"score": score.score,
"clearLamp": clearLamp,
"noteLamp": noteLamp,
"judgements": {
"jcrit": score.judgeHeaven + score.judgeCritical,
"justice": score.judgeJustice,
"attack": score.judgeAttack,
"miss": score.judgeGuilty
},
"matchType": "inGameID",
"identifier": score.musicId.toString(),
"difficulty": DIFFICULTY_MAP[score.level],
"timeAchieved": score.sortNumber * 1000,
"optional": {
"maxCombo": score.maxCombo
}
})
}
}
}
if (data.userData.classEmblemMedal in DAN_MAP) {
output.classes["dan"] = DAN_MAP[data.userData.classEmblemMedal]
}
if (data.userData.classEmblemBase in DAN_MAP) {
output.classes["emblem"] = DAN_MAP[data.userData.classEmblemBase]
}
download(JSON.stringify(output), `AquaDX_chu3_BatchManualExport_${userbox.userName}.json`)
submitting = ""
}
function download(data: string, filename: string) { function download(data: string, filename: string) {
const blob = new Blob([data]); const blob = new Blob([data]);
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -428,10 +301,6 @@
<Icon icon="bxs:file-export"/> <Icon icon="bxs:file-export"/>
{t('settings.export')} {t('settings.export')}
</button> </button>
<button class="exportBatchManualButton" on:click={exportBatchManual}>
<Icon icon="bxs:file-export"/>
{t('settings.batchManualExport')}
</button>
</div> </div>
{/if} {/if}

View File

@@ -1,17 +1,18 @@
<script> <script>
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { FADE_IN, FADE_OUT } from "../../libs/config"; import { FADE_IN, FADE_OUT } from "../../libs/config";
import GameSettingFields from "./GameSettingFields.svelte";
import { t, ts } from "../../libs/i18n"; import { t, ts } from "../../libs/i18n";
import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte"; import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte";
import RegionSelector from "./RegionSelector.svelte";
const rounding = useLocalStorage("rounding", true); const rounding = useLocalStorage("rounding", true);
</script> </script>
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields"> <div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
<blockquote> <blockquote>
{ts("settings.siteNotice")} {ts("settings.gameNotice")}
</blockquote> </blockquote>
<GameSettingFields game="general"/>
<div class="field"> <div class="field">
<div class="bool"> <div class="bool">
<input id="rounding" type="checkbox" bind:checked={rounding.value}/> <input id="rounding" type="checkbox" bind:checked={rounding.value}/>
@@ -21,11 +22,6 @@
</label> </label>
</div> </div>
</div> </div>
<div class="divider"></div>
<blockquote>
{ts("settings.regionNotice")}
</blockquote>
<RegionSelector/>
</div> </div>
<style lang="sass"> <style lang="sass">
@@ -48,10 +44,19 @@
.desc .desc
opacity: 0.6 opacity: 0.6
.divider .field
width: 100% display: flex
height: 0.5px flex-direction: column
background: white
opacity: 0.2 label
margin: 0.4rem 0 max-width: max-content
> div:not(.bool)
display: flex
align-items: center
gap: 1rem
margin-top: 0.5rem
> input
flex: 1
</style> </style>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { slide, fade } from "svelte/transition"; import { slide, fade } from "svelte/transition";
import { FADE_IN, FADE_OUT, DATA_HOST } from "../../libs/config"; import { FADE_IN, FADE_OUT } from "../../libs/config";
import { t } from "../../libs/i18n.js"; import { t } from "../../libs/i18n.js";
import Icon from "@iconify/svelte"; import Icon from "@iconify/svelte";
import StatusOverlays from "../StatusOverlays.svelte"; import StatusOverlays from "../StatusOverlays.svelte";
@@ -35,169 +35,6 @@
break break
} }
} }
async function exportBatchManual() {
submitting = "batchExport"
const DIFFICULTY_MAP: Record<number, string> = {
0: "Basic",
1: "Advanced",
2: "Expert",
3: "Master",
4: "Re:Master"
}
const DAN_MAP: Record<number, string> = {
1: "DAN_1",
2: "DAN_2",
3: "DAN_3",
4: "DAN_4",
5: "DAN_5",
6: "DAN_6",
7: "DAN_7",
8: "DAN_8",
9: "DAN_9",
10: "DAN_10",
11: "SHINDAN_1",
12: "SHINDAN_2",
13: "SHINDAN_3",
14: "SHINDAN_4",
15: "SHINDAN_5",
16: "SHINDAN_6",
17: "SHINDAN_7",
18: "SHINDAN_8",
19: "SHINDAN_9",
20: "SHINDAN_10",
21: "SHINKAIDEN",
22: "URAKAIDEN"
}
const CLASS_MAP: Record<number, string> = {
0: "B5",
1: "B4",
2: "B3",
3: "B2",
4: "B1",
5: "A5",
6: "A4",
7: "A3",
8: "A2",
9: "A1",
10: "S5",
11: "S4",
12: "S3",
13: "S2",
14: "S1",
15: "SS5",
16: "SS4",
17: "SS3",
18: "SS2",
19: "SS1",
20: "SSS5",
21: "SSS4",
22: "SSS3",
23: "SSS2",
24: "SSS1",
25: "LEGEND"
}
let data: any
let musicData: any
let output: any = {
"meta": {
"game": "maimaidx",
"playtype": "Single",
"service": "AquaDX-Manual"
},
"scores": [],
"classes": {}
}
try {
musicData = await fetch(`${DATA_HOST}/d/mai2/00/all-music.json`).then(res => res.json())
} catch (e) {
error = e.message;
submitting = ""
return;
}
try {
data = await GAME.export('mai2');
} catch (e) {
error = e.message;
submitting = ""
return;
}
if (data && "userPlaylogList" in data) {
for (let score of data.userPlaylogList) {
if(score.musicId > 100000){
continue; // UTAGE charts are not supported
}
const musicItem = musicData[score.musicId as string];
if (!musicItem) continue;
let difficulty = null;
if (!(score.level in DIFFICULTY_MAP))
continue;
const isDX = score.musicId >= 10000;
difficulty = isDX ? `DX ${DIFFICULTY_MAP[score.level]}` : DIFFICULTY_MAP[score.level];
const percent = score.achievement/10000;
const pcrit = score.tapCriticalPerfect + score.holdCriticalPerfect + score.slideCriticalPerfect + score.touchCriticalPerfect + score.breakCriticalPerfect;
const perfect = score.tapPerfect + score.holdPerfect + score.slidePerfect + score.touchPerfect + score.breakPerfect;
const great = score.tapGreat + score.holdGreat + score.slideGreat + score.touchGreat + score.breakGreat;
const good = score.tapGood + score.holdGood + score.slideGood + score.touchGood + score.breakGood;
const miss = score.tapMiss + score.holdMiss + score.slideMiss + score.touchMiss + score.breakMiss;
const judgements = {
"pcrit": pcrit,
"perfect": perfect,
"great": great,
"good": good,
"miss": miss
}
let lamp = null;
if (score.isAllPerfect) {
lamp = "ALL PERFECT";
if (score.percent == 101.0) {
lamp = "ALL PERFECT+";
}
} else if (score.isFullCombo) {
lamp = "FULL COMBO";
if (good == 0 && great == 0) {
lamp = "FULL COMBO+";
}
} else if (score.isClear) {
lamp = "CLEAR";
} else {
lamp = "FAILED";
}
const optional = {
"fast": score.fastCount,
"slow": score.lateCount,
"maxCombo": score.maxCombo
}
output.scores.push({
"percent": percent,
"lamp": lamp,
"matchType": "inGameID",
"identifier": score.musicId.toString(),
"difficulty": difficulty,
"timeAchieved": new Date(score.userPlayDate).getTime(),
"judgements": judgements,
"optional": optional
})
}
}
if(data.userData.courseRank in DAN_MAP){
output.classes["dan"] = DAN_MAP[data.userData.courseRank]
}
if(data.userData.classRank in CLASS_MAP){
output.classes["matchingClass"] = CLASS_MAP[data.userData.classRank]
}
download(JSON.stringify(output), `AquaDX_maimai2_BatchManualExport_${values[0]}.json`)
submitting = ""
}
function exportData() { function exportData() {
submitting = "export" submitting = "export"
@@ -233,10 +70,6 @@
<Icon icon="bxs:file-export"/> <Icon icon="bxs:file-export"/>
{t('settings.export')} {t('settings.export')}
</button> </button>
<button class="exportBatchManualButton" on:click={exportBatchManual}>
<Icon icon="bxs:file-export"/>
{t('settings.batchManualExport')}
</button>
</div> </div>
<StatusOverlays {error} loading={!values[0] || !!submitting}/> <StatusOverlays {error} loading={!values[0] || !!submitting}/>

View File

@@ -1,59 +0,0 @@
<script lang="ts">
import { USER} from "../../libs/sdk";
import { ts } from "../../libs/i18n";
import StatusOverlays from "../StatusOverlays.svelte";
let regionId = $state(0);
let submitting = ""
let error: string;
const prefectures = ["None","Aichi","Aomori","Akita","Ishikawa","Ibaraki","Iwate","Ehime","Oita","Osaka","Okayama","Okinawa","Kagawa","Kagoshima","Kanagawa","Gifu","Kyoto","Kumamoto","Gunma","Kochi","Saitama","Saga","Shiga","Shizuoka","Shimane","Chiba","Tokyo","Tokushima","Tochigi","Tottori","Toyama","Nagasaki","Nagano","Nara","Niigata","Hyogo","Hiroshima","Fukui","Fukuoka","Fukushima","Hokkaido","Mie","Miyagi","Miyazaki","Yamagata","Yamaguchi","Yamanashi","Wakayama"]
USER.me().then(user => {
const parsedRegion = parseInt(user.region);
if (!isNaN(parsedRegion) && parsedRegion > 0) {
regionId = parsedRegion - 1;
} else {
regionId = 0;
}
})
async function saveNewRegion() {
if (submitting) return false
submitting = "region"
await USER.changeRegion(regionId+1).catch(e => error = e.message).finally(() => submitting = "")
return true
}
</script>
<div class="fields">
<label for="rounding">
<span class="name">{ts(`settings.regionSelector.title`)}</span>
<span class="desc">{ts(`settings.regionSelector.desc`)}</span>
</label>
<select bind:value={regionId} on:change={saveNewRegion}>
<option value={0} disabled selected>{ts("settings.regionSelector.select")}</option>
{#each prefectures.slice(1) as prefecture, index}
<option value={index}>{prefecture}</option>
{/each}
</select>
</div>
<StatusOverlays {error} loading={!!submitting}/>
<style lang="sass">
@use "../../vars"
.fields
display: flex
flex-direction: column
gap: 12px
label
display: flex
flex-direction: column
.desc
opacity: 0.6
</style>

View File

@@ -19,7 +19,6 @@ export interface AquaNetUser {
email: string email: string
displayName: string displayName: string
country: string country: string
region:string
lastLogin: number lastLogin: number
regTime: number regTime: number
profileLocation: string profileLocation: string
@@ -107,8 +106,7 @@ export interface GenericGameSummary {
lastVersion: string lastVersion: string
ratingComposition: { [key: string]: any } ratingComposition: { [key: string]: any }
recent: GenericGamePlaylog[] recent: GenericGamePlaylog[]
rival?: boolean, rival?: boolean
favorites?: number[]
} }
export interface MusicMeta { export interface MusicMeta {

View File

@@ -28,38 +28,27 @@ export const EN_REF_USER = {
'UserHome.RemoveRival': "Remove from Rival", 'UserHome.RemoveRival': "Remove from Rival",
'UserHome.InvalidGame': "Game ${game} is not supported on the web UI yet. We only support maimai, chunithm, wacca, and ongeki for now.", 'UserHome.InvalidGame': "Game ${game} is not supported on the web UI yet. We only support maimai, chunithm, wacca, and ongeki for now.",
'UserHome.ShowMoreRecent': 'Show more', 'UserHome.ShowMoreRecent': 'Show more',
'UserHome.FavoriteSongs': 'Favorite Songs'
} }
export const EN_REF_Welcome = { export const EN_REF_Welcome = {
'back': 'Back', 'back': 'Back',
'email': 'Email', 'email': 'Email',
'password': 'Password', 'password': 'Password',
'new-password': 'New password',
'username': 'Username', 'username': 'Username',
'welcome.btn-login': 'Log in', 'welcome.btn-login': 'Log in',
'welcome.btn-signup': 'Sign up', 'welcome.btn-signup': 'Sign up',
'welcome.btn-reset-password': 'Forgot password?',
'welcome.btn-submit-reset-password': 'Send reset link',
'welcome.btn-submit-new-password': 'Change password',
'welcome.email-missing': 'Email is required',
'welcome.password-missing': 'Password is required',
'welcome.username-missing': 'Username/email is required',
'welcome.email-password-missing': 'Email and password are required', 'welcome.email-password-missing': 'Email and password are required',
'welcome.username-missing': 'Username/email is required',
'welcome.waiting-turnstile': 'Waiting for Turnstile to verify your network environment...', 'welcome.waiting-turnstile': 'Waiting for Turnstile to verify your network environment...',
'welcome.turnstile-error': 'Error verifying your network environment. Please turn off your VPN and try again.', 'welcome.turnstile-error': 'Error verifying your network environment. Please turn off your VPN and try again.',
'welcome.turnstile-timeout': 'Network verification timed out. Please try again.', 'welcome.turnstile-timeout': 'Network verification timed out. Please try again.',
'welcome.verification-sent': 'A verification email has been sent to ${email}. Please check your inbox!', 'welcome.verification-sent': 'A verification email has been sent to ${email}. Please check your inbox!',
'welcome.reset-password-sent': 'A password reset email has been sent to ${email}. Please check your inbox!', 'welcome.verify-state-0': 'You haven\'t verified your email. A verification email had been sent to your inbox less than a minute ago. Please check your inbox!',
'welcome.verify-state-0': 'You haven\'t verified your email. A verification email has been sent to your inbox just now. Please check your inbox!', 'welcome.verify-state-1': 'You haven\'t verified your email. We\'ve already sent 3 emails over the last 24 hours so we\'ll not send another one. Please check your inbox!',
'welcome.verify-state-1': 'You haven\'t verified your email. You have requested too many emails, please try again later.',
'welcome.verify-state-2': 'You haven\'t verified your email. We just sent you another verification email. Please check your inbox!', 'welcome.verify-state-2': 'You haven\'t verified your email. We just sent you another verification email. Please check your inbox!',
'welcome.reset-state-0': 'A reset email has been sent to your inbox just now. Please check your inbox!',
'welcome.reset-state-1': 'Too many emails have been sent. Another will not be sent.',
'welcome.verifying': 'Verifying your email... please wait.', 'welcome.verifying': 'Verifying your email... please wait.',
'welcome.verified': 'Your email has been verified! You can now log in now.', 'welcome.verified': 'Your email has been verified! You can now log in now.',
'welcome.verification-failed': 'Verification failed: ${message}. Please try again.', 'welcome.verification-failed': 'Verification failed: ${message}. Please try again.',
'welcome.password-reset-done': 'Your password has been updated! Please log back in.',
} }
export const EN_REF_LEADERBOARD = { export const EN_REF_LEADERBOARD = {
@@ -144,35 +133,23 @@ export const EN_REF_HOME = {
export const EN_REF_SETTINGS = { export const EN_REF_SETTINGS = {
'settings.title': 'Settings', 'settings.title': 'Settings',
'settings.tabs.profile': 'Profile', 'settings.tabs.profile': 'Profile',
'settings.tabs.global': 'Global', 'settings.tabs.game': 'Game',
'settings.tabs.chu3': 'Chuni', 'settings.tabs.chu3': 'Chuni',
'settings.tabs.mai2': 'Mai', 'settings.tabs.mai2': 'Mai',
'settings.tabs.ongeki': 'Ongeki', 'settings.tabs.ongeki': 'Ongeki',
'settings.tabs.wacca': 'Wacca', 'settings.tabs.wacca': 'Wacca',
'settings.fields.mai2UnlockMusic.name': 'Unlock All Music', 'settings.fields.unlockMusic.name': 'Unlock All Music',
'settings.fields.mai2UnlockMusic.desc': 'Unlock all music and master difficulty.', 'settings.fields.unlockMusic.desc': 'Unlock all music and master difficulty in game.',
'settings.fields.mai2UnlockChara.name': 'Unlock All Characters', 'settings.fields.unlockChara.name': 'Unlock All Characters',
'settings.fields.mai2UnlockChara.desc': 'Unlock all characters (new characters start at level 1).', 'settings.fields.unlockChara.desc': 'Unlock all characters, voices, and partners in game.',
'settings.fields.mai2UnlockCharaMaxLevel.name': 'Max Character Level', 'settings.fields.unlockCollectables.name': 'Unlock All Collectables',
'settings.fields.mai2UnlockCharaMaxLevel.desc': 'Set all characters to max level.', 'settings.fields.unlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame) in game.',
'settings.fields.mai2UnlockPartners.name': 'Unlock All Partners', 'settings.fields.unlockTickets.name': 'Unlock All Tickets',
'settings.fields.mai2UnlockPartners.desc': 'Unlock all partners.', 'settings.fields.unlockTickets.desc': 'Infinite map/ex tickets (Note: maimai still limits which tickets can be used).',
'settings.fields.mai2UnlockCollectables.name': 'Unlock All Collectables', 'settings.fields.waccaInfiniteWp.name': 'Wacca: Infinite WP',
'settings.fields.mai2UnlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame).', 'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999',
'settings.fields.mai2UnlockTickets.name': 'Unlock All Tickets', 'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP',
'settings.fields.mai2UnlockTickets.desc': 'Infinite tickets (Note: client still limits which tickets can be used).', 'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01',
'settings.fields.waccaUnlockMusic.name': 'Unlock All Music',
'settings.fields.waccaUnlockMusic.desc': 'Unlock all music.',
'settings.fields.waccaUnlockPlates.name': 'Unlock All Plates',
'settings.fields.waccaUnlockPlates.desc': 'Unlock all plates.',
'settings.fields.waccaUnlockCollectables.name': 'Unlock All Collectables',
'settings.fields.waccaUnlockCollectables.desc': 'Unlock all collectables (icon, trophy).',
'settings.fields.waccaUnlockTickets.name': 'Infinite Tickets',
'settings.fields.waccaUnlockTickets.desc': 'Infinite tickets.',
'settings.fields.waccaInfiniteWp.name': 'Infinite WP',
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999.',
'settings.fields.waccaAlwaysVip.name': 'Always VIP',
'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01.',
'settings.fields.chusanTeamName.name': 'Team Name', 'settings.fields.chusanTeamName.name': 'Team Name',
'settings.fields.chusanTeamName.desc': 'Customize the text displayed on the top of your profile.', 'settings.fields.chusanTeamName.desc': 'Customize the text displayed on the top of your profile.',
'settings.fields.chusanInfinitePenguins.name': 'Infinite Penguins', 'settings.fields.chusanInfinitePenguins.name': 'Infinite Penguins',
@@ -206,13 +183,8 @@ export const EN_REF_SETTINGS = {
'settings.profile.logout': 'Log out', 'settings.profile.logout': 'Log out',
'settings.profile.unchanged': 'Unchanged', 'settings.profile.unchanged': 'Unchanged',
'settings.export': 'Export Player Data', 'settings.export': 'Export Player Data',
'settings.batchManualExport': "Export in Batch Manual (for Tachi)",
'settings.cabNotice': "Note: These settings will only affect your own cab/setup. If you're playing on someone else's setup, please contact them to change these settings.", 'settings.cabNotice': "Note: These settings will only affect your own cab/setup. If you're playing on someone else's setup, please contact them to change these settings.",
'settings.siteNotice': "These settings only apply to the website.", 'settings.gameNotice': "These only apply to Mai and Wacca."
'settings.regionNotice': "These settings are shared amongst Mai, Ongeki and Chuni.",
'settings.regionSelector.title': "Prefecture Selector",
'settings.regionSelector.desc': "Select the region where you want the game to identify you",
'settings.regionSelector.select': "Select Prefecture",
} }
export const EN_REF_USERBOX = { export const EN_REF_USERBOX = {

View File

@@ -40,38 +40,27 @@ const zhUser: typeof EN_REF_USER = {
'UserHome.RemoveRival': "移除劲敌", 'UserHome.RemoveRival': "移除劲敌",
'UserHome.InvalidGame': "游戏 ${game} 还不支持网页端查看。我们目前只支持舞萌、中二、华卡和音击。", 'UserHome.InvalidGame': "游戏 ${game} 还不支持网页端查看。我们目前只支持舞萌、中二、华卡和音击。",
'UserHome.ShowMoreRecent': "显示更多", 'UserHome.ShowMoreRecent': "显示更多",
'UserHome.FavoriteSongs': "收藏歌曲"
} }
const zhWelcome: typeof EN_REF_Welcome = { const zhWelcome: typeof EN_REF_Welcome = {
'back': '返回', 'back': '返回',
'email': '邮箱', 'email': '邮箱',
'password': '密码', 'password': '密码',
'new-password': '新密码',
'username': '用户名', 'username': '用户名',
'welcome.btn-login': '登录', 'welcome.btn-login': '登录',
'welcome.btn-signup': '注册', 'welcome.btn-signup': '注册',
'welcome.btn-reset-password': '忘记密码?',
'welcome.btn-submit-reset-password': '发送重置链接',
'welcome.btn-submit-new-password': '修改密码',
'welcome.email-missing': '邮箱必须填哦',
'welcome.password-missing': '密码必须填哦',
'welcome.username-missing': '用户名/邮箱必须填哦',
'welcome.email-password-missing': '邮箱和密码必须填哦', 'welcome.email-password-missing': '邮箱和密码必须填哦',
'welcome.username-missing': '用户名/邮箱必须填哦',
'welcome.waiting-turnstile': '正在验证网络环境…', 'welcome.waiting-turnstile': '正在验证网络环境…',
'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试', 'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试',
'welcome.turnstile-timeout': '验证网络环境超时了,请重试', 'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱', 'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱',
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱', 'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱', 'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱',
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱', 'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!',
'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了',
'welcome.verifying': '正在验证邮箱…请稍等', 'welcome.verifying': '正在验证邮箱…请稍等',
'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了', 'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了',
'welcome.verification-failed': '验证失败:${message}。请重试', 'welcome.verification-failed': '验证失败:${message}。请重试',
'welcome.password-reset-done': '您的密码已更新!请重新登录',
} }
const zhLeaderboard: typeof EN_REF_LEADERBOARD = { const zhLeaderboard: typeof EN_REF_LEADERBOARD = {
@@ -156,35 +145,23 @@ const zhHome: typeof EN_REF_HOME = {
const zhSettings: typeof EN_REF_SETTINGS = { const zhSettings: typeof EN_REF_SETTINGS = {
'settings.title': '用户设置', 'settings.title': '用户设置',
'settings.tabs.profile': '个人资料', 'settings.tabs.profile': '个人资料',
'settings.tabs.global': '全局', 'settings.tabs.game': '游戏设置',
'settings.tabs.chu3': '中二', 'settings.tabs.chu3': '中二',
'settings.tabs.mai2': '舞萌', 'settings.tabs.mai2': '舞萌',
'settings.tabs.ongeki': '音击', 'settings.tabs.ongeki': '音击',
'settings.tabs.wacca': '华卡', 'settings.tabs.wacca': '华卡',
'settings.fields.mai2UnlockMusic.name': '解锁谱面', 'settings.fields.unlockMusic.name': '解锁谱面',
'settings.fields.mai2UnlockMusic.desc': '解锁所有曲目和大师难度谱面。', 'settings.fields.unlockMusic.desc': '在游戏中解锁所有曲目和大师难度谱面。',
'settings.fields.mai2UnlockChara.name': '解锁角色', 'settings.fields.unlockChara.name': '解锁角色',
'settings.fields.mai2UnlockChara.desc': '解锁所有角色(新角色从 1 级开始)。', 'settings.fields.unlockChara.desc': '在游戏中解锁所有角色、语音和伙伴。',
'settings.fields.mai2UnlockCharaMaxLevel.name': '角色满级', 'settings.fields.unlockCollectables.name': '解锁收藏品',
'settings.fields.mai2UnlockCharaMaxLevel.desc': '将所有角色设置为满级。', 'settings.fields.unlockCollectables.desc': '在游戏中解锁所有收藏品(名牌、称号、图标、背景图)。',
'settings.fields.mai2UnlockPartners.name': '解锁搭档', 'settings.fields.unlockTickets.name': '解锁游戏券',
'settings.fields.mai2UnlockPartners.desc': '解锁所有搭档。', 'settings.fields.unlockTickets.desc': '无限跑图券/解锁券maimai 客户端仍限制一些券不能使用)。',
'settings.fields.mai2UnlockCollectables.name': '解锁收藏品', 'settings.fields.waccaInfiniteWp.name': '华卡:无限 WP',
'settings.fields.mai2UnlockCollectables.desc': '解锁所有收藏品(姓名框、称号、头像、背景)。', 'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999',
'settings.fields.mai2UnlockTickets.name': '解锁功能票', 'settings.fields.waccaAlwaysVip.name': '华卡:永久会员',
'settings.fields.mai2UnlockTickets.desc': '无限功能票(注:客户端仍限制一些功能票不能使用)。', 'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01',
'settings.fields.waccaUnlockMusic.name': '解锁谱面',
'settings.fields.waccaUnlockMusic.desc': '解锁所有曲目。',
'settings.fields.waccaUnlockPlates.name': '解锁铭牌',
'settings.fields.waccaUnlockPlates.desc': '解锁所有铭牌。',
'settings.fields.waccaUnlockCollectables.name': '解锁收藏品',
'settings.fields.waccaUnlockCollectables.desc': '解锁所有收藏品。',
'settings.fields.waccaUnlockTickets.name': '无限解锁券',
'settings.fields.waccaUnlockTickets.desc': '无限解锁券。',
'settings.fields.waccaInfiniteWp.name': '无限 WP',
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999。',
'settings.fields.waccaAlwaysVip.name': '永久会员',
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01。',
'settings.fields.chusanTeamName.name': '队伍名称', 'settings.fields.chusanTeamName.name': '队伍名称',
'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。', 'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。',
'settings.fields.chusanInfinitePenguins.name': '我是桐谷遥', 'settings.fields.chusanInfinitePenguins.name': '我是桐谷遥',
@@ -218,18 +195,8 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.profile.logout': '登出', 'settings.profile.logout': '登出',
'settings.profile.unchanged': '未更改', 'settings.profile.unchanged': '未更改',
'settings.export': '导出玩家数据', 'settings.export': '导出玩家数据',
'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi",
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置', 'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
// AI 'settings.gameNotice': "这些设置仅对舞萌和华卡生效。",
'settings.siteNotice': "这些设置仅适用于网站。",
// AI
'settings.regionNotice': "这些设置在舞萌、音击和中二节奏之间共享。",
// AI
'settings.regionSelector.title': "地区选择器",
// AI
'settings.regionSelector.desc': "选择游戏中显示的地区",
// AI
'settings.regionSelector.select': "选择地区",
} }
export const zhUserbox: typeof EN_REF_USERBOX = { export const zhUserbox: typeof EN_REF_USERBOX = {

View File

@@ -163,22 +163,12 @@ async function login(user: { email: string, password: string, turnstile: string
localStorage.setItem('token', data.token) localStorage.setItem('token', data.token)
} }
async function resetPassword(user: { email: string, turnstile: string }) {
return await post('/api/v2/user/reset-password', user)
}
async function changePassword(user: { token: string, password: string }) {
return await post('/api/v2/user/change-password', user)
}
const isLoggedIn = () => !!localStorage.getItem('token') const isLoggedIn = () => !!localStorage.getItem('token')
const ensureLoggedIn = () => !isLoggedIn() && (window.location.href = '/') const ensureLoggedIn = () => !isLoggedIn() && (window.location.href = '/')
export const USER = { export const USER = {
register, register,
login, login,
resetPassword,
changePassword,
confirmEmail: (token: string) => confirmEmail: (token: string) =>
post('/api/v2/user/confirm-email', { token }), post('/api/v2/user/confirm-email', { token }),
me: (): Promise<AquaNetUser> => { me: (): Promise<AquaNetUser> => {
@@ -196,8 +186,6 @@ export const USER = {
}, },
isLoggedIn, isLoggedIn,
ensureLoggedIn, ensureLoggedIn,
changeRegion: (regionId: number) =>
post('/api/v2/user/change-region', { regionId }),
} }
export const USERBOX = { export const USERBOX = {
@@ -266,14 +254,5 @@ export const TRANSFER = {
post('/api/v2/transfer/push', {}, { json: { client: d, data } }), post('/api/v2/transfer/push', {}, { json: { client: d, data } }),
} }
export const FEDY = {
status: (): Promise<{ linkedAt: number }> =>
post('/api/v2/fedy/status'),
link: (nonce: string): Promise<{ linkedAt: number }> =>
post('/api/v2/fedy/link', { nonce }),
unlink: () =>
post('/api/v2/fedy/unlink'),
}
// @ts-ignore // @ts-ignore
window.sdk = { USER, USERBOX, CARD, GAME, DATA, SETTING, TRANSFER, FEDY } window.sdk = { USER, USERBOX, CARD, GAME, DATA, SETTING, TRANSFER }

View File

@@ -186,6 +186,8 @@ export function initializeDb() : Promise<void> {
export async function userboxFileProcess(folder: FileSystemEntry, progressUpdate: (progress: number, progressString: string) => void): Promise<string | null> { export async function userboxFileProcess(folder: FileSystemEntry, progressUpdate: (progress: number, progressString: string) => void): Promise<string | null> {
if (!isDirectory(folder)) if (!isDirectory(folder))
return t("userbox.new.error.invalidFolder") return t("userbox.new.error.invalidFolder")
if (!(await validateDirectories(folder, "bin/option") || await validateDirectories(folder, "option")) && !(await validateDirectories(folder, "data/A000")))
return t("userbox.new.error.invalidFolder");
initializeDb(); initializeDb();
const optionFolder = await scanRecursive(folder, "A001"); const optionFolder = await scanRecursive(folder, "A001");

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import { title } from "../libs/ui"; import { title } from "../libs/ui";
import { GAME } from "../libs/sdk"; import { GAME } from "../libs/sdk";
import type { GenericRanking } from "../libs/generalTypes"; import type { GenericRanking } from "../libs/generalTypes";
@@ -9,7 +8,6 @@
import { t } from "../libs/i18n"; import { t } from "../libs/i18n";
import UserCard from "../components/UserCard.svelte"; import UserCard from "../components/UserCard.svelte";
import Tooltip from "../components/Tooltip.svelte"; import Tooltip from "../components/Tooltip.svelte";
import Pagination from "../components/Pagination.svelte";
export let game: GameName = 'mai2'; export let game: GameName = 'mai2';
@@ -17,45 +15,15 @@
let d: { users: GenericRanking[] }; let d: { users: GenericRanking[] };
let error: string | null; let error: string | null;
let page = 1
const perPage = 50
let totalPages = 1
function handleUpdatePage(event: CustomEvent<number>) {
page = event.detail;
const url = new URL(window.location.toString())
url.searchParams.set('page', page.toString())
history.pushState({}, '', url.toString())
window.scrollTo(0, 0)
}
onMount(() => {
const url = new URL(window.location.toString())
const pageParam = url.searchParams.get('page')
if (pageParam) {
page = parseInt(pageParam, 10) || 1
}
window.addEventListener('popstate', () => {
const url = new URL(window.location.toString())
const pageParam = url.searchParams.get('page')
page = parseInt(pageParam, 10) || 1
window.scrollTo(0, 0)
})
})
Promise.all([GAME.ranking(game)]) Promise.all([GAME.ranking(game)])
.then(([users]) => { .then(([users]) => {
d = { users } console.log(users)
totalPages = Math.ceil(users.length / perPage) d = { users };
}) })
.catch((e) => error = e.message); .catch((e) => error = e.message);
let hoveringUser = ""; let hoveringUser = "";
let hoverLoading = false; let hoverLoading = false;
$: paginatedUsers = d ? d.users.slice((page - 1) * perPage, page * perPage) : []
</script> </script>
<main class="content leaderboard"> <main class="content leaderboard">
@@ -69,12 +37,8 @@
</div> </div>
{#if d} {#if d}
{#if page > 1}
<Pagination {page} {totalPages} on:updatePage={handleUpdatePage} />
{/if}
<div class="leaderboard-container"> <div class="leaderboard-container">
<div class="lb-user" on:mouseenter={() => hoveringUser = paginatedUsers[0]?.username} role="heading" aria-level="2"> <div class="lb-user" on:mouseenter={() => hoveringUser = d.users[0].username} role="heading" aria-level="2">
<span class="rank">{t("Leaderboard.Rank")}</span> <span class="rank">{t("Leaderboard.Rank")}</span>
<span class="name"></span> <span class="name"></span>
<span class="rating">{t("Leaderboard.Rating")}</span> <span class="rating">{t("Leaderboard.Rating")}</span>
@@ -82,7 +46,7 @@
<span class="fc">{t("Leaderboard.FC")}</span> <span class="fc">{t("Leaderboard.FC")}</span>
<span class="ap">{t("Leaderboard.AP")}</span> <span class="ap">{t("Leaderboard.AP")}</span>
</div> </div>
{#each paginatedUsers as user, i (user.rank)} {#each d.users as user, i (user.rank)}
<div class="lb-user" class:alternate={i % 2 === 1} role="listitem" <div class="lb-user" class:alternate={i % 2 === 1} role="listitem"
on:mouseover={() => hoveringUser = user.username} on:focus={() => {}}> on:mouseover={() => hoveringUser = user.username} on:focus={() => {}}>
@@ -106,8 +70,6 @@
{/each} {/each}
</div> </div>
<Pagination {page} {totalPages} on:updatePage={handleUpdatePage} />
<Tooltip triggeredBy=".name" loading={hoverLoading}> <Tooltip triggeredBy=".name" loading={hoverLoading}>
<UserCard username={hoveringUser} {game} setLoading={l => hoverLoading = l} /> <UserCard username={hoveringUser} {game} setLoading={l => hoverLoading = l} />
</Tooltip> </Tooltip>
@@ -170,4 +132,5 @@
&.alternate &.alternate
background-color: vars.$ov-light background-color: vars.$ov-light
</style> </style>

View File

@@ -84,7 +84,7 @@
}).catch(err => error = err.message).finally(() => loading = false) }).catch(err => error = err.message).finally(() => loading = false)
} }
$: isBlacklist = blacklist.filter(x => src.dns.includes(x)).length > 0 $: isBlacklist = !!blacklist.filter(x => src.dns.includes(x))
</script> </script>
<StatusOverlays {loading} /> <StatusOverlays {loading} />

View File

@@ -23,7 +23,7 @@
let error: string; let error: string;
let submitting = "" let submitting = ""
let tab = 0 let tab = 0
let tabs = ['profile'] let tabs = [ 'profile', 'game' ]
const profileFields = [ const profileFields = [
[ 'displayName', t('settings.profile.name') ], [ 'displayName', t('settings.profile.name') ],
@@ -45,11 +45,18 @@
me = m me = m
CARD.userGames(m.username).then(games => { CARD.userGames(m.username).then(games => {
tabs = [ if (games.chu3 && !tabs.includes('chu3')) {
...tabs, tabs = [...tabs, 'chu3']
...['chu3', 'mai2','wacca', 'ongeki'].filter(v => games[v as keyof typeof games]), // :xdx: }
'global' if (games.mai2 && !tabs.includes('mai2')) {
] tabs = [...tabs, 'mai2']
}
if (games.wacca && !tabs.includes('wacca')) {
tabs = [...tabs, 'wacca']
}
if (games.ongeki && !tabs.includes('ongeki')) {
tabs = [...tabs, 'ongeki']
}
}) })
}).catch(e => error = e.message) }).catch(e => error = e.message)
getMe() getMe()
@@ -73,12 +80,11 @@
// Don't know why this isn't just a part of the cropper module. Have to do this myself.. What a shame // Don't know why this isn't just a part of the cropper module. Have to do this myself.. What a shame
let canvas = document.createElement("canvas"); let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d"); let ctx = canvas.getContext("2d");
const size = Math.round(Math.min(pfpCrop.width, pfpCrop.height, 1024)); canvas.width = 256;
canvas.width = size; canvas.height = 256;
canvas.height = size;
let img = document.createElement("img"); let img = document.createElement("img");
img.onload = () => { img.onload = () => {
ctx?.drawImage(img, pfpCrop.x, pfpCrop.y, pfpCrop.width, pfpCrop.height, 0, 0, size, size); ctx?.drawImage(img, pfpCrop.x, pfpCrop.y, pfpCrop.width, pfpCrop.height, 0, 0, 256, 256);
canvas.toBlob(blob => { canvas.toBlob(blob => {
if (!blob) return; if (!blob) return;
submitting = 'profilePicture' submitting = 'profilePicture'
@@ -210,7 +216,7 @@
<WaccaSettings /> <WaccaSettings />
{:else if tabs[tab] === 'ongeki'} {:else if tabs[tab] === 'ongeki'}
<OngekiSettings /> <OngekiSettings />
{:else if tabs[tab] === 'global'} {:else if tabs[tab] === 'game'}
<GeneralGameSettings /> <GeneralGameSettings />
{/if} {/if}

View File

@@ -325,6 +325,7 @@
<RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} game={game != "auto" ? game : "mai2"} top={10}/> <RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} game={game != "auto" ? game : "mai2"} top={10}/>
{/if} {/if}
<div class="recent"> <div class="recent">
<h2>{t('UserHome.RecentScores')}</h2> <h2>{t('UserHome.RecentScores')}</h2>
<div class="scores"> <div class="scores">
@@ -367,22 +368,6 @@
{/if} {/if}
</div> </div>
</div> </div>
{#if d.user.favorites != null && d.user.favorites.length > 0}
<div class="favorites">
<h2>{t('UserHome.FavoriteSongs')}</h2>
<div class="scores">
{#each d.user.favorites as favoriteSongId, i}
<div>
<img src={`${DATA_HOST}/d/${game}/music/00${favoriteSongId.toString().padStart(6, '0').substring(2)}.png`} alt="" on:error={coverNotFound} />
<div class="info">
<div class="song-title">{allMusics[favoriteSongId.toString()] ? allMusics[favoriteSongId.toString()].name : t("UserHome.UnknownSong")}</div>
</div>
</div>
{/each}
</div>
</div>
{/if}
{/if} {/if}
<StatusOverlays {error} loading={!d || isLoading} /> <StatusOverlays {error} loading={!d || isLoading} />
@@ -569,57 +554,6 @@
flex-direction: row flex-direction: row
justify-content: space-between justify-content: space-between
.favorites
.scores
display: grid
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr))
gap: 20px
// Image and song info
> div
display: flex
align-items: center
gap: 20px
background-color: rgba(white, 0.03)
border-radius: vars.$border-radius
img
width: 50px
height: 50px
border-radius: vars.$border-radius
object-fit: cover
// Song info and score
> div.info
flex: 1
display: flex
justify-content: space-between
overflow: hidden
flex-direction: column
.first-line
display: flex
flex-direction: row
// Limit song name to one line
.song-title
max-width: 90%
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
// Make song score and rank not wrap
> div:last-child
white-space: nowrap
@media (max-width: vars.$w-mobile)
flex-direction: column
gap: 0
.rank-text
text-align: left
// Recent Scores section // Recent Scores section
.recent .recent
.scores .scores

View File

@@ -20,20 +20,18 @@
let error = "" let error = ""
let verifyMsg = "" let verifyMsg = ""
let token = ""
if (USER.isLoggedIn()) { if (USER.isLoggedIn()) {
window.location.href = "/home" window.location.href = "/home"
} }
if (params.get('code')) {
token = params.get('code')! if (params.get('confirm-email')) {
if (location.pathname === '/verify') {
state = 'verify' state = 'verify'
verifyMsg = t("welcome.verifying") verifyMsg = t("welcome.verifying")
submitting = true submitting = true
// Send request to server // Send request to server
USER.confirmEmail(token) USER.confirmEmail(params.get('confirm-email')!)
.then(() => { .then(() => {
verifyMsg = t('welcome.verified') verifyMsg = t('welcome.verified')
submitting = false submitting = false
@@ -43,10 +41,7 @@
}) })
.catch(e => verifyMsg = t('welcome.verification-failed', { message: e.message })) .catch(e => verifyMsg = t('welcome.verification-failed', { message: e.message }))
} }
else if (location.pathname === '/reset-password') {
state = 'reset'
}
}
async function submit(): Promise<any> { async function submit(): Promise<any> {
submitting = true submitting = true
@@ -99,77 +94,14 @@
state = 'verify' state = 'verify'
verifyMsg = t("welcome.verify-state-2") verifyMsg = t("welcome.verify-state-2")
} }
else {
error = e.message
submitting = false // unnecessary? see line 113, same for both reset functions
turnstileReset()
}
})
}
submitting = false
}
async function resetPassword(): Promise<any> {
submitting = true;
if (email === "") {
error = t("welcome.email-missing")
return submitting = false
}
if (TURNSTILE_SITE_KEY && turnstile === "") {
// Sleep for 100ms to allow Turnstile to finish
error = t("welcome.waiting-turnstile")
return setTimeout(resetPassword, 100)
}
// Send request to server
await USER.resetPassword({ email, turnstile })
.then(() => {
// Show email sent message, reusing email verify page
state = 'verify'
verifyMsg = t("welcome.reset-password-sent", { email })
})
.catch(e => {
if (e.message === "Reset request rejected - STATE_0") {
state = 'verify'
verifyMsg = t("welcome.reset-state-0")
}
else if (e.message === "Reset request rejected - STATE_1") {
state = 'verify'
verifyMsg = t("welcome.reset-state-1")
}
else { else {
error = e.message error = e.message
submitting = false submitting = false
turnstileReset() turnstileReset()
} }
}) })
submitting = false
} }
async function changePassword(): Promise<any> {
submitting = true
if (password === "") {
error = t("welcome.password-missing")
return submitting = false
}
// Send request to server
await USER.changePassword({ token, password })
.then(() => {
state = 'verify'
verifyMsg = t("welcome.password-reset-done")
})
.catch(e => {
error = e.message
submitting = false
turnstileReset()
})
submitting = false submitting = false
} }
@@ -188,13 +120,11 @@
{#if error} {#if error}
<span class="error">{error}</span> <span class="error">{error}</span>
{/if} {/if}
{#if error != t("welcome.waiting-turnstile")}
<div on:click={() => state = 'home'} on:keypress={() => state = 'home'} <div on:click={() => state = 'home'} on:keypress={() => state = 'home'}
role="button" tabindex="0" class="clickable"> role="button" tabindex="0" class="clickable">
<Icon icon="line-md:chevron-small-left" /> <Icon icon="line-md:chevron-small-left" />
<span>{t('back')}</span> <span>{t('back')}</span>
</div> </div>
{/if}
{#if isSignup} {#if isSignup}
<input type="text" placeholder={t('username')} bind:value={username}> <input type="text" placeholder={t('username')} bind:value={username}>
{/if} {/if}
@@ -207,37 +137,6 @@
{isSignup ? t('welcome.btn-signup') : t('welcome.btn-login')} {isSignup ? t('welcome.btn-signup') : t('welcome.btn-login')}
{/if} {/if}
</button> </button>
{#if state === "login" && !submitting}
<button on:click={() => state = 'submitreset'}>{t('welcome.btn-reset-password')}</button>
{/if}
{#if TURNSTILE_SITE_KEY}
<Turnstile siteKey={TURNSTILE_SITE_KEY} bind:reset={turnstileReset}
on:turnstile-callback={e => console.log(turnstile = e.detail.token)}
on:turnstile-error={_ => console.log(error = t("welcome.turnstile-error"))}
on:turnstile-expired={_ => window.location.reload()}
on:turnstile-timeout={_ => console.log(error = t('welcome.turnstile-timeout'))} />
{/if}
</div>
{:else if state === "submitreset"}
<div class="login-form" transition:slide>
{#if error}
<span class="error">{error}</span>
{/if}
{#if error != t("welcome.waiting-turnstile")}
<div on:click={() => state = 'login'} on:keypress={() => state = 'login'}
role="button" tabindex="0" class="clickable">
<Icon icon="line-md:chevron-small-left" />
<span>{t('back')}</span>
</div>
{/if}
<input type="email" placeholder={t('email')} bind:value={email}>
<button on:click={resetPassword}>
{#if submitting}
<Icon icon="line-md:loading-twotone-loop"/>
{:else}
{t('welcome.btn-submit-reset-password')}
{/if}
</button>
{#if TURNSTILE_SITE_KEY} {#if TURNSTILE_SITE_KEY}
<Turnstile siteKey={TURNSTILE_SITE_KEY} bind:reset={turnstileReset} <Turnstile siteKey={TURNSTILE_SITE_KEY} bind:reset={turnstileReset}
on:turnstile-callback={e => console.log(turnstile = e.detail.token)} on:turnstile-callback={e => console.log(turnstile = e.detail.token)}
@@ -253,20 +152,6 @@
<button on:click={() => state = 'home'} transition:slide>{t('back')}</button> <button on:click={() => state = 'home'} transition:slide>{t('back')}</button>
{/if} {/if}
</div> </div>
{:else if state === "reset"}
{#if error}
<span class="error">{error}</span>
{/if}
<div class="login-form" transition:slide>
<input type="password" placeholder={t('new-password')} bind:value={password}>
<button on:click={changePassword}>
{#if submitting}
<Icon icon="line-md:loading-twotone-loop"/>
{:else}
{t('welcome.btn-submit-new-password')}
{/if}
</button>
</div>
{/if} {/if}
</div> </div>

View File

@@ -33,9 +33,9 @@ Below is a list of games supported by this server.
| Game | Ver | Codename | Thanks to | | Game | Ver | Codename | Thanks to |
|------------------------|------|-------------|------------------------------------------------------| |------------------------|------|-------------|------------------------------------------------------|
| SDHD: CHUNITHM | 2.40 | X-VERSE | | | SDHD: CHUNITHM | 2.30 | VERSE | |
| SDEZ: MaiMai DX | 1.55 | PRiSM Plus | | | SDEZ: MaiMai DX | 1.55 | PRiSM Plus | |
| SDGA: MaiMai DX (Intl) | 1.55 | PRiSM | [@Clansty](https://github.com/clansty) | | SDGA: MaiMai DX (Intl) | 1.50 | PRiSM | [@Clansty](https://github.com/clansty) |
| SDED: Card Maker | 1.39 | | [@Becods](https://github.com/Becods) | | SDED: Card Maker | 1.39 | | [@Becods](https://github.com/Becods) |
| SDDT: O.N.G.E.K.I. | 1.50 | Re:Fresh | [@PenguinCaptain](https://github.com/PenguinCaptain) | | SDDT: O.N.G.E.K.I. | 1.50 | Re:Fresh | [@PenguinCaptain](https://github.com/PenguinCaptain) |
| SBZV: Project DIVA | 7.10 | Future Tone | | | SBZV: Project DIVA | 7.10 | Future Tone | |
@@ -45,8 +45,8 @@ Check out these docs for more information.
* [Game specific notes](docs/game_specific_notes.md) * [Game specific notes](docs/game_specific_notes.md)
* [Frequently asked questions](docs/frequently_asked_questions.md) * [Frequently asked questions](docs/frequently_asked_questions.md)
> [!WARNING] > [!TIP]
> CHUNITHM pre-NEW!! and maimai pre-DX are no longer supported after February 6th, 2026 and all associated data will be removed. > Some games may require additional patches and these will not be provided in this project and repository. You already found this, so you know where to find related resources too.
## Usage ## Usage
If you own a cab or controller and just want to play the game, follow the instructions below: If you own a cab or controller and just want to play the game, follow the instructions below:
@@ -59,7 +59,7 @@ If you own a cab or controller and just want to play the game, follow the instru
5. Pet your cat 🐱 5. Pet your cat 🐱
6. Link your card on the website. 6. Link your card on the website.
If you encounter any issue, please report in the [issue tracker](https://github.com/MewoLab/AquaDX/issues). If you encounter any issue, please report in the [issue tracker](https://MewoLab/AquaDX/issues).
> [!TIP] > [!TIP]
> If you don't know your card ID, there's always a button on the login screen of the game that can read a card's access code. > If you don't know your card ID, there's always a button on the login screen of the game that can read a card's access code.

View File

@@ -7,12 +7,14 @@ plugins {
val ktVer = "2.1.10" val ktVer = "2.1.10"
java java
kotlin("plugin.lombok") version ktVer
kotlin("jvm") version ktVer kotlin("jvm") version ktVer
kotlin("plugin.spring") version ktVer kotlin("plugin.spring") version ktVer
kotlin("plugin.jpa") version ktVer kotlin("plugin.jpa") version ktVer
kotlin("plugin.serialization") version ktVer kotlin("plugin.serialization") version ktVer
kotlin("plugin.allopen") version ktVer kotlin("plugin.allopen") version ktVer
kotlin("kapt") version ktVer kotlin("kapt") version ktVer
id("io.freefair.lombok") version "8.6"
id("org.springframework.boot") version "3.2.3" id("org.springframework.boot") version "3.2.3"
id("com.github.ben-manes.versions") version "0.51.0" id("com.github.ben-manes.versions") version "0.51.0"
id("org.hibernate.orm") version "6.4.4.Final" id("org.hibernate.orm") version "6.4.4.Final"
@@ -169,21 +171,3 @@ sourceSets {
java.srcDir("${layout.buildDirectory.get()}/generated/source/kapt/main") java.srcDir("${layout.buildDirectory.get()}/generated/source/kapt/main")
} }
} }
val copyDependencies by tasks.registering(Copy::class) {
from(configurations.runtimeClasspath)
into("${layout.buildDirectory.get()}/libs/lib")
}
val packageThin by tasks.registering(Jar::class) {
group = "build"
from(sourceSets.main.get().output)
manifest {
attributes(
"Main-Class" to "icu.samnyan.aqua.EntryKt",
"Class-Path" to configurations.runtimeClasspath.get().files.joinToString(" ") { "lib/${it.name}" }
)
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
dependsOn(copyDependencies)
}

View File

@@ -131,11 +131,6 @@ server.error.whitelabel.enabled=false
aqua-net.frontier.enabled=false aqua-net.frontier.enabled=false
aqua-net.frontier.ftk=0x00 aqua-net.frontier.ftk=0x00
## Fedy Settings
aqua-net.fedy.enabled=false
aqua-net.fedy.key=maigo
aqua-net.fedy.remote=http://localhost:2528/api/fedy
## APIs for bot management ## APIs for bot management
aqua-net.bot.enabled=true aqua-net.bot.enabled=true
aqua-net.bot.secret=hunter2 aqua-net.bot.secret=hunter2

View File

@@ -59,7 +59,7 @@ Located at: [icu.samnyan.aqua.net.UserRegistrar](icu/samnyan/aqua/net/UserRegist
* token: String * token: String
* **Returns**: User information * **Returns**: User information
**/user/login** : Login with email/username and password. This will also check if the email is verified and send another confirmation. **/user/login** : Login with email/username and password. This will also check if the email is verified and send another confirmation
* email: String * email: String
* password: String * password: String
@@ -74,18 +74,6 @@ Located at: [icu.samnyan.aqua.net.UserRegistrar](icu/samnyan/aqua/net/UserRegist
* turnstile: String * turnstile: String
* **Returns**: Success message * **Returns**: Success message
**/user/reset-password** : Send the user a reset password email. This will also check if the email is verified or if many requests were sent recently.
* email: String
* turnstile: String
* **Returns** Success message
**/user/change-password** : Reset a user's password with a token sent through email to the user.
* token: String
* password: String
* **Returns** Success message
**/user/setting** : Validate and set a user setting field. **/user/setting** : Validate and set a user setting field.
* token: String * token: String

View File

@@ -15,7 +15,6 @@ import kotlinx.coroutines.withContext
import org.apache.tika.Tika import org.apache.tika.Tika
import org.apache.tika.mime.MimeTypes import org.apache.tika.mime.MimeTypes
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationContext
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity.BodyBuilder import org.springframework.http.ResponseEntity.BodyBuilder
@@ -35,15 +34,12 @@ import java.util.concurrent.locks.Lock
import kotlin.reflect.KCallable import kotlin.reflect.KCallable
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmErasure
typealias RP = RequestParam typealias RP = RequestParam
typealias RB = RequestBody typealias RB = RequestBody
typealias RT = RequestPart
typealias RH = RequestHeader typealias RH = RequestHeader
typealias PV = PathVariable typealias PV = PathVariable
typealias API = RequestMapping typealias API = RequestMapping
@@ -83,9 +79,7 @@ annotation class SettingField(
// Reflection // Reflection
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Any> KClass<T>.ownVars() = declaredMemberProperties.sortedBy { it.javaField?.declaringClass?.declaredFields?.indexOf(it.javaField) ?: Int.MAX_VALUE }.mapNotNull { it as? Var<T, Any> } fun <T : Any> KClass<T>.vars() = memberProperties.mapNotNull { it as? Var<T, Any> }
@Suppress("UNCHECKED_CAST")
fun <T : Any> KClass<T>.vars(): List<Var<T, Any>> = supertypes.mapNotNull { it.classifier as? KClass<*> }.filter { !it.java.isInterface }.flatMap{ it.vars() as List<Var<T, Any>> } + ownVars()
fun <T : Any> KClass<T>.varsMap() = vars().associateBy { it.name } fun <T : Any> KClass<T>.varsMap() = vars().associateBy { it.name }
fun <T : Any> KClass<T>.getters() = java.methods.filter { it.name.startsWith("get") } fun <T : Any> KClass<T>.getters() = java.methods.filter { it.name.startsWith("get") }
fun <T : Any> KClass<T>.gettersMap() = getters().associateBy { it.name.removePrefix("get").firstCharLower() } fun <T : Any> KClass<T>.gettersMap() = getters().associateBy { it.name.removePrefix("get").firstCharLower() }
@@ -217,8 +211,6 @@ val <K, V> Map<K, V>.mut get() = toMutableMap()
val <T> Set<T>.mut get() = toMutableSet() val <T> Set<T>.mut get() = toMutableSet()
fun <T> List<T>.unique(fn: (T) -> Any) = distinctBy(fn).ifEmpty { null } fun <T> List<T>.unique(fn: (T) -> Any) = distinctBy(fn).ifEmpty { null }
val <T> Collection<T>.csv get() = joinToString(",")
val IntArray.csv get() = joinToString(",")
// Optionals // Optionals
operator fun <T> Optional<T>.invoke(): T? = orElse(null) operator fun <T> Optional<T>.invoke(): T? = orElse(null)
@@ -235,7 +227,6 @@ fun Str.fromChusanUsername() = String(this.toByteArray(StandardCharsets.ISO_8859
fun Str.truncate(len: Int) = if (this.length > len) this.take(len) + "..." else this fun Str.truncate(len: Int) = if (this.length > len) this.take(len) + "..." else this
val Str.some get() = ifBlank { null } val Str.some get() = ifBlank { null }
val ByteArray.hexStr get() = toHexString() val ByteArray.hexStr get() = toHexString()
operator fun StringBuilder.plusAssign(other: String) { this.append(other) }
// Coroutine // Coroutine
suspend fun <T> async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() } suspend fun <T> async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() }
@@ -264,7 +255,6 @@ operator fun <E> List<E>.component13(): E = get(12)
inline operator fun <reified E> List<Any?>.invoke(i: Int) = get(i) as E inline operator fun <reified E> List<Any?>.invoke(i: Int) = get(i) as E
val empty = emptyList<Any>() val empty = emptyList<Any>()
val emptyMap = emptyMap<Any, Any>()
val <F> Pair<F, *>.l get() = component1() val <F> Pair<F, *>.l get() = component1()
val <S> Pair<*, S>.r get() = component2() val <S> Pair<*, S>.r get() = component2()
@@ -273,6 +263,3 @@ val <S> Pair<*, S>.r get() = component2()
val Query.exec get() = resultList.map { (it as Array<*>).toList() } val Query.exec get() = resultList.map { (it as Array<*>).toList() }
fun List<List<Any?>>.numCsv(vararg head: Str) = head.joinToString(",") + "\n" + fun List<List<Any?>>.numCsv(vararg head: Str) = head.joinToString(",") + "\n" +
joinToString("\n") { it.joinToString(",") } joinToString("\n") { it.joinToString(",") }
// DI
inline fun <reified T> ApplicationContext.lazy() = lazy { getBean(T::class.java) }

View File

@@ -8,8 +8,6 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy import kotlinx.serialization.json.JsonNamingStrategy
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
// Jackson // Jackson
val ACCEPTABLE_FALSE = setOf("0", "false", "no", "off", "False", "None", "null") val ACCEPTABLE_FALSE = setOf("0", "false", "no", "off", "False", "None", "null")
@@ -21,15 +19,9 @@ val JSON_FUZZY_BOOLEAN = SimpleModule().addDeserializer(Boolean::class.java, obj
else -> 400 - "Invalid boolean value ${parser.text}" else -> 400 - "Invalid boolean value ${parser.text}"
} }
}) })
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<LocalDateTime>() { val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<java.time.LocalDateTime>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext) = override fun deserialize(parser: JsonParser, context: DeserializationContext) =
// First try standard formats via asDateTime() method parser.text.asDateTime() ?: (400 - "Invalid date time value ${parser.text}")
parser.text.takeIf { it.isNotEmpty() }?.run { asDateTime() ?: try {
// Try maimai2 format (yyyy-MM-dd HH:mm:ss.0)
LocalDateTime.parse(parser.text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0"))
} catch (e: Exception) {
400 - "Invalid date time value ${parser.text}"
} }
}) })
val JACKSON = jacksonObjectMapper().apply { val JACKSON = jacksonObjectMapper().apply {
setSerializationInclusion(JsonInclude.Include.NON_NULL) setSerializationInclusion(JsonInclude.Include.NON_NULL)

View File

@@ -4,7 +4,6 @@ import ext.*
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.chusan.model.Chu3Repos import icu.samnyan.aqua.sega.chusan.model.Chu3Repos
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.model.CardStatus import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.model.sensitiveInfo import icu.samnyan.aqua.sega.general.model.sensitiveInfo
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
@@ -53,7 +52,7 @@ class BotController(
secret.checkSecret() secret.checkSecret()
// 1. Find user card // 1. Find user card
val oc = (us.cardRepo.findByLuid(card) ?: (404 - "Card not found")).maybeGhost() val oc = (us.cardRepo.findByLuid(card)() ?: (404 - "Card not found")).maybeGhost()
// 2. Change the status to migrated // 2. Change the status to migrated
us.cardRepo.save(oc.apply { us.cardRepo.save(oc.apply {
@@ -67,7 +66,7 @@ class BotController(
fun clearMigrateFlag(@RP secret: Str, @RP card: Str): Any { fun clearMigrateFlag(@RP secret: Str, @RP card: Str): Any {
secret.checkSecret() secret.checkSecret()
val oc = (us.cardRepo.findByLuid(card) ?: (404 - "Card not found")).maybeGhost() val oc = (us.cardRepo.findByLuid(card)() ?: (404 - "Card not found")).maybeGhost()
us.cardRepo.save(oc.apply { us.cardRepo.save(oc.apply {
status = CardStatus.NORMAL status = CardStatus.NORMAL
@@ -83,14 +82,14 @@ class BotController(
secret.checkSecret() secret.checkSecret()
// 1. Check if the card exist // 1. Check if the card exist
var cards: MutableList<Card> = listOfNotNull( var cards = listOfNotNull(
us.cardRepo.findByLuid(cardId), us.cardRepo.findByLuid(cardId)(),
).mut ).mut
cardId.toLongOrNull()?.let { cardId.toLongOrNull()?.let {
cards += listOfNotNull( cards += listOfNotNull(
us.cardRepo.findById(it)(), us.cardRepo.findById(it)(),
us.cardRepo.findByExtId(it), us.cardRepo.findByExtId(it)(),
) )
cards += listOfNotNull( cards += listOfNotNull(
@@ -111,8 +110,8 @@ class BotController(
return cards.map { card -> return cards.map { card ->
// Find all games played by this card // Find all games played by this card
val chu3 = chu3Db.userData.findByCard_ExtId(card.extId) val chu3 = chu3Db.userData.findByCard_ExtId(card.extId)()
val mai2 = mai2Db.userData.findByCard_ExtId(card.extId) val mai2 = mai2Db.userData.findByCard_ExtId(card.extId)()
val gamesDict = listOfNotNull(chu3, mai2).map { val gamesDict = listOfNotNull(chu3, mai2).map {
// Find the keychip owner // Find the keychip owner
val keychip = it.lastClientId val keychip = it.lastClientId

View File

@@ -8,7 +8,6 @@ import icu.samnyan.aqua.net.games.IUserData
import icu.samnyan.aqua.net.utils.AquaNetProps import icu.samnyan.aqua.net.utils.AquaNetProps
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.chusan.model.Chu3UserDataRepo import icu.samnyan.aqua.sega.chusan.model.Chu3UserDataRepo
import icu.samnyan.aqua.sega.diva.PlayerProfileRepository
import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService import icu.samnyan.aqua.sega.general.service.CardService
@@ -20,6 +19,7 @@ import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.jvm.optionals.getOrNull
import kotlin.random.Random import kotlin.random.Random
@RestController @RestController
@@ -30,8 +30,7 @@ class CardController(
val cardService: CardService, val cardService: CardService,
val cardGameService: CardGameService, val cardGameService: CardGameService,
val cardRepository: CardRepository, val cardRepository: CardRepository,
val props: AquaNetProps, val props: AquaNetProps
val fedy: Fedy
) { ) {
companion object { companion object {
val log = logger() val log = logger()
@@ -81,12 +80,10 @@ class CardController(
val id = cardService.sanitizeCardId(cardId) val id = cardService.sanitizeCardId(cardId)
// Create a new card // Create a new card
val newCard = cardService.registerByAccessCode(id, u) cardService.registerByAccessCode(id, u)
log.info("Net /card/link : Created new card $id for user ${u.username}") log.info("Net /card/link : Created new card $id for user ${u.username}")
fedy.onCardLinked(newCard.luid, oldExtId = null, ghostExtId = u.ghostCard.extId, emptyList())
return SUCCESS return SUCCESS
} }
@@ -101,8 +98,6 @@ class CardController(
val games = migrate.split(',') val games = migrate.split(',')
cardGameService.migrate(card, games) cardGameService.migrate(card, games)
fedy.onCardLinked(card.luid, oldExtId = card.extId, ghostExtId = u.ghostCard.extId, games)
log.info("Net /card/link : Linked card ${card.id} to user ${u.username} and migrated data to ${games.joinToString()}") log.info("Net /card/link : Linked card ${card.id} to user ${u.username} and migrated data to ${games.joinToString()}")
SUCCESS SUCCESS
@@ -120,14 +115,10 @@ class CardController(
// Ghost cards cannot be unlinked // Ghost cards cannot be unlinked
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked" if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
val luid = card.luid
// Unbind the card // Unbind the card
card.aquaUser = null card.aquaUser = null
async { cardRepository.save(card) } async { cardRepository.save(card) }
fedy.onCardUnlinked(luid)
log.info("Net /card/unlink : Unlinked card ${card.id} from user ${u.username}") log.info("Net /card/unlink : Unlinked card ${card.id} from user ${u.username}")
SUCCESS SUCCESS
@@ -145,7 +136,7 @@ class CardController(
* *
* Assumption: The card is already linked to the user. * Assumption: The card is already linked to the user.
*/ */
suspend fun <T : IUserData> migrateCard(gameName: Str, repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card): Bool { suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card): Bool {
val ghost = card.aquaUser!!.ghostCard val ghost = card.aquaUser!!.ghostCard
// Check if data already exists in the user's ghost card // Check if data already exists in the user's ghost card
@@ -153,7 +144,7 @@ suspend fun <T : IUserData> migrateCard(gameName: Str, repo: GenericUserDataRepo
// Create a new dummy card for deleted data // Create a new dummy card for deleted data
it.card = async { it.card = async {
cardRepo.save(Card().apply { cardRepo.save(Card().apply {
luid = "Migrated data of ghost card ${ghost.id} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()} (${gameName})" luid = "Migrated data of ghost card ${ghost.id} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()}"
// Randomize an extId outside the normal range // Randomize an extId outside the normal range
extId = Random.nextLong(0x7FFFFFF7L shl 32, 0x7FFFFFFFL shl 32) extId = Random.nextLong(0x7FFFFFF7L shl 32, 0x7FFFFFFFL shl 32)
registerTime = LocalDateTime.now() registerTime = LocalDateTime.now()
@@ -171,23 +162,6 @@ suspend fun <T : IUserData> migrateCard(gameName: Str, repo: GenericUserDataRepo
return true return true
} }
suspend fun <T : IUserData> orphanData(gameName: Str, repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card) {
// Orphan the data by assigning them to a dummy card
repo.findByCard(card)?.let {
// Create a new dummy card for orphaned data
it.card = async {
cardRepo.save(Card().apply {
luid = "Unmigrated data of card ${card.luid} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()} (${gameName})"
// Randomize an extId outside the normal range
extId = Random.nextLong(0x7FFFFFF7L shl 32, 0x7FFFFFFFL shl 32)
registerTime = LocalDateTime.now()
accessTime = registerTime
})
}
async { repo.save(it) }
}
}
suspend fun getSummaryFor(repo: GenericUserDataRepo<*>, card: Card): Map<Str, Any>? { suspend fun getSummaryFor(repo: GenericUserDataRepo<*>, card: Card): Map<Str, Any>? {
val data = async { repo.findByCard(card) } ?: return null val data = async { repo.findByCard(card) } ?: return null
return mapOf( return mapOf(
@@ -203,11 +177,10 @@ class CardGameService(
val chusan: Chu3UserDataRepo, val chusan: Chu3UserDataRepo,
val wacca: WcUserRepo, val wacca: WcUserRepo,
val ongeki: OgkUserDataRepo, val ongeki: OgkUserDataRepo,
val diva: PlayerProfileRepository, val diva: icu.samnyan.aqua.sega.diva.dao.userdata.PlayerProfileRepository,
val safety: AquaNetSafetyService, val safety: AquaNetSafetyService,
val cardRepo: CardRepository, val cardRepo: CardRepository,
val em: EntityManager, val em: EntityManager
val cardService: CardService
) { ) {
companion object { companion object {
val log = logger() val log = logger()
@@ -216,22 +189,18 @@ class CardGameService(
suspend fun migrate(crd: Card, games: List<String>) = async { suspend fun migrate(crd: Card, games: List<String>) = async {
// Migrate data from the card to the user's ghost card // Migrate data from the card to the user's ghost card
// An easy migration is to change the UserData card field to the user's ghost card // An easy migration is to change the UserData card field to the user's ghost card
val dataRepos = mapOf(
"mai2" to maimai2,
"chu3" to chusan,
"ongeki" to ongeki,
"wacca" to wacca,
)
val remainingGames = dataRepos.keys.toMutableSet()
games.forEach { game -> games.forEach { game ->
val dataRepo = dataRepos[game] ?: return@forEach when (game) {
if (migrateCard(game, dataRepo, cardRepo, crd)) "mai2" -> migrateCard(maimai2, cardRepo, crd)
// Update timestamp for the ghost card (data migrated in) "chu3" -> migrateCard(chusan, cardRepo, crd)
cardService.updateCardTimestamp(crd.aquaUser!!.ghostCard, game, resetCreatedAt = true) "ongeki" -> migrateCard(ongeki, cardRepo, crd)
remainingGames.remove(game) "wacca" -> migrateCard(wacca, cardRepo, crd)
// TODO: diva
// "diva" -> diva.findByPdId(card.extId.toInt()).getOrNull()?.let {
// it.pdId = card.aquaUser!!.ghostCard
// }
}
} }
// For remaining games, orphan the data by assigning them to a dummy card
remainingGames.forEach { game -> orphanData(game, dataRepos[game]!!, cardRepo, crd) }
} }
suspend fun getSummary(card: Card) = async { suspend fun getSummary(card: Card) = async {
@@ -240,7 +209,7 @@ class CardGameService(
"chu3" to getSummaryFor(chusan, card), "chu3" to getSummaryFor(chusan, card),
"ongeki" to getSummaryFor(ongeki, card), "ongeki" to getSummaryFor(ongeki, card),
"wacca" to getSummaryFor(wacca, card), "wacca" to getSummaryFor(wacca, card),
"diva" to diva.findByPdId(card.extId)()?.let { "diva" to diva.findByPdId(card.extId).getOrNull()?.let {
mapOf( mapOf(
"name" to it.playerName, "name" to it.playerName,
"rating" to it.level, "rating" to it.level,

View File

@@ -1,370 +0,0 @@
package icu.samnyan.aqua.net
import ext.*
import icu.samnyan.aqua.net.components.EmailProperties
import icu.samnyan.aqua.net.components.JWT
import icu.samnyan.aqua.net.db.AquaGameOptions
import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.games.ExportOptions
import icu.samnyan.aqua.net.games.GenericUserDataRepo
import icu.samnyan.aqua.net.games.IUserData
import icu.samnyan.aqua.net.games.mai2.Mai2Import
import icu.samnyan.aqua.net.utils.ApiException
import icu.samnyan.aqua.net.utils.PathProps
import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.chusan.model.Chu3UserDataRepo
import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
import icu.samnyan.aqua.sega.ongeki.OgkUserDataRepo
import icu.samnyan.aqua.sega.wacca.model.db.WcUserRepo
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate
import org.springframework.web.bind.annotation.RestController
import org.springframework.context.ApplicationContext
import org.springframework.web.multipart.MultipartFile
import java.time.Instant
import java.security.MessageDigest
import java.util.concurrent.CompletableFuture
import kotlin.io.path.getLastModifiedTime
import kotlin.io.path.isRegularFile
import kotlin.io.path.writeBytes
import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler as Mai2UploadUserPlaylogHandler
import icu.samnyan.aqua.sega.maimai2.handler.UpsertUserAllHandler as Mai2UpsertUserAllHandler
@Configuration
@ConfigurationProperties(prefix = "aqua-net.fedy")
class FedyProps {
var enabled: Boolean = false
var key: String = ""
var remote: String = ""
}
data class UserProfilePicture(val url: Str, val updatedAtMs: Long)
data class UserBasicInfo(
val auId: Long, val ghostExtId: Long, val registrationTimeMs: Long,
val username: Str, val displayName: Str, val email: Str, val passwordHash: Str, val profileBio: Str,
val profilePicture: UserProfilePicture?, val gameOptions: Map<Str, Any?>?,
)
private data class UserUpdatedEvent(val user: UserBasicInfo, val isNewlyCreated: Bool)
private data class CardCreatedEvent(val luid: Str, val extId: Long)
private data class CardLinkedEvent(val luid: Str, val oldExtId: Long?, val ghostExtId: Long, val migratedGames: List<Str>)
private data class CardUnlinkedEvent(val luid: Str)
private data class DataUpdatedEvent(val extId: Long, val isGhostCard: Bool, val game: Str, val removeOldData: Bool)
private data class FedyEvent(
var userUpdated: UserUpdatedEvent? = null,
var cardCreated: CardCreatedEvent? = null,
var cardLinked: CardLinkedEvent? = null,
var cardUnlinked: CardUnlinkedEvent? = null,
var dataUpdated: DataUpdatedEvent? = null,
)
@RestController
@API("/api/v2/fedy", consumes = ["multipart/form-data"])
class Fedy(
val jwt: JWT,
val emailProps: EmailProperties,
val cardRepo: CardRepository,
val mai2UserDataRepo: Mai2UserDataRepo,
val chu3UserDataRepo: Chu3UserDataRepo,
val ongekiUserDataRepo: OgkUserDataRepo,
val waccaUserDataRepo: WcUserRepo,
val props: FedyProps,
val paths: PathProps,
val transactionManager: PlatformTransactionManager,
ctx: ApplicationContext
) {
val us by ctx.lazy<AquaUserServices>()
val cardService by ctx.lazy<CardService>()
val mai2Import by ctx.lazy<Mai2Import>()
val mai2UploadUserPlaylog by ctx.lazy<Mai2UploadUserPlaylogHandler>()
val mai2UpsertUserAll by ctx.lazy<Mai2UpsertUserAllHandler>()
val transaction by lazy { TransactionTemplate(transactionManager) }
private fun Str.checkKey() {
if (!props.enabled) 403 - "Fedy is disabled"
if (!MessageDigest.isEqual(this.toByteArray(), props.key.toByteArray())) 403 - "Invalid Key"
}
val suppressEvents = ThreadLocal.withInitial { false }
private fun <T> handleFedy(key: Str, block: () -> T): T {
val old = suppressEvents.get()
suppressEvents.set(true)
try {
key.checkKey()
return block()
} finally { suppressEvents.set(old) }
}
data class FedyErr(val code: Int, val message: Str)
data class UserPullReq(val auId: Long)
data class UserPullRes(val user: UserBasicInfo?)
@API("/user/pull")
fun handleUserPull(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: UserPullReq): UserPullRes = handleFedy(key) {
UserPullRes(us.userRepo.findByAuId(req.auId)?.fedyBasicInfo())
}
data class UserLookupReq(val username: Str?, val email: Str?)
data class UserLookupRes(val user: UserBasicInfo?)
@API("/user/lookup")
fun handleUserLogin(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: UserLookupReq): UserLookupRes = handleFedy(key) {
UserLookupRes(user =
(req.username?.let { us.userRepo.findByUsernameIgnoreCase(it) } ?: req.email?.let {us.userRepo.findByEmailIgnoreCase(it) })
?.takeIf { it.emailConfirmed || !emailProps.enable }
?.fedyBasicInfo()
)
}
data class UserRegisterReq(val username: Str, val email: Str, val password: Str)
data class UserRegisterRes(val error: FedyErr? = null, val user: UserBasicInfo? = null)
@API("/user/register")
fun handleUserRegister(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: UserRegisterReq): UserRegisterRes = handleFedy(key) {
{
UserRegisterRes(user = us.create(req.username, req.email, req.password, "", emailConfirmed = true).fedyBasicInfo())
} caught { UserRegisterRes(error = it) }
}
data class UserUpdateReq(val auId: Long, val fields: Map<Str, Str?>?, val gameOptions: Map<Str, Any?>?)
data class UserUpdateRes(val error: FedyErr? = null, val user: UserBasicInfo? = null)
@API("/user/update")
fun handleUserUpdate(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: UserUpdateReq, @RT(PFP_PART) pfpFile: MultipartFile?): UserUpdateRes = handleFedy(key) {
{
val ru = us.userRepo.findByAuId(req.auId) ?: (404 - "User not found")
val fields = req.fields?.filterValues { it != null }?.mapValues { it.value as Str } ?: emptyMap()
fields.forEach { (k, v) ->
if (k == "email") { ru.email = us.validateEmail(v) }
else us.update(ru, k, v)
}
pfpFile?.apply {
val mime = TIKA.detect(pfpFile.bytes).takeIf { it.startsWith("image/") } ?: (400 - "Invalid file type")
val name = "${ru.auId}${MIMES.forName(mime)?.extension ?: ".jpg"}"
(paths.aquaNetPortrait.path() / name).writeBytes(bytes)
ru.profilePicture = name
}
req.gameOptions?.apply {
val options = ru.gameOptions ?: AquaGameOptions().also { ru.gameOptions = it }
forEach { (k, v) -> v?.let { GAME_OPTIONS_FIELDS[k]?.set(options, it) } }
}
us.userRepo.save(ru)
if (fields.containsKey("pwHash") ?: false) { us.clearAllSessions(ru) }
UserUpdateRes(user = ru.fedyBasicInfo())
} caught { UserUpdateRes(error = it) }
}
private fun AquaNetUser.fedyBasicInfo() = UserBasicInfo(
auId, ghostCard.extId, regTime,
username, displayName, email, pwHash, profileBio ?: "",
profilePicture
?.let { paths.aquaNetPortrait.path() / it }?.takeIf { it.isRegularFile() }
?.let { UserProfilePicture(
url = "/uploads/net/portrait/${profilePicture}",
updatedAtMs = it.getLastModifiedTime().toMillis()
) },
gameOptions?.let { o -> GAME_OPTIONS_FIELDS.mapValues { it.value.get(o) } }
)
data class DataPullReq(val extId: Long, val game: Str, val createdAtMs: Long, val updatedAtMs: Long, val exportOptions: ExportOptions)
data class DataPullResult(val data: Any?, val createdAtMs: Long, val updatedAtMs: Long, val isRebased: Bool)
data class DataPullRes(val error: FedyErr? = null, val result: DataPullResult? = null)
@API("/data/pull")
fun handleDataPull(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: DataPullReq): DataPullRes = handleFedy(key) {
val card = cardRepo.findByExtId(req.extId)
?: (404 - "Card with extId ${req.extId} not found")
val cardTimestamp = cardService.getCardTimestamp(card, req.game)
if (cardTimestamp.updatedAt.toEpochMilli() == req.updatedAtMs) return@handleFedy DataPullRes(error = null, result = null) // No changes
val isRebased = req.createdAtMs > 0 && cardTimestamp.createdAt.toEpochMilli() > req.createdAtMs
val exportOptions = if (!isRebased) { req.exportOptions } else { req.exportOptions.copy(playlogAfter = null) }
{
DataPullRes(result = DataPullResult(data = when (req.game) {
"mai2" -> mai2Import.export(card, exportOptions)
else -> 406 - "Unsupported game"
}, createdAtMs = cardTimestamp.createdAt.toEpochMilli(), updatedAtMs = cardTimestamp.updatedAt.toEpochMilli(), isRebased = isRebased))
} caught { DataPullRes(error = it) }
}
data class DataPushReq(val extId: Long, val game: Str, val data: JDict, val removeOldData: Bool, val updatedAtMs: Long)
@Suppress("UNCHECKED_CAST")
@API("/data/push")
fun handleDataPush(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: DataPushReq): Any = handleFedy(key) {
val extId = req.extId
fun<UserData : IUserData, UserRepo : GenericUserDataRepo<UserData>> removeOldData(repo: UserRepo) {
repo.findByCard_ExtId(extId)?.let { oldData ->
log.info("Fedy: Deleting old data for $extId (${req.game})")
repo.delete(oldData);
repo.flush()
}
}
val card = cardRepo.findByExtId(extId) ?: (404 - "Card not found")
transaction.execute { when (req.game) {
"mai2" -> {
if (req.removeOldData) { removeOldData(mai2UserDataRepo) }
val userAll = req.data["upsertUserAll"] as JDict // UserAll first, prevent using backlog
mai2UpsertUserAll.handle(mapOf("userId" to extId, "upsertUserAll" to userAll))
val playlogs = req.data["userPlaylogList"] as List<JDict>
playlogs.forEach { mai2UploadUserPlaylog.handle(mapOf("userId" to extId, "userPlaylog" to it)) }
}
else -> 406 - "Unsupported game"
} }
cardService.updateCardTimestamp(card, req.game, now = Instant.ofEpochMilli(req.updatedAtMs), resetCreatedAt = req.removeOldData)
SUCCESS
}
data class CardResolveReq(val luid: Str, val pairedLuid: Str?, val createIfNotFound: Bool)
data class CardResolveRes(val extId: Long, val isGhostCard: Bool, val isNewlyCreated: Bool, val isPairedLuidDiverged: Bool)
@API("/card/resolve")
fun handleCardResolve(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: CardResolveReq): CardResolveRes = handleFedy(key) {
var card = cardService.tryLookup(req.luid)
var isNewlyCreated = false
if (card != null) {
card = card.maybeGhost()
if (!card.isGhost) isNewlyCreated = isCardFresh(card)
} else if (req.createIfNotFound) {
card = cardService.registerByAccessCode(req.luid, null)
isNewlyCreated = true
log.info("Fedy /card/resolve : Created new card ${card.id} (${card.luid})")
}
var isPairedLuidDiverged = false
if (req.pairedLuid != null) {
var pairedCard = cardService.tryLookup(req.pairedLuid)?.maybeGhost()
if (pairedCard?.extId != card?.extId) {
var isGhost = pairedCard?.isGhost == true
var isNonFresh = pairedCard != null && !isCardFresh(pairedCard)
if (isGhost || isNonFresh) isPairedLuidDiverged = true
else if (card?.isGhost == true) {
// Ensure paired card is linked, if the main card is linked
// If the main card is not linked, there's nothing Fedy can do. It's Fedy's best effort.
if (pairedCard == null) { pairedCard = cardService.registerByAccessCode(req.pairedLuid, card.aquaUser) }
else { pairedCard.aquaUser = card.aquaUser; cardRepo.save(pairedCard) }
log.info("Fedy /card/resolve : Created paired card ${pairedCard.id} (${pairedCard.luid}) for user ${card.aquaUser?.auId} (${card.aquaUser?.username})")
}
}
}
CardResolveRes(
card?.extId ?: 0,
card?.isGhost ?: false,
isNewlyCreated,
isPairedLuidDiverged)
}
data class CardLinkReq(val auId: Long, val luid: Str)
@API("/card/link")
fun handleCardLink(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: CardLinkReq): Any = handleFedy(key) {
val ru = us.userRepo.findByAuId(req.auId) ?: (404 - "User not found")
var card = cardService.tryLookup(req.luid)
if (card == null) {
card = cardService.registerByAccessCode(req.luid, ru)
log.info("Fedy /card/link : Linked new card ${card.id} (${card.luid}) to user ${ru.auId} (${ru.username})")
} else {
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
val cu = card.aquaUser
if (cu != null) {
if (cu.auId == req.auId) log.info("Fedy /card/link : Existing card ${card.id} (${card.luid}) already linked to user ${ru.auId} (${ru.username})")
else 400 - "Card linked to another user"
} else {
card.aquaUser = ru
cardRepo.save(card)
log.info("Fedy /card/link : Linked existing card ${card.id} (${card.luid}) to user ${ru.auId} (${ru.username})")
}
}
}
data class CardUnlinkReq(val auId: Long, val luid: Str)
@API("/card/unlink")
fun handleCardUnlink(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: CardUnlinkReq): Any = handleFedy(key) {
val card = cardService.tryLookup(req.luid)
val cu = card?.aquaUser ?: return@handleFedy SUCCESS // Nothing to do
if (cu.auId != req.auId) 400 - "Card linked to another user"
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
card.aquaUser = null
cardRepo.save(card)
log.info("Fedy /card/unlink : Unlinked card ${card.id} (${card.luid}) from user ${cu.auId} (${cu.username})")
}
fun onUserUpdated(u: AquaNetUser, isNew: Bool = false) = maybeNotifyAsync { FedyEvent(userUpdated = UserUpdatedEvent(u.fedyBasicInfo(), isNew)) }
fun onCardCreated(luid: Str, extId: Long) = maybeNotifyAsync { FedyEvent(cardCreated = CardCreatedEvent(luid, extId)) }
fun onCardLinked(luid: Str, oldExtId: Long?, ghostExtId: Long, migratedGames: List<Str>) = maybeNotifyAsync { FedyEvent(cardLinked = CardLinkedEvent(luid, oldExtId, ghostExtId, migratedGames)) }
fun onCardUnlinked(luid: Str) = maybeNotifyAsync { FedyEvent(cardUnlinked = CardUnlinkedEvent(luid)) }
fun onDataUpdated(extId: Long, game: Str, removeOldData: Bool) = maybeNotifyAsync {
val card = cardRepo.findByExtId(extId) ?: return@maybeNotifyAsync null // Card not found, nothing to do
FedyEvent(dataUpdated = DataUpdatedEvent(extId, card.isGhost, game, removeOldData))
}
private fun maybeNotifyAsync(getEvent: () -> FedyEvent?) = if (!props.enabled || suppressEvents.get()) {} else CompletableFuture.runAsync {
var event: FedyEvent? = null
try {
event = getEvent()
if (event == null) return@runAsync // Nothing to do
notify(event)
} catch (e: Exception) {
log.error("Error handling Fedy on maybeNotifyAsync($event)", e)
}
}.let {}
private fun notify(event: FedyEvent) {
val MAX_RETRY = 3
val body = event.toJson() ?: "{}"
var retry = 0
var shouldRetry = true
while (true) {
try {
val response = "${props.remote.trimEnd('/')}/notify".request()
.header("Content-Type" to "application/json")
.header(KEY_HEADER to props.key)
.post(body)
val statusCodeStr = response.statusCode().toString()
val hasError = !statusCodeStr.startsWith("2")
// Check for non-transient errors
if (hasError) {
if (!statusCodeStr.startsWith("5")) { shouldRetry = false }
throw Exception("Failed to notify Fedy event $event with body $body, status code $statusCodeStr")
}
return
} catch (e: Exception) {
retry++
if (retry >= MAX_RETRY || !shouldRetry) throw e
log.error("Error notifying Fedy event $event with body $body, retrying ($retry/$MAX_RETRY)", e)
}
}
}
// Apparently existing cards could possibly be fresh and never used in any game. Treat them as new cards.
private fun isCardFresh(c: Card): Bool {
fun <T : IUserData> checkForGame(repo: GenericUserDataRepo<T>, card: Card): Bool = repo.findByCard(card) != null
return when {
checkForGame(mai2UserDataRepo, c) -> false
checkForGame(chu3UserDataRepo, c) -> false
checkForGame(ongekiUserDataRepo, c) -> false
checkForGame(waccaUserDataRepo, c) -> false
else -> true
}
}
private infix fun <T> (() -> T).caught(onError: (FedyErr) -> T) =
try { this() }
catch (e: ApiException) { onError(FedyErr(code = e.code, message = e.message.toString())) }
companion object
{
const val KEY_HEADER = "X-Fedy-Key"
const val REQ_PART = "request"
const val PFP_PART = "profilePicture"
@Suppress("UNCHECKED_CAST")
val GAME_OPTIONS_FIELDS = listOf(
O::mai2UnlockMusic, O::mai2UnlockChara, O::mai2UnlockCharaMaxLevel, O::mai2UnlockPartners, O::mai2UnlockCollectables, O::mai2UnlockTickets
).map { it as Var<O, Any?> }.associateBy { it.name }
val log = logger()
}
}
typealias O = AquaGameOptions

View File

@@ -19,8 +19,7 @@ class FrontierProps {
@API("/api/v2/frontier") @API("/api/v2/frontier")
class Frontier( class Frontier(
val cardService: CardService, val cardService: CardService,
val props: FrontierProps, val props: FrontierProps
val fedy: Fedy
) { ) {
fun Str.checkFtk() { fun Str.checkFtk() {
if (this != props.ftk) 403 - "Invalid FTK" if (this != props.ftk) 403 - "Invalid FTK"
@@ -33,12 +32,9 @@ class Frontier(
if (accessCode.length != 20) 400 - "Invalid access code" if (accessCode.length != 20) 400 - "Invalid access code"
// if (!accessCode.startsWith("9900")) 400 - "Frontier access code must start with 9900" // if (!accessCode.startsWith("9900")) 400 - "Frontier access code must start with 9900"
if (async { cardService.cardRepo.findByLuid(accessCode) } != null) 400 - "Card already registered" if (async { cardService.cardRepo.findByLuid(accessCode) }.isPresent) 400 - "Card already registered"
val card = async { cardService.registerByAccessCode(accessCode) } val card = async { cardService.registerByAccessCode(accessCode) }
fedy.onCardCreated(accessCode, card.extId)
return mapOf( return mapOf(
"card" to card, "card" to card,
"id" to card.extId // Expose hidden ID "id" to card.extId // Expose hidden ID

View File

@@ -1,6 +1,7 @@
package icu.samnyan.aqua.net package icu.samnyan.aqua.net
import ext.JACKSON import ext.JACKSON
import ext.invoke
import ext.logger import ext.logger
import ext.parse import ext.parse
import icu.samnyan.aqua.net.db.AquaNetUserRepo import icu.samnyan.aqua.net.db.AquaNetUserRepo
@@ -23,7 +24,7 @@ class Migrations(
@PostConstruct @PostConstruct
fun migrate() { fun migrate() {
val db = props.findByPropertyKey("migrations") ?: PropertyEntry("migrations", "[]") val db = props.findByPropertyKey("migrations")() ?: PropertyEntry("migrations", "[]")
val p = JACKSON.parse<ArrayList<String>>(db.propertyValue) val p = JACKSON.parse<ArrayList<String>>(db.propertyValue)
val old = p.size val old = p.size
@@ -46,7 +47,7 @@ class Migrations(
if (c.extId > max) { if (c.extId > max) {
var new = c.extId and max var new = c.extId and max
log.info("Removing signed bit: {${c.extId} -> $new} for ${c.luid}") log.info("Removing signed bit: {${c.extId} -> $new} for ${c.luid}")
while (cardRepo.findByExtId(new) != null) { while (cardRepo.findByExtId(new).isPresent) {
log.error("> Conflicting card found for ${c.luid}: $new") log.error("> Conflicting card found for ${c.luid}: $new")
new++ new++
} }

View File

@@ -14,8 +14,7 @@ import kotlin.reflect.jvm.jvmErasure
class SettingsApi( class SettingsApi(
val us: AquaUserServices, val us: AquaUserServices,
val userRepo: AquaNetUserRepo, val userRepo: AquaNetUserRepo,
val goRepo: AquaGameOptionsRepo, val goRepo: AquaGameOptionsRepo
val fedy: Fedy
) { ) {
// Get all params with SettingField annotation // Get all params with SettingField annotation
val fields = AquaGameOptions::class.vars() val fields = AquaGameOptions::class.vars()
@@ -42,6 +41,6 @@ class SettingsApi(
} }
// Check field type // Check field type
field.setCast(options, value) field.setCast(options, value)
goRepo.save(options).also { fedy.onUserUpdated(u) } goRepo.save(options)
} }
} }

View File

@@ -2,20 +2,21 @@ package icu.samnyan.aqua.net
import ext.* import ext.*
import icu.samnyan.aqua.net.components.* import icu.samnyan.aqua.net.components.*
import icu.samnyan.aqua.net.db.AquaNetUserRepo import icu.samnyan.aqua.net.db.*
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices.Companion.SETTING_FIELDS
import icu.samnyan.aqua.net.db.EmailConfirmationRepo
import icu.samnyan.aqua.net.db.ResetPasswordRepo
import icu.samnyan.aqua.net.utils.PathProps import icu.samnyan.aqua.net.utils.PathProps
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.model.CardStatus import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.service.CardService
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime
import kotlin.io.path.writeBytes import kotlin.io.path.writeBytes
@RestController @RestController
@@ -25,12 +26,11 @@ class UserRegistrar(
val hasher: PasswordEncoder, val hasher: PasswordEncoder,
val turnstileService: TurnstileService, val turnstileService: TurnstileService,
val emailService: EmailService, val emailService: EmailService,
val fedy: Fedy,
val geoIP: GeoIP, val geoIP: GeoIP,
val jwt: JWT, val jwt: JWT,
val confirmationRepo: EmailConfirmationRepo, val confirmationRepo: EmailConfirmationRepo,
val resetPasswordRepo: ResetPasswordRepo,
val cardRepo: CardRepository, val cardRepo: CardRepository,
val cardService: CardService,
val validator: AquaUserServices, val validator: AquaUserServices,
val emailProps: EmailProperties, val emailProps: EmailProperties,
final val paths: PathProps final val paths: PathProps
@@ -66,7 +66,29 @@ class UserRegistrar(
val country = geoIP.getCountry(ip) val country = geoIP.getCountry(ip)
// Create user // Create user
val u = async { validator.create(username, email, password, country) } val u = async { AquaNetUser(
username = validator.checkUsername(username),
email = validator.checkEmail(email),
pwHash = validator.checkPwHash(password),
regTime = millis(), lastLogin = millis(), country = country,
) }
// Create a ghost card
val card = Card().apply {
extId = cardService.randExtID(cardExtIdStart, cardExtIdEnd)
luid = extId.toString()
registerTime = LocalDateTime.now()
accessTime = registerTime
aquaUser = u
isGhost = true
}
u.ghostCard = card
// Save the user
async {
userRepo.save(u)
cardRepo.save(card)
}
// Send confirmation email // Send confirmation email
emailService.sendConfirmation(u) emailService.sendConfirmation(u)
@@ -90,6 +112,8 @@ class UserRegistrar(
?: (400 - "User not found") ?: (400 - "User not found")
if (!hasher.matches(password, user.pwHash)) 400 - "Invalid password" if (!hasher.matches(password, user.pwHash)) 400 - "Invalid password"
if (user.ghostCard.status == CardStatus.MIGRATED_TO_MINATO) 400 - "Login not allowed: Card has been migrated to Minato."
// Check if email is verified // Check if email is verified
if (!user.emailConfirmed && emailProps.enable) { if (!user.emailConfirmed && emailProps.enable) {
// Check if last confirmation email was sent within a minute // Check if last confirmation email was sent within a minute
@@ -120,70 +144,6 @@ class UserRegistrar(
return mapOf("token" to token) return mapOf("token" to token)
} }
@API("/reset-password")
@Doc("Reset password with a token sent through email to the user, if it exists.", "Success message")
suspend fun resetPassword(
@RP email: Str, @RP turnstile: Str,
request: HttpServletRequest
) : Any {
// Check captcha
val ip = geoIP.getIP(request)
log.info("Net: /user/reset-password from $ip : $email")
if (!turnstileService.validate(turnstile, ip)) 400 - "Invalid captcha"
// Check if user exists, treat as email / username
val user = async { userRepo.findByEmailIgnoreCase(email) ?: userRepo.findByUsernameIgnoreCase(email) }
?: return SUCCESS // obviously dont tell them if the email exists or not
// Check if email is verified
if (!user.emailConfirmed && emailProps.enable) 400 - "Email not verified"
val resets = async { resetPasswordRepo.findByAquaNetUserAuId(user.auId) }
val lastReset = resets.maxByOrNull { it.createdAt }
if (lastReset?.createdAt?.plusSeconds(60)?.isAfter(Instant.now()) == true) {
400 - "Reset request rejected - STATE_0"
}
// Check if we have sent more than 3 confirmation emails in the last 24 hours
if (resets.count { it.createdAt.plusSeconds(60 * 60 * 24).isAfter(Instant.now()) } > 3) {
400 - "Reset request rejected - STATE_1"
}
// Send a password reset email
emailService.sendPasswordReset(user)
return SUCCESS
}
@API("/change-password")
@Doc("Change a user's password given a reset code", "Success message")
suspend fun changePassword(
@RP token: Str, @RP password: Str,
request: HttpServletRequest
) : Any {
// Find the reset token
val reset = async { resetPasswordRepo.findByToken(token) }
// Check if the token is valid
if (reset == null) 400 - "Invalid token"
// Check if the token is expired
if (reset.createdAt.plusSeconds(60 * 60 * 24).isBefore(Instant.now())) 400 - "Token expired"
// Change the password
val u = reset.aquaNetUser
async { userRepo.save(u.apply { pwHash = validator.checkPwHash(password) }) }
fedy.onUserUpdated(u)
// Remove the token from the list
resetPasswordRepo.delete(reset)
return SUCCESS
}
@API("/confirm-email") @API("/confirm-email")
@Doc("Confirm email address with a token sent through email to the user.", "Success message") @Doc("Confirm email address with a token sent through email to the user.", "Success message")
suspend fun confirmEmail(@RP token: Str): Any { suspend fun confirmEmail(@RP token: Str): Any {
@@ -198,13 +158,8 @@ class UserRegistrar(
// Check if the token is expired // Check if the token is expired
if (confirmation.createdAt.plusSeconds(60 * 60 * 24).isBefore(Instant.now())) 400 - "Token expired" if (confirmation.createdAt.plusSeconds(60 * 60 * 24).isBefore(Instant.now())) 400 - "Token expired"
// Check if the email is already confirmed
val u = confirmation.aquaNetUser
if (u.emailConfirmed) 400 - "Email already confirmed"
// Confirm the email // Confirm the email
async { userRepo.save(u.apply { emailConfirmed = true }) } async { userRepo.save(confirmation.aquaNetUser.apply { emailConfirmed = true }) }
fedy.onUserUpdated(u, isNew = true)
return SUCCESS return SUCCESS
} }
@@ -221,16 +176,16 @@ class UserRegistrar(
@API("/setting") @API("/setting")
@Doc("Validate and set a user setting field.", "Success message") @Doc("Validate and set a user setting field.", "Success message")
suspend fun setting(@RP token: Str, @RP key: Str, @RP value: Str) = jwt.auth(token) { u -> suspend fun setting(@RP token: Str, @RP key: Str, @RP value: Str) = jwt.auth(token) { u ->
// Check if the key is a settable field
val field = SETTING_FIELDS.find { it.name == key } ?: (400 - "Invalid setting")
async { async {
validator.update(u, key, value) // Set the validated field
field.setter.call(u, field.checker.call(validator, value))
// Save the user // Save the user
userRepo.save(u) userRepo.save(u)
// Clear all tokens if changing password
if (key == "pwHash") validator.clearAllSessions(u)
} }
fedy.onUserUpdated(u)
SUCCESS SUCCESS
} }
@@ -269,20 +224,6 @@ class UserRegistrar(
(portraitPath / name).writeBytes(bytes) (portraitPath / name).writeBytes(bytes)
userRepo.save(u.apply { profilePicture = name }) userRepo.save(u.apply { profilePicture = name })
} }
fedy.onUserUpdated(u)
SUCCESS
}
@API("/change-region")
@Doc("Change the region of the user.", "Success message")
suspend fun changeRegion(@RP token: Str, @RP regionId: Str) = jwt.auth(token) { u ->
// Check if the region is valid (between 1 and 47)
val r = regionId.toIntOrNull() ?: (400 - "Invalid region")
if (r !in 1..47) 400 - "Invalid region"
async {
userRepo.save(u.apply { region = r.toString() })
}
SUCCESS SUCCESS
} }

View File

@@ -3,7 +3,9 @@ package icu.samnyan.aqua.net.components
import ext.Bool import ext.Bool
import ext.Str import ext.Str
import ext.logger import ext.logger
import icu.samnyan.aqua.net.db.* import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.net.db.EmailConfirmation
import icu.samnyan.aqua.net.db.EmailConfirmationRepo
import org.simplejavamail.api.mailer.Mailer import org.simplejavamail.api.mailer.Mailer
import org.simplejavamail.email.EmailBuilder import org.simplejavamail.email.EmailBuilder
import org.simplejavamail.springsupport.SimpleJavaMailSpringSupport import org.simplejavamail.springsupport.SimpleJavaMailSpringSupport
@@ -36,13 +38,10 @@ class EmailService(
val mailer: Mailer, val mailer: Mailer,
val props: EmailProperties, val props: EmailProperties,
val confirmationRepo: EmailConfirmationRepo, val confirmationRepo: EmailConfirmationRepo,
val resetPasswordRepo: ResetPasswordRepo,
) { ) {
val log = logger() val log = logger()
val confirmTemplate: Str = this::class.java.getResource("/email/confirm.html")?.readText() val confirmTemplate: Str = this::class.java.getResource("/email/confirm.html")?.readText()
?: throw Exception("Email Confirm Template Not Found") ?: throw Exception("Email Template Not Found")
val resetTemplate: Str = this::class.java.getResource("/email/reset.html")?.readText()
?: throw Exception("Password Reset Template Not Found")
@Async @Async
@EventListener(ApplicationStartedEvent::class) @EventListener(ApplicationStartedEvent::class)
@@ -70,38 +69,15 @@ class EmailService(
confirmationRepo.save(confirmation) confirmationRepo.save(confirmation)
// Send email // Send email
log.info("Sending verification email to ${user.email}") log.info("Sending confirmation email to ${user.email}")
mailer.sendMail(EmailBuilder.startingBlank() mailer.sendMail(EmailBuilder.startingBlank()
.from(props.senderName, props.senderAddr) .from(props.senderName, props.senderAddr)
.to(user.computedName, user.email) .to(user.computedName, user.email)
.withSubject("Verify Your Email Address for AquaNet") .withSubject("Confirm Your Email Address for AquaNet")
.withHTMLText(confirmTemplate .withHTMLText(confirmTemplate
.replace("{{name}}", user.computedName) .replace("{{name}}", user.computedName)
.replace("{{url}}", "https://${props.webHost}/verify?code=$token")) .replace("{{url}}", "https://${props.webHost}?confirm-email=$token"))
.buildEmail()).thenRun { log.info("Verification email sent to ${user.email}") } .buildEmail()).thenRun { log.info("Confirmation email sent to ${user.email}") }
}
/**
* Send a reset password email to the user
*/
fun sendPasswordReset (user: AquaNetUser) {
if (!props.enable) return
// Generate token (UUID4)
val token = UUID.randomUUID().toString()
val reset = ResetPassword(token = token, aquaNetUser = user, createdAt = Date().toInstant())
resetPasswordRepo.save(reset)
// Send email
log.info("Sending reset password email to ${user.email}")
mailer.sendMail(EmailBuilder.startingBlank()
.from(props.senderName, props.senderAddr)
.to(user.computedName, user.email)
.withSubject("Reset Your Password for AquaNet")
.withHTMLText(resetTemplate
.replace("{{name}}", user.computedName)
.replace("{{url}}", "https://${props.webHost}/reset-password?code=$token"))
.buildEmail()).thenRun { log.info("Reset password email sent to ${user.email}") }
} }
fun testEmail(addr: Str, name: Str) { fun testEmail(addr: Str, name: Str) {

View File

@@ -2,17 +2,16 @@ package icu.samnyan.aqua.net.components
import ext.Str import ext.Str
import ext.minus import ext.minus
import icu.samnyan.aqua.net.db.* import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.net.db.AquaNetUserRepo
import io.jsonwebtoken.JwtParser import io.jsonwebtoken.JwtParser
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.Keys
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import jakarta.transaction.Transactional
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.Instant
import java.util.* import java.util.*
import javax.crypto.SecretKey import javax.crypto.SecretKey
@@ -25,8 +24,7 @@ class JWTProperties {
@Service @Service
class JWT( class JWT(
val props: JWTProperties, val props: JWTProperties,
val userRepo: AquaNetUserRepo, val userRepo: AquaNetUserRepo
val sessionRepo: SessionTokenRepo
) { ) {
val log = LoggerFactory.getLogger(JWT::class.java)!! val log = LoggerFactory.getLogger(JWT::class.java)!!
lateinit var key: SecretKey lateinit var key: SecretKey
@@ -57,53 +55,20 @@ class JWT(
log.info("JWT Service Enabled") log.info("JWT Service Enabled")
} }
@Transactional
fun gen(user: AquaNetUser): Str {
val activeTokens = sessionRepo.findByAquaNetUserAuId(user.auId)
.sortedByDescending { it.expiry }.drop(9) // the cap is 10, but we append a new token after the fact
if (activeTokens.isNotEmpty()) {
sessionRepo.deleteAll(activeTokens)
}
val token = SessionToken().apply {
aquaNetUser = user
}
sessionRepo.save(token)
return Jwts.builder().header() fun gen(user: AquaNetUser): Str = Jwts.builder().header()
.keyId("aqua-net") .keyId("aqua-net")
.and() .and()
.subject(token.token) .subject(user.auId.toString())
.issuedAt(Date()) .issuedAt(Date())
.signWith(key) .signWith(key)
.compact() .compact()
}
@Transactional fun parse(token: Str): AquaNetUser? = try {
fun parse(token: Str): AquaNetUser? { userRepo.findByAuId(parser.parseSignedClaims(token).payload.subject.toLong())
try {
val uuid = parser.parseSignedClaims(token).payload.subject.toString()
val token = sessionRepo.findByToken(uuid)
if (token != null) {
val toBeRemoved = sessionRepo.findByAquaNetUserAuId(token.aquaNetUser.auId)
.filter { it.expiry < Instant.now() }
if (toBeRemoved.isNotEmpty())
sessionRepo.deleteAll(toBeRemoved)
if (token.expiry < Instant.now()) {
sessionRepo.delete(token)
return null
}
sessionRepo.save(token.apply{
expiry = getTokenExpiry()
})
}
return token?.aquaNetUser
} catch (e: Exception) { } catch (e: Exception) {
log.debug("Failed to parse JWT", e) log.debug("Failed to parse JWT", e)
return null null
}
} }
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token") fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")

View File

@@ -1,33 +0,0 @@
package icu.samnyan.aqua.net.db
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.io.Serializable
import java.time.Instant
@Entity
@Table(name = "aqua_net_email_reset_password")
class ResetPassword(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
@Column(nullable = false)
var token: String = "",
// Token creation time
@Column(nullable = false)
var createdAt: Instant = Instant.now(),
// Linking to the AquaNetUser
@ManyToOne
@JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser = AquaNetUser()
) : Serializable
@Repository
interface ResetPasswordRepo : JpaRepository<ResetPassword, Long> {
fun findByToken(token: String): ResetPassword?
fun findByAquaNetUserAuId(auId: Long): List<ResetPassword>
}

View File

@@ -2,7 +2,10 @@ package icu.samnyan.aqua.net.db
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import ext.SettingField import ext.SettingField
import jakarta.persistence.* import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
@Entity @Entity
@@ -11,29 +14,21 @@ class AquaGameOptions(
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0, var id: Long = 0,
@SettingField("mai2") @Column(name = "mai2_unlock_music") @SettingField("general")
var mai2UnlockMusic: Boolean = false, var unlockMusic: Boolean = false,
@SettingField("mai2") @Column(name = "mai2_unlock_chara")
var mai2UnlockChara: Boolean = false, @SettingField("general")
@SettingField("mai2") @Column(name = "mai2_unlock_chara_max_level") var unlockChara: Boolean = false,
var mai2UnlockCharaMaxLevel: Boolean = false,
@SettingField("mai2") @Column(name = "mai2_unlock_partners") @SettingField("general")
var mai2UnlockPartners: Boolean = false, var unlockCollectables: Boolean = false,
@SettingField("mai2") @Column(name = "mai2_unlock_collectables")
var mai2UnlockCollectables: Boolean = false, @SettingField("general")
@SettingField("mai2") @Column(name = "mai2_unlock_tickets") var unlockTickets: Boolean = false,
var mai2UnlockTickets: Boolean = false,
@SettingField("wacca")
var waccaUnlockMusic: Boolean = false,
@SettingField("wacca")
var waccaUnlockPlates: Boolean = false,
@SettingField("wacca")
var waccaUnlockCollectables: Boolean = false,
@SettingField("wacca")
var waccaUnlockTickets: Boolean = false,
@SettingField("wacca") @SettingField("wacca")
var waccaInfiniteWp: Boolean = false, var waccaInfiniteWp: Boolean = false,
@SettingField("wacca") @SettingField("wacca")
var waccaAlwaysVip: Boolean = false, var waccaAlwaysVip: Boolean = false,

View File

@@ -1,33 +0,0 @@
package icu.samnyan.aqua.net.db
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.io.Serializable
import java.time.Instant
import java.util.*
fun getTokenExpiry() = Instant.now().plusSeconds(7 * 86400)
@Entity
@Table(name = "aqua_net_session")
class SessionToken(
@Id
@Column(nullable = false)
var token: String = UUID.randomUUID().toString(),
// Token creation time
@Column(nullable = false)
var expiry: Instant = getTokenExpiry(),
// Linking to the AquaNetUser
@ManyToOne
@JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser = AquaNetUser()
) : Serializable
@Repository
interface SessionTokenRepo : JpaRepository<SessionToken, String> {
fun findByToken(token: String): SessionToken?
fun findByAquaNetUserAuId(auId: Long): List<SessionToken>
}

View File

@@ -2,8 +2,6 @@ package icu.samnyan.aqua.net.db
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import ext.* import ext.*
import icu.samnyan.aqua.net.UserRegistrar.Companion.cardExtIdEnd
import icu.samnyan.aqua.net.UserRegistrar.Companion.cardExtIdStart
import icu.samnyan.aqua.net.components.JWT import icu.samnyan.aqua.net.components.JWT
import icu.samnyan.aqua.sega.allnet.AllNetProps import icu.samnyan.aqua.sega.allnet.AllNetProps
import icu.samnyan.aqua.sega.allnet.KeyChipRepo import icu.samnyan.aqua.sega.allnet.KeyChipRepo
@@ -11,13 +9,11 @@ import icu.samnyan.aqua.sega.allnet.KeychipSession
import icu.samnyan.aqua.sega.general.GameMusicPopularity import icu.samnyan.aqua.sega.general.GameMusicPopularity
import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService
import jakarta.persistence.* import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.Serializable import java.io.Serializable
import java.time.LocalDateTime
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
import kotlin.reflect.KMutableProperty import kotlin.reflect.KMutableProperty
@@ -47,10 +43,6 @@ class AquaNetUser(
@Column(length = 3) @Column(length = 3)
var country: String = "", var country: String = "",
// Region code at most 2 characters
@Column(length = 2)
var region: String = "",
// Last login time // Last login time
var lastLogin: Long = 0L, var lastLogin: Long = 0L,
@@ -106,7 +98,6 @@ interface AquaNetUserRepo : JpaRepository<AquaNetUser, Long> {
fun findByEmailIgnoreCase(email: String): AquaNetUser? fun findByEmailIgnoreCase(email: String): AquaNetUser?
fun findByUsernameIgnoreCase(username: String): AquaNetUser? fun findByUsernameIgnoreCase(username: String): AquaNetUser?
fun findByKeychip(keychip: String): AquaNetUser? fun findByKeychip(keychip: String): AquaNetUser?
fun findByGhostCardExtId(extId: Long): AquaNetUser?
} }
data class SettingField( data class SettingField(
@@ -128,9 +119,7 @@ class AquaUserServices(
val allNetProps: AllNetProps, val allNetProps: AllNetProps,
val jwt: JWT, val jwt: JWT,
val em: EntityManager, val em: EntityManager,
val pop: GameMusicPopularity, val pop: GameMusicPopularity
val cardService: CardService,
val sessionRepo: SessionTokenRepo,
) { ) {
companion object { companion object {
val SETTING_FIELDS = AquaUserServices::class.functions val SETTING_FIELDS = AquaUserServices::class.functions
@@ -142,49 +131,12 @@ class AquaUserServices(
} }
} }
fun create(username: Str, email: Str, password: Str, country: Str, emailConfirmed: Boolean = false): AquaNetUser {
// Create user
val u = AquaNetUser(
username = checkUsername(username),
email = validateEmail(email),
pwHash = checkPwHash(password),
regTime = millis(), lastLogin = millis(), country = country,
emailConfirmed = emailConfirmed
)
// Create a ghost card
val card = Card().apply {
extId = cardService.randExtID(cardExtIdStart, cardExtIdEnd)
luid = extId.toString()
registerTime = LocalDateTime.now()
accessTime = registerTime
aquaUser = u
isGhost = true
}
u.ghostCard = card
// Save the user
userRepo.save(u)
cardRepo.save(card)
return u
}
fun update(user: AquaNetUser, key: Str, value: Str) {
// Check if the key is a settable field
val field = SETTING_FIELDS.find { it.name == key } ?: (400 - "Invalid setting")
// Set the validated field
field.setter.call(user, field.checker.call(this, value))
}
fun clearAllSessions(user: AquaNetUser) = sessionRepo.deleteAll(sessionRepo.findByAquaNetUserAuId(user.auId))
suspend fun <T> byName(username: Str, callback: suspend (AquaNetUser) -> T) = suspend fun <T> byName(username: Str, callback: suspend (AquaNetUser) -> T) =
async { userRepo.findByUsernameIgnoreCase(username) }?.let { callback(it) } ?: (404 - "User not found") async { userRepo.findByUsernameIgnoreCase(username) }?.let { callback(it) } ?: (404 - "User not found")
suspend fun cardByName(username: Str) = suspend fun cardByName(username: Str) =
if (username.startsWith("user")) username.substring(4).toLongOrNull() if (username.startsWith("user")) username.substring(4).toLongOrNull()
?.let { cardRepo.findById(it)() } ?: (404 - "Card not found") ?.let { cardRepo.findById(it).getOrNull() } ?: (404 - "Card not found")
else byName(username) { it.ghostCard } else byName(username) { it.ghostCard }
suspend fun <T> cardByName(username: Str, callback: suspend (Card) -> T) = callback(cardByName(username)) suspend fun <T> cardByName(username: Str, callback: suspend (Card) -> T) = callback(cardByName(username))
@@ -216,7 +168,7 @@ class AquaUserServices(
400 - "User with username `$this` already exists" 400 - "User with username `$this` already exists"
} }
fun validateEmail(email: Str) = email.apply { fun checkEmail(email: Str) = email.apply {
// Check if email is valid // Check if email is valid
if (!isValidEmail()) 400 - "Invalid email" if (!isValidEmail()) 400 - "Invalid email"

View File

@@ -5,7 +5,7 @@ import icu.samnyan.aqua.net.BotProps
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService import icu.samnyan.aqua.sega.general.model.CardStatus
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@@ -27,12 +27,9 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
abstract val playlogRepo: GenericPlaylogRepo<*> abstract val playlogRepo: GenericPlaylogRepo<*>
abstract val userMusicRepo: GenericUserMusicRepo<*> abstract val userMusicRepo: GenericUserMusicRepo<*>
abstract val shownRanks: List<Pair<Int, String>> abstract val shownRanks: List<Pair<Int, String>>
abstract val settableFields: Map<String, (T, String) -> Unit> abstract val settableFields: Map<String, (T, String) -> Unit>
open val gettableFields: Set<String> = setOf() open val gettableFields: Set<String> = setOf()
@Autowired lateinit var cardService: CardService
@API("trend") @API("trend")
abstract suspend fun trend(@RP username: String): List<TrendOut> abstract suspend fun trend(@RP username: String): List<TrendOut>
@API("user-summary") @API("user-summary")
@@ -59,7 +56,7 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
val reqUser = token?.let { us.jwt.auth(it) }?.let { u -> val reqUser = token?.let { us.jwt.auth(it) }?.let { u ->
// Optimization: If the user is not banned, we don't need to process user information // Optimization: If the user is not banned, we don't need to process user information
if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned } && u.ghostCard.status.isNormal) null if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned } && u.ghostCard.status == CardStatus.NORMAL) null
else u else u
} }
@@ -95,7 +92,7 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
AVG(p.achievement) / 10000.0 AS acc, AVG(p.achievement) / 10000.0 AS acc,
SUM(p.is_full_combo) AS fc, SUM(p.is_full_combo) AS fc,
SUM(p.is_all_perfect) AS ap, SUM(p.is_all_perfect) AS ap,
c.ranking_banned or a.opt_out_of_leaderboard or c.status = 12 AS hide, c.ranking_banned or a.opt_out_of_leaderboard AS hide,
a.username a.username
FROM ${tableName}_user_playlog_view p FROM ${tableName}_user_playlog_view p
JOIN ${tableName}_user_data_view u ON p.user_id = u.id JOIN ${tableName}_user_data_view u ON p.user_id = u.id
@@ -121,17 +118,19 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
} }
@API("playlog") @API("playlog")
fun playlog(@RP id: Long): IGenericGamePlaylog = playlogRepo.findById(id)() ?: (404 - "Playlog not found") fun playlog(@RP id: Long): IGenericGamePlaylog = playlogRepo.findById(id).getOrNull() ?: (404 - "Playlog not found")
val userDetailFields by lazy { userDataClass.gettersMap().let { vm -> val userDetailFields by lazy { userDataClass.gettersMap().let { vm ->
(settableFields.keys.toSet() + gettableFields) (settableFields.keys.toSet() + gettableFields)
.associateWith { k -> (vm[k] ?: error("Field $k not found")) } .associateWith { k -> (vm[k] ?: error("Field $k not found")) }
} } } }
@API("user-detail") @API("user-detail")
suspend fun userDetail(@RP username: String) = us.cardByName(username) { card -> suspend fun userDetail(@RP username: String) = us.cardByName(username) { card ->
val u = userDataRepo.findByCard(card) ?: (404 - "User not found") val u = userDataRepo.findByCard(card) ?: (404 - "User not found")
userDetailFields.toList().associate { (k, f) -> k to f.invoke(u) } userDetailFields.toList().associate { (k, f) -> k to f.invoke(u) }
} }
@API("user-detail-set") @API("user-detail-set")
suspend fun userDetailSet(@RP token: String, @RP field: String, @RP value: String): Any { suspend fun userDetailSet(@RP token: String, @RP field: String, @RP value: String): Any {
val prop = settableFields[field] ?: (400 - "Invalid field $field") val prop = settableFields[field] ?: (400 - "Invalid field $field")
@@ -140,22 +139,16 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
val user = async { userDataRepo.findByCard(u.ghostCard) } ?: (404 - "User not found") val user = async { userDataRepo.findByCard(u.ghostCard) } ?: (404 - "User not found")
prop(user, value) prop(user, value)
async { userDataRepo.save(user) } async { userDataRepo.save(user) }
cardService.updateCardTimestamp(u.ghostCard, name)
SUCCESS SUCCESS
} }
} }
@API("user-option")
open suspend fun userOption(@RP token: String): Any? = 400 - "Unsupported by this game"
@API("user-option-set")
open suspend fun userOptionSet(@RP token: String, @RP field: String, @RP value: Int): Any = 400 - "Unsupported by this game"
@API("user-music-from-list") @API("user-music-from-list")
suspend fun userMusicFromList(@RP username: Str, @RB musicList: List<Int>) = us.cardByName(username) { card -> suspend fun userMusicFromList(@RP username: Str, @RB musicList: List<Int>) = us.cardByName(username) { card ->
userMusicRepo.findByUser_Card_ExtIdAndMusicIdIn(card.extId, musicList) userMusicRepo.findByUser_Card_ExtIdAndMusicIdIn(card.extId, musicList)
} }
fun genericUserSummary(card: Card, ratingComp: Map<String, String>, rival: Boolean? = null, favorites: List<Int>? = null): GenericGameSummary { fun genericUserSummary(card: Card, ratingComp: Map<String, String>, rival: Boolean? = null): GenericGameSummary {
// Summary values: total plays, player rating, server-wide ranking // Summary values: total plays, player rating, server-wide ranking
// number of each rank, max combo, number of full combo, number of all perfect // number of each rank, max combo, number of full combo, number of all perfect
val user = userDataRepo.findByCard(card) ?: (404 - "Game data not found") val user = userDataRepo.findByCard(card) ?: (404 - "Game data not found")
@@ -206,8 +199,7 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
ratingComposition = ratingComp, ratingComposition = ratingComp,
recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(100).reversed(), recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(100).reversed(),
lastPlayedHost = user.lastClientId?.let { us.userRepo.findByKeychip(it)?.username }, lastPlayedHost = user.lastClientId?.let { us.userRepo.findByKeychip(it)?.username },
rival = rival, rival = rival
favorites = favorites
) )
} }

View File

@@ -8,10 +8,10 @@ import java.time.LocalDate
const val LETTERS = "" + const val LETTERS = "" +
"" + "" +
"" ""
const val SYMBOLS = "・:;?!~/+-×÷=♂♀∀#&*@☆○◎◇□△▽♪†‡ΣαβγθφψωДё$()._ " const val SYMBOLS = "・:;?!~/+-×÷=♂♀∀#&*@☆○◎◇□△▽♪†‡ΣαβγθφψωДё$()._"
const val KANA = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん" + const val KANA = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん" +
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン" "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン"
const val SEGA_USERNAME_CHARS = LETTERS + SYMBOLS + KANA const val SEGA_USERNAME_CAHRS = LETTERS + SYMBOLS + KANA
const val WACCA_USERNAME_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + const val WACCA_USERNAME_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"abcdefghijklmnopqrstuvwxyz" + "abcdefghijklmnopqrstuvwxyz" +
"0123456789" + "0123456789" +

View File

@@ -1,28 +1,21 @@
package icu.samnyan.aqua.net.games package icu.samnyan.aqua.net.games
import ext.* import ext.*
import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.db.AquaNetUser import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.utils.AquaNetProps import icu.samnyan.aqua.net.utils.AquaNetProps
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.NoRepositoryBean import org.springframework.data.repository.NoRepositoryBean
import org.springframework.transaction.PlatformTransactionManager import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.*
import kotlin.io.path.Path import kotlin.io.path.Path
import kotlin.io.path.writeText import kotlin.io.path.writeText
import kotlin.reflect.KClass import kotlin.reflect.KClass
data class ExportOptions(
val playlogAfter: String? = null
)
// Import class with renaming // Import class with renaming
data class ImportClass<T : Any>( data class ImportClass<T : Any>(
val type: KClass<T>, val type: KClass<T>,
@@ -43,14 +36,13 @@ interface IExportClass<UserModel: IUserData> {
@NoRepositoryBean @NoRepositoryBean
interface IUserRepo<UserModel, ThisModel>: JpaRepository<ThisModel, Long> { interface IUserRepo<UserModel, ThisModel>: JpaRepository<ThisModel, Long> {
fun findByUser(user: UserModel): List<ThisModel> fun findByUser(user: UserModel): List<ThisModel>
fun findSingleByUser(user: UserModel): ThisModel? fun findSingleByUser(user: UserModel): Optional<ThisModel>
} }
/** /**
* Import controller for a game * Import controller for a game
* *
* @param game: 4-letter Game ID * @param game: 4-letter Game ID
* @param gameName: mai2/chu3/ongeki
* @param exportFields: Mapping of type names to variables in the export model * @param exportFields: Mapping of type names to variables in the export model
* (e.g. "Mai2UserCharacter" -> Mai2DataExport::userCharacterList) * (e.g. "Mai2UserCharacter" -> Mai2DataExport::userCharacterList)
* @param exportRepos: Mapping of variables to repositories that can be used to find the data * @param exportRepos: Mapping of variables to repositories that can be used to find the data
@@ -58,13 +50,10 @@ interface IUserRepo<UserModel, ThisModel>: JpaRepository<ThisModel, Long> {
*/ */
abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel: IUserData>( abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel: IUserData>(
val game: String, val game: String,
val gameName: String,
val exportClass: KClass<ExportModel>, val exportClass: KClass<ExportModel>,
val exportFields: Map<String, Var<ExportModel, Any>>, val exportFields: Map<String, Var<ExportModel, Any>>,
val exportRepos: Map<Var<ExportModel, Any>, IUserRepo<UserModel, *>>, val exportRepos: Map<Var<ExportModel, Any>, IUserRepo<UserModel, *>>,
val artemisRenames: Map<String, ImportClass<*>>, val artemisRenames: Map<String, ImportClass<*>>,
val customExporters: Map<Var<ExportModel, Any>, (UserModel, ExportOptions) -> Any?> = emptyMap(),
val customImporters: Map<Var<ExportModel, Any>, (ExportModel, UserModel) -> Unit> = emptyMap()
) { ) {
abstract fun createEmpty(): ExportModel abstract fun createEmpty(): ExportModel
abstract val userDataRepo: GenericUserDataRepo<UserModel> abstract val userDataRepo: GenericUserDataRepo<UserModel>
@@ -73,7 +62,6 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
@Autowired lateinit var netProps: AquaNetProps @Autowired lateinit var netProps: AquaNetProps
@Autowired lateinit var transManager: PlatformTransactionManager @Autowired lateinit var transManager: PlatformTransactionManager
val trans by lazy { TransactionTemplate(transManager) } val trans by lazy { TransactionTemplate(transManager) }
@Autowired lateinit var cardService: CardService
init { init {
artemisRenames.values.forEach { artemisRenames.values.forEach {
@@ -84,17 +72,12 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
val listRepos = exportRepos.filter { it.key returns List::class } val listRepos = exportRepos.filter { it.key returns List::class }
val singleRepos = exportRepos.filter { !(it.key returns List::class) } val singleRepos = exportRepos.filter { !(it.key returns List::class) }
fun export(u: AquaNetUser): ExportModel = export(u.ghostCard, ExportOptions()) fun export(u: AquaNetUser) = createEmpty().apply {
fun export(c: Card, options: ExportOptions) = createEmpty().apply {
gameId = game gameId = game
userData = userDataRepo.findByCard(c) ?: (404 - "User not found") userData = userDataRepo.findByCard(u.ghostCard) ?: (404 - "User not found")
exportRepos.forEach { (f, u) -> exportRepos.forEach { (f, u) ->
if (f returns List::class) f.set(this, u.findByUser(userData)) if (f returns List::class) f.set(this, u.findByUser(userData))
else u.findSingleByUser(userData)?.let { f.set(this, it) } else u.findSingleByUser(userData)()?.let { f.set(this, it) }
}
customExporters.forEach { (f, exporter) ->
exporter(userData, options)?.let { f.set(this, it) }
} }
} }
@@ -112,7 +95,6 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
val lists = listRepos.toList().associate { (f, r) -> r to f.get(export) as List<IUserEntity<UserModel>> }.vNotNull() val lists = listRepos.toList().associate { (f, r) -> r to f.get(export) as List<IUserEntity<UserModel>> }.vNotNull()
val singles = singleRepos.toList().associate { (f, r) -> r to f.get(export) as IUserEntity<UserModel> }.vNotNull() val singles = singleRepos.toList().associate { (f, r) -> r to f.get(export) as IUserEntity<UserModel> }.vNotNull()
var repoFieldMap = exportRepos.toList().associate { (f, r) -> r to f }
// Validate new user data // Validate new user data
// Check that all ids are 0 (this should be true since all ids are @JsonIgnore) // Check that all ids are 0 (this should be true since all ids are @JsonIgnore)
@@ -144,13 +126,7 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
// Save new data // Save new data
singles.forEach { (repo, single) -> (repo as IUserRepo<UserModel, Any>).save(single) } singles.forEach { (repo, single) -> (repo as IUserRepo<UserModel, Any>).save(single) }
lists.forEach { (repo, list) -> (repo as IUserRepo<UserModel, Any>).saveAll(list) } lists.forEach { (repo, list) -> (repo as IUserRepo<UserModel, Any>).saveAll(list) }
// Handle custom importers
customImporters.forEach { (field, importer) ->
importer(export, nu)
} }
}
cardService.updateCardTimestamp(u.ghostCard, gameName, resetCreatedAt = true)
SUCCESS SUCCESS
} }

View File

@@ -47,8 +47,7 @@ data class GenericGameSummary(
val recent: List<IGenericGamePlaylog>, val recent: List<IGenericGamePlaylog>,
val rival: Boolean?, val rival: Boolean?
val favorites: List<Int>?
) )
data class GenericRankingPlayer( data class GenericRankingPlayer(
@@ -125,7 +124,7 @@ open class BaseEntity(
@NoRepositoryBean @NoRepositoryBean
interface GenericUserDataRepo<T : IUserData> : JpaRepository<T, Long> { interface GenericUserDataRepo<T : IUserData> : JpaRepository<T, Long> {
fun findByCard(card: Card): T? fun findByCard(card: Card): T?
fun findByCard_ExtId(extId: Long): T? fun findByCard_ExtId(extId: Long): Optional<T>
@Query("select e from #{#entityName} e where e.card.rankingBanned = false") @Query("select e from #{#entityName} e where e.card.rankingBanned = false")
fun findAllNonBanned(): List<T> fun findAllNonBanned(): List<T>

View File

@@ -17,7 +17,7 @@ import kotlin.reflect.full.declaredMembers
class Chu3Import( class Chu3Import(
val repos: Chu3Repos, val repos: Chu3Repos,
) : ImportController<Chu3DataExport, Chu3UserData>( ) : ImportController<Chu3DataExport, Chu3UserData>(
"SDHD", "chu3", Chu3DataExport::class, "SDHD", Chu3DataExport::class,
exportFields = Chu3DataExport::class.vars().associateBy { exportFields = Chu3DataExport::class.vars().associateBy {
it.name.replace("List", "").lowercase() it.name.replace("List", "").lowercase()
}, },

View File

@@ -3,18 +3,10 @@ package icu.samnyan.aqua.net.games.chu3
import ext.* import ext.*
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.games.* import icu.samnyan.aqua.net.games.*
import icu.samnyan.aqua.net.utils.chu3Scores import icu.samnyan.aqua.net.utils.*
import icu.samnyan.aqua.sega.chusan.model.Chu3Repos import icu.samnyan.aqua.sega.chusan.model.*
import icu.samnyan.aqua.sega.chusan.model.Chu3UserDataRepo
import icu.samnyan.aqua.sega.chusan.model.Chu3UserMusicDetailRepo
import icu.samnyan.aqua.sega.chusan.model.Chu3UserPlaylogRepo
import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserData import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserData
import icu.samnyan.aqua.sega.chusan.model.userdata.UserGameOption
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import kotlin.jvm.optionals.getOrNull
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.memberProperties
@RestController @RestController
@API("api/v2/game/chu3") @API("api/v2/game/chu3")
@@ -33,7 +25,7 @@ class Chusan(
// Only show > AAA rank // Only show > AAA rank
override val shownRanks = chu3Scores.filter { it.first >= 95 * 10000 } override val shownRanks = chu3Scores.filter { it.first >= 95 * 10000 }
override val settableFields: Map<String, (Chu3UserData, String) -> Unit> by lazy { mapOf( override val settableFields: Map<String, (Chu3UserData, String) -> Unit> by lazy { mapOf(
"userName" to usernameCheck(SEGA_USERNAME_CHARS), "userName" to usernameCheck(SEGA_USERNAME_CAHRS),
"nameplateId" to { u, v -> u.nameplateId = v.int }, "nameplateId" to { u, v -> u.nameplateId = v.int },
"frameId" to { u, v -> u.frameId = v.int }, "frameId" to { u, v -> u.frameId = v.int },
"trophyId" to { u, v -> u.trophyId = v.int }, "trophyId" to { u, v -> u.trophyId = v.int },
@@ -41,7 +33,6 @@ class Chusan(
"trophyIdSub2" to { u, v -> u.trophyIdSub2 = v.int }, "trophyIdSub2" to { u, v -> u.trophyIdSub2 = v.int },
"mapIconId" to { u, v -> u.mapIconId = v.int }, "mapIconId" to { u, v -> u.mapIconId = v.int },
"voiceId" to { u, v -> u.voiceId = v.int }, "voiceId" to { u, v -> u.voiceId = v.int },
"characterId" to { u, v -> u.characterId = v.int },
"avatarWear" to { u, v -> u.avatarWear = v.int }, "avatarWear" to { u, v -> u.avatarWear = v.int },
"avatarHead" to { u, v -> u.avatarHead = v.int }, "avatarHead" to { u, v -> u.avatarHead = v.int },
"avatarFace" to { u, v -> u.avatarFace = v.int }, "avatarFace" to { u, v -> u.avatarFace = v.int },
@@ -53,7 +44,7 @@ class Chusan(
"lastRomVersion" to { u, v -> u.lastRomVersion = v }, "lastRomVersion" to { u, v -> u.lastRomVersion = v },
"lastDataVersion" to { u, v -> u.lastDataVersion = v }, "lastDataVersion" to { u, v -> u.lastDataVersion = v },
) } ) }
override val gettableFields: Set<String> = setOf("level", "playerRating") override val gettableFields: Set<String> = setOf("level", "playerRating", "characterId")
override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card -> override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card ->
// Summary values: total plays, player rating, server-wide ranking // Summary values: total plays, player rating, server-wide ranking
@@ -69,9 +60,7 @@ class Chusan(
"new" to (extra["rating_new_list"] ?: ""), "new" to (extra["rating_new_list"] ?: ""),
) )
val misc = rp.userMisc.findByUser_Card_ExtId(card.extId).firstOrNull() genericUserSummary(card, ratingComposition)
genericUserSummary(card, ratingComposition, null, misc?.favMusic)
} }
/** /**
@@ -103,23 +92,6 @@ class Chusan(
) )
} }
@API("user-option")
override suspend fun userOption(@RP token: String): Any? = us.jwt.auth(token) { u ->
rp.userGameOption.findByUser_Card_ExtId(u.ghostCard.extId).getOrNull(0)
}
@API("user-option-set")
override suspend fun userOptionSet(@RP token: String, @RP field: String, @RP value: Int): Any = us.jwt.auth(token) { u ->
val gameOptions = rp.userGameOption.findSingleByUser_Card_ExtId(u.ghostCard.extId)
val property = UserGameOption::class.memberProperties.filterIsInstance<KMutableProperty1<Any, Any?>>().find{ it.name == field }
if (property != null && gameOptions != null) {
property.setter.call(gameOptions, value)
rp.userGameOption.save(gameOptions)
200 - "Success"
} else
400 - "Invalid parameters"
}
// UserBox related APIs // UserBox related APIs
@API("user-box") @API("user-box")
fun userBox(@RP token: String) = us.jwt.auth(token) { fun userBox(@RP token: String) = us.jwt.auth(token) {

View File

@@ -3,38 +3,30 @@ package icu.samnyan.aqua.net.games.mai2
import ext.API import ext.API
import ext.returns import ext.returns
import ext.vars import ext.vars
import icu.samnyan.aqua.net.games.ExportOptions
import icu.samnyan.aqua.net.games.IExportClass import icu.samnyan.aqua.net.games.IExportClass
import icu.samnyan.aqua.net.games.ImportClass import icu.samnyan.aqua.net.games.ImportClass
import icu.samnyan.aqua.net.games.ImportController import icu.samnyan.aqua.net.games.ImportController
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserLinked import icu.samnyan.aqua.sega.maimai2.model.Mai2UserLinked
import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UserFavoriteItem
import icu.samnyan.aqua.sega.maimai2.model.userdata.* import icu.samnyan.aqua.sega.maimai2.model.userdata.*
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import kotlin.reflect.full.declaredMembers import kotlin.reflect.full.declaredMembers
@Suppress("UNCHECKED_CAST")
@RestController @RestController
@API("api/v2/game/mai2") @API("api/v2/game/mai2")
class Mai2Import( class Mai2Import(
val repos: Mai2Repos, val repos: Mai2Repos,
) : ImportController<Maimai2DataExport, Mai2UserDetail>( ) : ImportController<Maimai2DataExport, Mai2UserDetail>(
"SDEZ", "mai2", Maimai2DataExport::class, "SDEZ", Maimai2DataExport::class,
exportFields = Maimai2DataExport::class.vars().associateBy { exportFields = Maimai2DataExport::class.vars().associateBy {
it.name.replace("List", "").lowercase() it.name.replace("List", "").lowercase()
}, },
exportRepos = Maimai2DataExport::class.vars() exportRepos = Maimai2DataExport::class.vars()
.filter { f -> f.name !in setOf("gameId", "userData", "userPlaylogList", "userFavoriteMusicList") } .filter { f -> f.name !in setOf("gameId", "userData") }
.associateWith { field -> .associateWith { Mai2Repos::class.declaredMembers
val repoName = when (field.name) {
"userKaleidxScopeList" -> "userKaleidx"
else -> field.name.replace("List", "")
}
Mai2Repos::class.declaredMembers
.filter { f -> f returns Mai2UserLinked::class } .filter { f -> f returns Mai2UserLinked::class }
.firstOrNull { f -> f.name == repoName } .firstOrNull { f -> f.name == it.name || f.name == it.name.replace("List", "") }
?.call(repos) as Mai2UserLinked<*>? ?: error("No matching field found for ${field.name}") ?.call(repos) as Mai2UserLinked<*>? ?: error("No matching field found for ${it.name}")
}, },
artemisRenames = mapOf( artemisRenames = mapOf(
"mai2_item_character" to ImportClass(Mai2UserCharacter::class), "mai2_item_character" to ImportClass(Mai2UserCharacter::class),
@@ -52,71 +44,34 @@ class Mai2Import(
"mai2_profile_option" to ImportClass(Mai2UserOption::class, mapOf("version" to null)), "mai2_profile_option" to ImportClass(Mai2UserOption::class, mapOf("version" to null)),
"mai2_score_best" to ImportClass(Mai2UserMusicDetail::class), "mai2_score_best" to ImportClass(Mai2UserMusicDetail::class),
"mai2_score_course" to ImportClass(Mai2UserCourse::class), "mai2_score_course" to ImportClass(Mai2UserCourse::class),
), )
customExporters = mapOf(
Maimai2DataExport::userPlaylogList to { user: Mai2UserDetail, options: ExportOptions ->
if (options.playlogAfter != null) {
repos.userPlaylog.findByUserAndUserPlayDateAfter(user, options.playlogAfter)
} else {
repos.userPlaylog.findByUser(user)
}
},
Maimai2DataExport::userFavoriteMusicList to { user: Mai2UserDetail, _: ExportOptions ->
repos.userGeneralData.findByUserAndPropertyKey(user, "favorite_music")
?.propertyValue
?.takeIf { it.isNotEmpty() }
?.split(",")
?.mapIndexed { index, id -> Mai2UserFavoriteItem().apply { orderId = index; this.id = id.toInt() } }
?: emptyList()
}
) as Map<kotlin.reflect.KMutableProperty1<Maimai2DataExport, Any>, (Mai2UserDetail, ExportOptions) -> Any?>,
customImporters = mapOf(
Maimai2DataExport::userPlaylogList to { export: Maimai2DataExport, user: Mai2UserDetail ->
repos.userPlaylog.saveAll(export.userPlaylogList.map { it.apply { it.user = user } })
},
Maimai2DataExport::userFavoriteMusicList to { export: Maimai2DataExport, user: Mai2UserDetail ->
val favoriteMusicList = export.userFavoriteMusicList
if (favoriteMusicList.isNotEmpty()) {
val key = "favorite_music"
// This field always imports as incremental, since the userGeneralData field (for backwards compatibility) is processed before this
val data = repos.userGeneralData.findByUserAndPropertyKey(user, key)
?: Mai2UserGeneralData().apply { this.user = user; propertyKey = key }
repos.userGeneralData.save(data.apply {
propertyValue = favoriteMusicList.sortedBy { it.orderId }.map { it.id }.joinToString(",")
})
}
}
) as Map<kotlin.reflect.KMutableProperty1<Maimai2DataExport, Any>, (Maimai2DataExport, Mai2UserDetail) -> Unit>
) { ) {
override fun createEmpty() = Maimai2DataExport() override fun createEmpty() = Maimai2DataExport()
override val userDataRepo = repos.userData override val userDataRepo = repos.userData
} }
data class Maimai2DataExport( data class Maimai2DataExport(
override var userData: Mai2UserDetail = Mai2UserDetail(), override var userData: Mai2UserDetail,
var userExtend: Mai2UserExtend = Mai2UserExtend(), var userExtend: Mai2UserExtend,
var userOption: Mai2UserOption = Mai2UserOption(), var userOption: Mai2UserOption,
var userUdemae: Mai2UserUdemae = Mai2UserUdemae(), var userUdemae: Mai2UserUdemae,
var mapEncountNpcList: List<Mai2MapEncountNpc> = mutableListOf(), var mapEncountNpcList: List<Mai2MapEncountNpc>,
var userActList: List<Mai2UserAct> = mutableListOf(), var userActList: List<Mai2UserAct>,
var userCharacterList: List<Mai2UserCharacter> = mutableListOf(), var userCharacterList: List<Mai2UserCharacter>,
var userChargeList: List<Mai2UserCharge> = mutableListOf(), var userChargeList: List<Mai2UserCharge>,
var userCourseList: List<Mai2UserCourse> = mutableListOf(), var userCourseList: List<Mai2UserCourse>,
var userFavoriteList: List<Mai2UserFavorite> = mutableListOf(), var userFavoriteList: List<Mai2UserFavorite>,
var userFriendSeasonRankingList: List<Mai2UserFriendSeasonRanking> = mutableListOf(), var userFriendSeasonRankingList: List<Mai2UserFriendSeasonRanking>,
var userGeneralDataList: List<Mai2UserGeneralData> = mutableListOf(), var userGeneralDataList: List<Mai2UserGeneralData>,
var userItemList: List<Mai2UserItem> = mutableListOf(), var userItemList: List<Mai2UserItem>,
var userLoginBonusList: List<Mai2UserLoginBonus> = mutableListOf(), var userLoginBonusList: List<Mai2UserLoginBonus>,
var userMapList: List<Mai2UserMap> = mutableListOf(), var userMapList: List<Mai2UserMap>,
var userMusicDetailList: List<Mai2UserMusicDetail> = mutableListOf(), var userMusicDetailList: List<Mai2UserMusicDetail>,
var userIntimateList: List<Mai2UserIntimate> = mutableListOf(), var userPlaylogList: List<Mai2UserPlaylog>,
var userFavoriteMusicList: List<Mai2UserFavoriteItem> = mutableListOf(),
var userKaleidxScopeList: List<Mai2UserKaleidx> = mutableListOf(),
var userPlaylogList: List<Mai2UserPlaylog> = mutableListOf(),
// Not supported yet:
// var userWeeklyData
// var userMissionDataList
// var userShopStockList
// var userTradeItemList
override var gameId: String = "SDEZ", override var gameId: String = "SDEZ",
): IExportClass<Mai2UserDetail> ): IExportClass<Mai2UserDetail> {
constructor() : this(Mai2UserDetail(), Mai2UserExtend(), Mai2UserOption(), Mai2UserUdemae(),
mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(),
mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(),
mutableListOf())
}

View File

@@ -1,12 +1,8 @@
package icu.samnyan.aqua.net.games.mai2 package icu.samnyan.aqua.net.games.mai2
import ext.API import ext.*
import ext.RB
import ext.RP
import ext.minus
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserMusicDetail import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserMusicDetail
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
@@ -17,28 +13,32 @@ import org.springframework.web.bind.annotation.RestController
class Mai2MusicDetailImport( class Mai2MusicDetailImport(
val us: AquaUserServices, val us: AquaUserServices,
val repos: Mai2Repos, val repos: Mai2Repos,
val cardService: CardService,
) { ) {
@PostMapping("import-music-detail") @PostMapping("import-music-detail")
suspend fun importMusicDetail(@RP token: String, @RB data: List<Mai2UserMusicDetail>) = us.jwt.auth(token) { u -> suspend fun importMusicDetail(@RP token: String, @RB data: List<Mai2UserMusicDetail>) = us.jwt.auth(token) { u ->
us.cardByName(u.username) { card -> us.cardByName(u.username) { card ->
val user = repos.userData.findByCardExtId(card.extId) ?: (404 - "User not found") val user = repos.userData.findByCardExtId(card.extId).orElse(null) ?: (404 - "User not found")
data.forEach { newMusic -> data.forEach { newMusic ->
newMusic.user = user val musicRec = repos.userMusicDetail.findByUserAndMusicIdAndLevel(user, newMusic.musicId, newMusic.level)
repos.userMusicDetail.findByUserAndMusicIdAndLevel(user, newMusic.musicId, newMusic.level)?.let { m -> if (musicRec.isPresent) {
val music = musicRec.get()
newMusic.apply { newMusic.apply {
id = m.id id = music.id
achievement = achievement.coerceAtLeast(m.achievement) this.user = user
scoreRank = scoreRank.coerceAtLeast(m.scoreRank) achievement = achievement.coerceAtLeast(music.achievement)
comboStatus = comboStatus.coerceAtLeast(m.comboStatus) scoreRank = scoreRank.coerceAtLeast(music.scoreRank)
syncStatus = syncStatus.coerceAtLeast(m.syncStatus) comboStatus = comboStatus.coerceAtLeast(music.comboStatus)
deluxscoreMax = deluxscoreMax.coerceAtLeast(m.deluxscoreMax) syncStatus = syncStatus.coerceAtLeast(music.syncStatus)
playCount = playCount.coerceAtLeast(m.playCount) deluxscoreMax = deluxscoreMax.coerceAtLeast(music.deluxscoreMax)
playCount = playCount.coerceAtLeast(music.playCount)
}
} else {
newMusic.apply {
this.user = user
} }
} }
} }
repos.userMusicDetail.saveAll(data) repos.userMusicDetail.saveAll(data)
cardService.updateCardTimestamp(card, "mai2")
SUCCESS SUCCESS
} }
} }

View File

@@ -3,23 +3,14 @@ package icu.samnyan.aqua.net.games.mai2
import ext.* import ext.*
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.games.* import icu.samnyan.aqua.net.games.*
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.*
import icu.samnyan.aqua.net.utils.mai2Scores
import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPhotoHandler import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPhotoHandler
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos import icu.samnyan.aqua.sega.maimai2.model.*
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo import icu.samnyan.aqua.sega.maimai2.model.userdata.*
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserMusicDetailRepo
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPlaylogRepo
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserDetail
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserGeneralData
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserLoginBonus
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserOption
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import kotlin.jvm.optionals.getOrNull import java.util.*
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.memberProperties
@RestController @RestController
@API("api/v2/game/mai2") @API("api/v2/game/mai2")
@@ -38,7 +29,7 @@ class Maimai2(
// Only show > S rank // Only show > S rank
override val shownRanks = mai2Scores.filter { it.first >= 97 * 10000 } override val shownRanks = mai2Scores.filter { it.first >= 97 * 10000 }
override val settableFields: Map<String, (Mai2UserDetail, String) -> Unit> by lazy { mapOf( override val settableFields: Map<String, (Mai2UserDetail, String) -> Unit> by lazy { mapOf(
"userName" to usernameCheck(SEGA_USERNAME_CHARS), "userName" to usernameCheck(SEGA_USERNAME_CAHRS),
"iconId" to { u, v -> u.iconId = v.int() }, "iconId" to { u, v -> u.iconId = v.int() },
"plateId" to { u, v -> u.plateId = v.int() }, "plateId" to { u, v -> u.plateId = v.int() },
"titleId" to { u, v -> u.titleId = v.int() }, "titleId" to { u, v -> u.titleId = v.int() },
@@ -67,15 +58,15 @@ class Maimai2(
us.jwt.auth(t) { u -> us.jwt.auth(t) { u ->
if (u.username == username) return@auth null if (u.username == username) return@auth null
us.cardByName(u.username) { myCard -> us.cardByName(u.username) { myCard ->
val user = repos.userData.findByCardExtId(card.extId) ?: (404 - "User not found") val user = repos.userData.findByCardExtId(card.extId).orElse(null) ?: (404 - "User not found")
val myRival = (repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival")?.propertyValue?.split(',') ?: emptyList()) val myRival = repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival")
.filter { it.isNotEmpty() }.map { it.long() } .map { it.propertyValue.split(',') }.orElse(emptyList()).filter { it.isNotEmpty() }.map { it.long() }
myRival.contains(user.id) myRival.contains(user.id)
} }
} }
} }
genericUserSummary(card, ratingComposition, isMyRival, extra["favorite_music"]?.split(",")?.mapNotNull{it -> it.toIntOrNull()}) genericUserSummary(card, ratingComposition, isMyRival)
} }
@API("user-rating") @API("user-rating")
@@ -119,7 +110,6 @@ class Maimai2(
val user = userDataRepo.findByCard(card) ?: (404 - "User not found") val user = userDataRepo.findByCard(card) ?: (404 - "User not found")
user.userName = newNameFull user.userName = newNameFull
userDataRepo.save(user) userDataRepo.save(user)
cardService.updateCardTimestamp(card, "mai2")
} }
mapOf("newName" to newNameFull) mapOf("newName" to newNameFull)
} }
@@ -142,36 +132,17 @@ class Maimai2(
if (loginBonus.none { it.bonusId == bonusId }) { if (loginBonus.none { it.bonusId == bonusId }) {
// create one // create one
val newBonus = Mai2UserLoginBonus().apply { val newBonus = Mai2UserLoginBonus().apply {
user = repos.userData.findByCardExtId(card.extId) ?: (404 - "User not found") user = repos.userData.findByCardExtId(card.extId).orElse(null) ?: (404 - "User not found")
this.bonusId = bonusId this.bonusId = bonusId
isCurrent = true isCurrent = true
} }
loginBonus.add(newBonus) loginBonus.add(newBonus)
} }
repos.userLoginBonus.saveAll(loginBonus) repos.userLoginBonus.saveAll(loginBonus)
cardService.updateCardTimestamp(card, "mai2")
} }
SUCCESS SUCCESS
} }
@API("user-option")
override suspend fun userOption(@RP token: String) = us.jwt.auth(token) { u ->
repos.userOption.findByUser_Card_ExtId(u.ghostCard.extId).getOrNull(0)
}
@API("user-option-set")
override suspend fun userOptionSet(@RP token: String, @RP field: String, @RP value: Int): Any = us.jwt.auth(token) { u ->
val gameOptions = repos.userOption.findSingleByUser_Card_ExtId(u.ghostCard.extId)
val property = Mai2UserOption::class.memberProperties.filterIsInstance<KMutableProperty1<Any, Any?>>().find{ it.name == field }
if (property != null && gameOptions != null) {
property.setter.call(gameOptions, value)
repos.userOption.save(gameOptions)
200 - "Success"
} else
400 - "Invalid parameters"
}
@API("owned-items") @API("owned-items")
suspend fun ownedItems(@RP token: String) = us.jwt.auth(token) { u -> suspend fun ownedItems(@RP token: String) = us.jwt.auth(token) { u ->
us.cardByName(u.username) { card -> us.cardByName(u.username) { card ->
@@ -183,10 +154,10 @@ class Maimai2(
suspend fun setRival(@RP token: String, @RP rivalUserName: String, @RP isAdd: Boolean) = us.jwt.auth(token) { u -> suspend fun setRival(@RP token: String, @RP rivalUserName: String, @RP isAdd: Boolean) = us.jwt.auth(token) { u ->
us.cardByName(u.username) { myCard -> us.cardByName(u.username) { myCard ->
val rivalCard = us.cardByName(rivalUserName) { it } val rivalCard = us.cardByName(rivalUserName) { it }
val rivalUser = repos.userData.findByCardExtId(rivalCard.extId) ?: (404 - "User not found") val rivalUser = repos.userData.findByCardExtId(rivalCard.extId).orElse(null) ?: (404 - "User not found")
val myRival = repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival") val myRival = repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival").orElse(null)
?: Mai2UserGeneralData().apply { ?: Mai2UserGeneralData().apply {
user = repos.userData.findByCardExtId(myCard.extId) ?: (404 - "User not found") user = repos.userData.findByCardExtId(myCard.extId).orElse(null) ?: (404 - "User not found")
propertyKey = "favorite_rival" propertyKey = "favorite_rival"
} }
val myRivalList = myRival.propertyValue.split(',').filter { it.isNotEmpty() }.mut val myRivalList = myRival.propertyValue.split(',').filter { it.isNotEmpty() }.mut
@@ -201,7 +172,6 @@ class Maimai2(
myRival.propertyValue = myRivalList.joinToString(",") myRival.propertyValue = myRivalList.joinToString(",")
repos.userGeneralData.save(myRival) repos.userGeneralData.save(myRival)
cardService.updateCardTimestamp(myCard, "mai2")
} }
SUCCESS SUCCESS
} }

View File

@@ -1,22 +1,15 @@
package icu.samnyan.aqua.net.games.ongeki package icu.samnyan.aqua.net.games.ongeki
import ext.API import ext.API
import ext.RP
import ext.minus
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.games.* import icu.samnyan.aqua.net.games.*
import icu.samnyan.aqua.net.utils.ongekiScores import icu.samnyan.aqua.net.utils.*
import icu.samnyan.aqua.sega.ongeki.OgkUserDataRepo import icu.samnyan.aqua.sega.ongeki.OgkUserDataRepo
import icu.samnyan.aqua.sega.ongeki.OgkUserGeneralDataRepo import icu.samnyan.aqua.sega.ongeki.OgkUserGeneralDataRepo
import icu.samnyan.aqua.sega.ongeki.OgkUserMusicDetailRepo import icu.samnyan.aqua.sega.ongeki.OgkUserMusicDetailRepo
import icu.samnyan.aqua.sega.ongeki.OgkUserOptionRepo
import icu.samnyan.aqua.sega.ongeki.OgkUserPlaylogRepo import icu.samnyan.aqua.sega.ongeki.OgkUserPlaylogRepo
import icu.samnyan.aqua.sega.ongeki.model.UserData import icu.samnyan.aqua.sega.ongeki.model.UserData
import icu.samnyan.aqua.sega.ongeki.model.UserOption
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import kotlin.jvm.optionals.getOrNull
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.memberProperties
@RestController @RestController
@API("api/v2/game/ongeki") @API("api/v2/game/ongeki")
@@ -25,8 +18,7 @@ class Ongeki(
override val playlogRepo: OgkUserPlaylogRepo, override val playlogRepo: OgkUserPlaylogRepo,
override val userDataRepo: OgkUserDataRepo, override val userDataRepo: OgkUserDataRepo,
override val userMusicRepo: OgkUserMusicDetailRepo, override val userMusicRepo: OgkUserMusicDetailRepo,
val userGeneralDataRepository: OgkUserGeneralDataRepo, val userGeneralDataRepository: OgkUserGeneralDataRepo
val userOptionRepo: OgkUserOptionRepo
): GameApiController<UserData>("ongeki", UserData::class) { ): GameApiController<UserData>("ongeki", UserData::class) {
override suspend fun trend(username: String) = us.cardByName(username) { card -> override suspend fun trend(username: String) = us.cardByName(username) { card ->
findTrend(playlogRepo.findByUser_Card_ExtId(card.extId) findTrend(playlogRepo.findByUser_Card_ExtId(card.extId)
@@ -35,7 +27,7 @@ class Ongeki(
override val shownRanks = ongekiScores.filter { it.first >= 950000 } override val shownRanks = ongekiScores.filter { it.first >= 950000 }
override val settableFields: Map<String, (UserData, String) -> Unit> by lazy { mapOf( override val settableFields: Map<String, (UserData, String) -> Unit> by lazy { mapOf(
"userName" to usernameCheck(SEGA_USERNAME_CHARS), "userName" to usernameCheck(SEGA_USERNAME_CAHRS),
"lastRomVersion" to { u, v -> u.lastRomVersion = v }, "lastRomVersion" to { u, v -> u.lastRomVersion = v },
"lastDataVersion" to { u, v -> u.lastDataVersion = v }, "lastDataVersion" to { u, v -> u.lastDataVersion = v },
@@ -53,21 +45,4 @@ class Ongeki(
genericUserSummary(card, ratingComposition) genericUserSummary(card, ratingComposition)
} }
@API("user-option")
override suspend fun userOption(@RP token: String) = us.jwt.auth(token) { u ->
userOptionRepo.findByUser_Card_ExtId(u.ghostCard.extId).getOrNull(0)
}
@API("user-option-set")
override suspend fun userOptionSet(@RP token: String, @RP field: String, @RP value: Int): Any = us.jwt.auth(token) { u ->
val gameOptions = userOptionRepo.findSingleByUser_Card_ExtId(u.ghostCard.extId)
val property = UserOption::class.memberProperties.filterIsInstance<KMutableProperty1<Any, Any?>>().find{ it.name == field }
if (property != null && gameOptions != null) {
property.setter.call(gameOptions, value)
userOptionRepo.save(gameOptions)
200 - "Success"
} else
400 - "Invalid parameters"
}
} }

View File

@@ -1,9 +1,6 @@
package icu.samnyan.aqua.net.games.wacca package icu.samnyan.aqua.net.games.wacca
import ext.API import ext.*
import ext.RP
import ext.isoDate
import ext.utc
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.games.* import icu.samnyan.aqua.net.games.*
import icu.samnyan.aqua.net.utils.waccaScores import icu.samnyan.aqua.net.utils.waccaScores
@@ -34,10 +31,7 @@ class Wacca(
override suspend fun userSummary(@RP username: String, @RP token: String?) = us.cardByName(username) { card -> override suspend fun userSummary(@RP username: String, @RP token: String?) = us.cardByName(username) { card ->
// TODO: Rating composition // TODO: Rating composition
genericUserSummary(card, mapOf())
val data = userDataRepo.findByCard_ExtId(card.extId)
genericUserSummary(card, mapOf(), null, data?.favoriteSongs)
} }
override val shownRanks: List<Pair<Int, String>> = waccaScores.filter { it.first > 85 * 10000 } override val shownRanks: List<Pair<Int, String>> = waccaScores.filter { it.first > 85 * 10000 }

View File

@@ -3,10 +3,10 @@ package icu.samnyan.aqua.net.transfer
import ext.header import ext.header
import ext.post import ext.post
import ext.request import ext.request
import java.net.URI
import icu.samnyan.aqua.sega.aimedb.AimeDbClient import icu.samnyan.aqua.sega.aimedb.AimeDbClient
import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder
import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder.decodeAllNetResp import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder.decodeAllNetResp
import java.net.URI
val keychipPattern = Regex("([A-Z\\d]{4}-[A-Z\\d]{11}|[A-Z\\d]{11})") val keychipPattern = Regex("([A-Z\\d]{4}-[A-Z\\d]{11}|[A-Z\\d]{11})")

View File

@@ -11,9 +11,9 @@ import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserItem
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserMusicDetail import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserMusicDetail
import icu.samnyan.aqua.sega.ongeki.model.OngekiUpsertUserAll import icu.samnyan.aqua.sega.ongeki.model.OngekiUpsertUserAll
import icu.samnyan.aqua.sega.ongeki.model.UserItem import icu.samnyan.aqua.sega.ongeki.model.UserItem
import icu.samnyan.aqua.sega.util.BasicMapper import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import icu.samnyan.aqua.sega.util.IMapper import icu.samnyan.aqua.sega.util.jackson.IMapper
import icu.samnyan.aqua.sega.util.StringMapper import icu.samnyan.aqua.sega.util.jackson.StringMapper
abstract class DataBroker( abstract class DataBroker(

View File

@@ -1,11 +1,11 @@
package icu.samnyan.aqua.sega.aimedb package icu.samnyan.aqua.sega.aimedb
import ext.logger import ext.*
import ext.toHex import icu.samnyan.aqua.net.BotProps
import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.sega.allnet.AllNetProps import icu.samnyan.aqua.sega.allnet.AllNetProps
import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.service.CardService import icu.samnyan.aqua.sega.general.service.CardService
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil import io.netty.buffer.ByteBufUtil
@@ -26,7 +26,6 @@ class AimeDB(
val cardService: CardService, val cardService: CardService,
val us: AquaUserServices, val us: AquaUserServices,
val allNetProps: AllNetProps, val allNetProps: AllNetProps,
val fedy: Fedy,
): ChannelInboundHandlerAdapter() { ): ChannelInboundHandlerAdapter() {
val logger = logger() val logger = logger()
@@ -69,9 +68,9 @@ class AimeDB(
*/ */
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg !is Map<*, *>) return if (msg !is Map<*, *>) return
try {
val type = msg["type"] as Int val type = msg["type"] as Int
val data = msg["data"] as ByteBuf val data = msg["data"] as ByteBuf
try {
val base = data.decodeHeader() val base = data.decodeHeader()
val handler = handlers[type] ?: return logger.error("AimeDB: Unknown request type 0x${type.toString(16)}") val handler = handlers[type] ?: return logger.error("AimeDB: Unknown request type 0x${type.toString(16)}")
@@ -90,7 +89,6 @@ class AimeDB(
handler.fn(data)?.let { ctx.write(it) } handler.fn(data)?.let { ctx.write(it) }
} finally { } finally {
data.release()
ctx.flush() ctx.flush()
ctx.close() ctx.close()
} }
@@ -126,7 +124,7 @@ class AimeDB(
} }
} }
fun getCard(accessCode: String) = us.cardRepo.findByLuid(accessCode)?.maybeGhost()?.let { card -> fun getCard(accessCode: String) = us.cardRepo.findByLuid(accessCode)()?.maybeGhost()?.let { card ->
// Update card access time and return the extId // Update card access time and return the extId
us.cardRepo.save(card.apply { accessTime = LocalDateTime.now() }).extId us.cardRepo.save(card.apply { accessTime = LocalDateTime.now() }).extId
} ?: -1 } ?: -1
@@ -197,13 +195,11 @@ class AimeDB(
var status = 0 var status = 0
var aimeId = 0L var aimeId = 0L
if (us.cardRepo.findByLuid(luid) == null) { if (us.cardRepo.findByLuid(luid).isEmpty) {
val card: Card = cardService.registerByAccessCode(luid) val card: Card = cardService.registerByAccessCode(luid)
status = 1 status = 1
aimeId = card.extId aimeId = card.extId
fedy.onCardCreated(luid, card.extId)
} }
else logger.warn("> Duplicated Aime Card Register detected, access code: $luid") else logger.warn("> Duplicated Aime Card Register detected, access code: $luid")

View File

@@ -1,30 +1,12 @@
package icu.samnyan.aqua.sega.aimedb package icu.samnyan.aqua.sega.aimedb
import icu.samnyan.aqua.sega.util.ByteBufUtil
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled.copiedBuffer import io.netty.buffer.Unpooled.copiedBuffer
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
fun ByteBuf.toBytes(): ByteArray {
val readerPos = readerIndex()
resetReaderIndex()
val result = ByteArray(readableBytes())
readBytes(result)
readerIndex(readerPos)
return result
}
fun ByteBuf.toAllBytes(): ByteArray {
val readerPos = readerIndex()
resetReaderIndex()
writerIndex(capacity())
val result = ByteArray(capacity())
readBytes(result)
readerIndex(readerPos)
return result
}
/** /**
* @author samnyan (privateamusement@protonmail.com) * @author samnyan (privateamusement@protonmail.com)
*/ */
@@ -33,6 +15,7 @@ object AimeDbEncryption {
val enc = Cipher.getInstance("AES/ECB/NoPadding").apply { init(Cipher.ENCRYPT_MODE, KEY) } val enc = Cipher.getInstance("AES/ECB/NoPadding").apply { init(Cipher.ENCRYPT_MODE, KEY) }
val dec = Cipher.getInstance("AES/ECB/NoPadding").apply { init(Cipher.DECRYPT_MODE, KEY) } val dec = Cipher.getInstance("AES/ECB/NoPadding").apply { init(Cipher.DECRYPT_MODE, KEY) }
fun decrypt(src: ByteBuf) = copiedBuffer(dec.doFinal(src.toBytes())) fun decrypt(src: ByteBuf) = copiedBuffer(dec.doFinal(ByteBufUtil.toBytes(src)))
fun encrypt(src: ByteBuf) = copiedBuffer(enc.doFinal(src.toAllBytes()))
fun encrypt(src: ByteBuf) = copiedBuffer(enc.doFinal(ByteBufUtil.toAllBytes(src)))
} }

View File

@@ -9,6 +9,8 @@ import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.handler.logging.LogLevel import io.netty.handler.logging.LogLevel
import io.netty.handler.logging.LoggingHandler import io.netty.handler.logging.LoggingHandler
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.stereotype.Component import org.springframework.stereotype.Component

View File

@@ -3,6 +3,7 @@ package icu.samnyan.aqua.sega.allnet
import ext.* import ext.*
import icu.samnyan.aqua.net.db.AquaNetUserRepo import icu.samnyan.aqua.net.db.AquaNetUserRepo
import icu.samnyan.aqua.sega.allnet.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.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
@@ -13,6 +14,7 @@ import java.io.InputStream
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.*
@Configuration @Configuration
@ConfigurationProperties(prefix = "allnet.server") @ConfigurationProperties(prefix = "allnet.server")
@@ -82,7 +84,7 @@ class AllNet(
logger.info("AllNet /DownloadOrder : $reqMap") logger.info("AllNet /DownloadOrder : $reqMap")
val serial = reqMap["serial"] ?: "A69E01A8888" val serial = reqMap["serial"] ?: AquaConst.DEFAULT_KEYCHIP_ID
val resp = mapOf( val resp = mapOf(
"stat" to "1", "stat" to "1",
"serial" to serial "serial" to serial
@@ -101,7 +103,6 @@ class AllNet(
// encode UTF-8, format_ver 3, hops 1 token 2010451813 // encode UTF-8, format_ver 3, hops 1 token 2010451813
val reqMap = decodeAllNet(dataStream.readAllBytes()) val reqMap = decodeAllNet(dataStream.readAllBytes())
val serial = reqMap["serial"] ?: "" val serial = reqMap["serial"] ?: ""
var region = props.map.mut["region0"] ?: "1"
logger.info("AllNet /PowerOn : $reqMap") logger.info("AllNet /PowerOn : $reqMap")
var session: String? = null var session: String? = null
@@ -113,10 +114,6 @@ class AllNet(
if (u != null) { if (u != null) {
// Create a new session for the user // Create a new session for the user
logger.info("> Keychip authenticated: ${u.auId} ${u.computedName}") logger.info("> Keychip authenticated: ${u.auId} ${u.computedName}")
// If the user defined its own region apply it
if (u.region.isNotBlank()) {
region = u.region
}
session = keychipSessionService.new(u, reqMap["game_id"] ?: "").token session = keychipSessionService.new(u, reqMap["game_id"] ?: "").token
} }
@@ -143,7 +140,6 @@ class AllNet(
val resp = props.map.mut + mapOf( val resp = props.map.mut + mapOf(
"uri" to switchUri(here, localPort, gameId, ver, session), "uri" to switchUri(here, localPort, gameId, ver, session),
"host" to props.host.ifBlank { here }, "host" to props.host.ifBlank { here },
"region0" to region
) )
// Different responses for different versions // Different responses for different versions

View File

@@ -1,108 +0,0 @@
package icu.samnyan.aqua.sega.cardmaker
import ext.API
import ext.logger
import ext.long
import ext.parsing
import icu.samnyan.aqua.sega.allnet.TokenChecker
import icu.samnyan.aqua.sega.util.BasicMapper
import org.springframework.beans.factory.annotation.Value
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.net.InetAddress
import java.net.UnknownHostException
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@RestController
@RequestMapping("/g/card")
class CardMakerController(
val mapper: BasicMapper,
@param:Value("\${allnet.server.host:}") val ALLNET_HOST: String,
@param:Value("\${allnet.server.port:}") val ALLNET_PORT: String,
@param:Value("\${server.port:}") val SERVER_PORT: String
) {
val logger = logger()
@API("GetGameSettingApi")
fun getGameSetting(@ModelAttribute request: MutableMap<String, Any>): Any? {
val formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")
val rebootStartTime = LocalDateTime.now().minusHours(3)
val rebootEndTime = LocalDateTime.now().minusHours(2)
val gameSetting = mapOf(
"dataVersion" to "1.35.0",
"ongekiCmVersion" to "1.32.0",
"chuniCmVersion" to "1.30.0",
"maimaiCmVersion" to "1.45.0",
"isMaintenance" to false,
"requestInterval" to 10,
"rebootStartTime" to rebootStartTime.format(formatter),
"rebootEndTime" to rebootEndTime.format(formatter),
"isBackgroundDistribute" to false,
"maxCountCharacter" to 100,
"maxCountItem" to 100,
"maxCountCard" to 100,
"watermark" to false
)
val json = mapper.write(mapOf(
"gameSetting" to gameSetting,
"isDumpUpload" to false,
"isAou" to false
))
logger.info("Response: {}", json)
return json
}
fun gameConnect(modelKind: Int, modelVersion: Int, url: String) =
mapOf("modelKind" to modelKind, "modelVersion" to modelVersion, "url" to url)
@API("GetGameConnectApi")
fun getGameConnect(@ModelAttribute request: MutableMap<String, Any>): Any? {
val version = parsing { request["version"]!!.long } // Rom version
val session = TokenChecker.Companion.getCurrentSession()
val addr = ALLNET_HOST.ifBlank { null } ?:
try { InetAddress.getLocalHost().hostAddress }
catch (_: UnknownHostException) { "localhost" }
val port = ALLNET_PORT.ifBlank { null } ?: SERVER_PORT
val base = if (session == null) "/g" else "/gs/" + session.token
val json = mapper.write(mapOf(
"length" to 3,
"gameConnectList" to listOf(
gameConnect(0, 1, "http://$addr:$port$base/chu3/$version/"),
gameConnect(1, 1, "http://$addr:$port$base/mai2/"),
gameConnect(2, 1, "http://$addr:$port$base/ongeki/")
)
))
logger.info("Response: $json")
return json
}
@API("GetClientBookkeepingApi")
fun getClientBookkeeping(@ModelAttribute request: MutableMap<String, Any>): Any? {
val placeId = parsing { request["placeId"]!!.long }
val json = mapper.write(mapOf(
"placeId" to placeId,
"length" to 0,
"clientBookkeepingList" to mutableListOf<Any>()
))
logger.info("Response: $json")
return json
}
@API("UpsertClientBookkeepingApi")
fun upsertClientBookkeeping() = "{\"returnCode\":1,\"apiName\":\"UpsertClientBookkeepingApi\"}"
@API("UpsertClientSettingApi")
fun upsertClientSetting() = "{\"returnCode\":1,\"apiName\":\"UpsertClientSettingApi\"}"
}

View File

@@ -1,31 +0,0 @@
package icu.samnyan.aqua.sega.cardmaker
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import ext.logger
import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.RestControllerAdvice
import java.nio.charset.StandardCharsets
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@RestControllerAdvice(basePackages = ["icu.samnyan.aqua.sega.cardmaker"])
class CardMakerControllerAdvice {
val logger = logger()
val mapper = ObjectMapper()
/**
* Get the map object from json string
*
* @param request HttpServletRequest
*/
@ModelAttribute
fun preHandle(request: HttpServletRequest): MutableMap<String, Any> {
val src = request.inputStream.readAllBytes()
val outputString = String(src, StandardCharsets.UTF_8).trim { it <= ' ' }
logger.info("Request ${request.requestURI}: $outputString")
return mapper.readValue(outputString, object : TypeReference<MutableMap<String, Any>>() {})
}
}

View File

@@ -0,0 +1,56 @@
package icu.samnyan.aqua.sega.cardmaker.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.cardmaker.handler.impl.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@RestController
@RequestMapping("/g/card")
public class CardMakerController {
private final GetGameSettingHandler getGameSettingHandler;
private final GetClientBookkeepingHandler getClientBookkeepingHandler;
private final GetGameConnectHandler getGameConnectHandler;
@Autowired
public CardMakerController(GetGameSettingHandler getGameSettingHandler, GetClientBookkeepingHandler getClientBookkeepingHandler, GetGameConnectHandler getGameConnectHandler) {
this.getGameSettingHandler = getGameSettingHandler;
this.getClientBookkeepingHandler = getClientBookkeepingHandler;
this.getGameConnectHandler = getGameConnectHandler;
}
@PostMapping("GetGameSettingApi")
public String getGameSetting(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getGameSettingHandler.handle(request);
}
@PostMapping("GetGameConnectApi")
public String getGameConnect(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getGameConnectHandler.handle(request);
}
@PostMapping("GetClientBookkeepingApi")
public String getClientBookkeeping(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getClientBookkeepingHandler.handle(request);
}
@PostMapping("UpsertClientBookkeepingApi")
public String upsertClientBookkeeping(@ModelAttribute Map<String, Object> request) {
return "{\"returnCode\":1,\"apiName\":\"UpsertClientBookkeepingApi\"}";
}
@PostMapping("UpsertClientSettingApi")
public String upsertClientSetting(@ModelAttribute Map<String, Object> request) {
return "{\"returnCode\":1,\"apiName\":\"UpsertClientSettingApi\"}";
}
}

View File

@@ -0,0 +1,39 @@
package icu.samnyan.aqua.sega.cardmaker.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@RestControllerAdvice(basePackages = "icu.samnyan.aqua.sega.cardmaker")
public class CardMakerControllerAdvice {
private static final Logger logger = LoggerFactory.getLogger(CardMakerControllerAdvice.class);
/**
* Get the map object from json string
*
* @param request HttpServletRequest
*/
@ModelAttribute
public Map<String, Object> preHandle(HttpServletRequest request) throws IOException {
byte[] src = request.getInputStream().readAllBytes();
String outputString = new String(src, StandardCharsets.UTF_8).trim();
logger.info("Request " + request.getRequestURI() + ": " + outputString);
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(outputString, new TypeReference<>() {
});
}
}

View File

@@ -0,0 +1,45 @@
package icu.samnyan.aqua.sega.cardmaker.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.util.jackson.BasicMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component("CardMakerGetClientBookkeepingHandler")
public class GetClientBookkeepingHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetClientBookkeepingHandler.class);
private final BasicMapper mapper;
@Autowired
public GetClientBookkeepingHandler(BasicMapper mapper) {
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
long placeId = ((Number) request.get("placeId")).longValue();
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("placeId", placeId);
resultMap.put("length", 0);
resultMap.put("clientBookkeepingList", List.of());
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,80 @@
package icu.samnyan.aqua.sega.cardmaker.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.allnet.KeychipSession;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.cardmaker.model.response.data.GameConnect;
import icu.samnyan.aqua.sega.util.jackson.BasicMapper;
import icu.samnyan.aqua.sega.allnet.TokenChecker;
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.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component("CardMakerGetGameConnectHandler")
public class GetGameConnectHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetGameConnectHandler.class);
private final BasicMapper mapper;
private final String ALLNET_HOST;
private final String ALLNET_PORT;
private final String SERVER_PORT;
@Autowired
public GetGameConnectHandler(BasicMapper mapper, @Value("${allnet.server.host:}") String ALLNET_HOST,
@Value("${allnet.server.port:}") String ALLNET_PORT, @Value("${server.port:}") String SERVER_PORT) {
this.mapper = mapper;
this.ALLNET_HOST = ALLNET_HOST;
this.ALLNET_PORT = ALLNET_PORT;
this.SERVER_PORT = SERVER_PORT;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
int type = ((Number) request.get("type")).intValue(); // Allnet enabled or not
long version = ((Number) request.get("version")).longValue(); // Rom version
KeychipSession session = TokenChecker.Companion.getCurrentSession();
// Unless ip and port is explicitly overridden, use the guessed ip and port as same as AllNet Controller does.
String localAddr;
try {
localAddr = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
// If above didn't work then how did this run? I really don't know.
localAddr = "localhost";
}
String addr = ALLNET_HOST.equals("") ? localAddr : ALLNET_HOST;
String port = ALLNET_PORT.equals("") ? SERVER_PORT : ALLNET_PORT;
String base = session == null ? "/g" : "/gs/" + session.getToken();
List<GameConnect> gameConnectList = new ArrayList<>();
GameConnect chuni = new GameConnect(0, 1, "http://" + addr + ":" + port + base + "/chu3/" + version + "/");
GameConnect mai = new GameConnect(1, 1, "http://" + addr + ":" + port + base + "/mai2/");
GameConnect ongeki = new GameConnect(2, 1, "http://" + addr + ":" + port + base + "/ongeki/");
gameConnectList.add(chuni);
gameConnectList.add(mai);
gameConnectList.add(ongeki);
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("length", gameConnectList.size());
resultMap.put("gameConnectList", gameConnectList);
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,67 @@
package icu.samnyan.aqua.sega.cardmaker.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.cardmaker.model.response.GetGameSettingResp;
import icu.samnyan.aqua.sega.cardmaker.model.response.data.GameSetting;
import icu.samnyan.aqua.sega.util.jackson.BasicMapper;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component("CardMakerGetGameSettingHandler")
public class GetGameSettingHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetGameSettingHandler.class);
private final BasicMapper mapper;
@Autowired
public GetGameSettingHandler(BasicMapper mapper) {
this.mapper = mapper;
}
@Override
public String handle(@NotNull Map<String, ?> request) throws JsonProcessingException {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
LocalDateTime rebootStartTime = LocalDateTime.now().minusHours(3);
LocalDateTime rebootEndTime = LocalDateTime.now().minusHours(2);
GameSetting gameSetting = new GameSetting(
"1.35.0",
"1.32.0",
"1.30.0",
"1.45.0",
false,
10,
rebootStartTime.format(formatter),
rebootEndTime.format(formatter),
false,
100,
100,
100,
false);
GetGameSettingResp resp = new GetGameSettingResp(
gameSetting,
false,
false
);
String json = mapper.write(resp);
logger.info("Response: {}", json);
return json;
}
}

View File

@@ -0,0 +1,16 @@
package icu.samnyan.aqua.sega.cardmaker.model.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CodeResp {
private int returnCode;
private String apiName;
}

View File

@@ -0,0 +1,21 @@
package icu.samnyan.aqua.sega.cardmaker.model.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import icu.samnyan.aqua.sega.cardmaker.model.response.data.GameSetting;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GetGameSettingResp {
private GameSetting gameSetting;
@JsonProperty("isDumpUpload")
private boolean isDumpUpload;
@JsonProperty("isAou")
private boolean isAou;
}

View File

@@ -0,0 +1,14 @@
package icu.samnyan.aqua.sega.cardmaker.model.response.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GameConnect {
private int modelKind; // 0: chunithm, 1: maimai, 2: ongeki
private int type; // 0: LAN, 1: WAN
private String titleUri;
}

View File

@@ -0,0 +1,31 @@
package icu.samnyan.aqua.sega.cardmaker.model.response.data;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GameSetting {
private String dataVersion;
private String ongekiCmVersion;
private String chuniCmVersion;
private String maimaiCmVersion;
@JsonProperty("isMaintenance")
private boolean isMaintenance;
private int requestInterval;
private String rebootStartTime;
private String rebootEndTime;
@JsonProperty("isBackgroundDistribute")
private boolean isBackgroundDistribute;
private int maxCountCharacter;
private int maxCountItem;
private int maxCountCard;
private boolean watermark;
}

View File

@@ -0,0 +1,269 @@
package icu.samnyan.aqua.sega.chunithm.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.chunithm.handler.impl.*;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@RestController
@RequestMapping("/g/chu2/{ROM_VERSION}/{CLIENT_ID}/ChuniServlet")
@AllArgsConstructor
public class ChuniServletController {
private final GameLoginHandler gameLoginHandler;
private final GameLogoutHandler gameLogoutHandler;
private final GetGameChargeHandler getGameChargeHandler;
private final GetGameEventHandler getGameEventHandler;
private final GetGameIdlistHandler getGameIdlistHandler;
private final GetGameMessageHandler getGameMessageHandler;
private final GetGameRankingHandler getGameRankingHandler;
private final GetGameSaleHandler getGameSaleHandler;
private final GetGameSettingHandler getGameSettingHandler;
private final GetTeamCourseRuleHandler getTeamCourseRuleHandler;
private final GetTeamCourseSettingHandler getTeamCourseSettingHandler;
private final GetUserActivityHandler getUserActivityHandler;
private final GetUserCharacterHandler getUserCharacterHandler;
private final GetUserChargeHandler getUserChargeHandler;
private final GetUserCourseHandler getUserCourseHandler;
private final GetUserDataExHandler getUserDataExHandler;
private final GetUserDataHandler getUserDataHandler;
private final GetUserDuelHandler getUserDuelHandler;
private final GetUserFavoriteItemHandler getUserFavoriteItemHandler;
private final GetUserFavoriteMusicHandler getUserFavoriteMusicHandler;
private final GetUserItemHandler getUserItemHandler;
private final GetUserLoginBonusHandler getUserLoginBonusHandler;
private final GetUserMapHandler getUserMapHandler;
private final GetUserMusicHandler getUserMusicHandler;
private final GetUserOptionExHandler getUserOptionExHandler;
private final GetUserOptionHandler getUserOptionHandler;
private final GetUserPreviewHandler getUserPreviewHandler;
private final GetUserRecentRatingHandler getUserRecentRatingHandler;
private final GetUserRegionHandler getUserRegionHandler;
private final GetUserRivalDataHandler getUserRivalDataHandler;
private final GetUserRivalMusicHandler getUserRivalMusicHandler;
private final GetUserTeamHandler getUserTeamHandler;
private final UpsertClientSettingHandler upsertClientSettingHandler;
private final UpsertUserAllHandler upsertUserAllHandler;
private final UpsertUserChargelogHandler upsertUserChargelogHandler;
@PostMapping("GameLoginApi")
String gameLogin(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return gameLoginHandler.handle(request);
}
@PostMapping("GameLogoutApi")
String gameLogout(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return gameLogoutHandler.handle(request);
}
@PostMapping("GetGameChargeApi")
String getGameCharge(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getGameChargeHandler.handle(request);
}
@PostMapping("GetGameEventApi")
String getGameEvent(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getGameEventHandler.handle(request);
}
@PostMapping("GetGameIdlistApi")
String getGameIdList(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getGameIdlistHandler.handle(request);
}
@PostMapping("GetGameMessageApi")
String getGameMessage(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getGameMessageHandler.handle(request);
}
@PostMapping("GetGameRankingApi")
String getGameRanking(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getGameRankingHandler.handle(request);
}
@PostMapping("GetGameSaleApi")
String getGameSale(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getGameSaleHandler.handle(request);
}
/**
* The game start up request
*
* @return json of GameSetting object
*/
@PostMapping("GetGameSettingApi")
String getGameSetting(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getGameSettingHandler.handle(request);
}
@PostMapping("GetTeamCourseRuleApi")
String getTeamCourseRule(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getTeamCourseRuleHandler.handle(request);
}
@PostMapping("GetTeamCourseSettingApi")
String getTeamCourseSetting(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getTeamCourseSettingHandler.handle(request);
}
@PostMapping("GetUserActivityApi")
String getUserActivity(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserActivityHandler.handle(request);
}
@PostMapping("GetUserCharacterApi")
String getUserCharacter(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserCharacterHandler.handle(request);
}
@PostMapping("GetUserChargeApi")
String getUserCharge(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserChargeHandler.handle(request);
}
@PostMapping("GetUserCourseApi")
String getUserCourse(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserCourseHandler.handle(request);
}
@PostMapping("GetUserDataApi")
String getUserData(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserDataHandler.handle(request);
}
@PostMapping("GetUserDataExApi")
String getUserDataEx(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserDataExHandler.handle(request);
}
@PostMapping("GetUserDuelApi")
String getUserDuel(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserDuelHandler.handle(request);
}
@PostMapping("GetUserFavoriteItemApi")
String getUserFavoriteItem(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserFavoriteItemHandler.handle(request);
}
@PostMapping("GetUserFavoriteMusicApi")
public String getUserFavoriteMusic(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserFavoriteMusicHandler.handle(request);
}
@PostMapping("GetUserItemApi")
String getUserItem(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserItemHandler.handle(request);
}
@PostMapping("GetUserLoginBonusApi")
String getUserLoginBonus(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserLoginBonusHandler.handle(request);
}
@PostMapping("GetUserMapApi")
String getUserMap(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserMapHandler.handle(request);
}
@PostMapping("GetUserMusicApi")
String getUserMusic(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserMusicHandler.handle(request);
}
@PostMapping("GetUserOptionApi")
String getUserOption(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserOptionHandler.handle(request);
}
@PostMapping("GetUserOptionExApi")
String getUserOptionEx(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserOptionExHandler.handle(request);
}
// Call when login. Return null if no profile exist
@PostMapping("GetUserPreviewApi")
String getUserPreview(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserPreviewHandler.handle(request);
}
@PostMapping("GetUserRecentRatingApi")
String getUserRecentRating(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserRecentRatingHandler.handle(request);
}
/**
* For older version chunithm
*/
@PostMapping("GetUserRecentPlayerApi")
String getUserRecentPlayerApi(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserRecentRatingHandler.handle(request);
}
@PostMapping("GetUserRegionApi")
String getUserRegion(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserRegionHandler.handle(request);
}
@PostMapping("GetUserRivalDataApi")
String getUserRivalData(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserRivalDataHandler.handle(request);
}
@PostMapping("GetUserRivalMusicApi")
String getUserRivalMusic(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserRivalMusicHandler.handle(request);
}
@PostMapping("GetUserTeamApi")
String getUserTeam(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserTeamHandler.handle(request);
}
@PostMapping("UpsertClientBookkeepingApi")
String upsertClientBookkeeping(@ModelAttribute Map<String, Object> request) {
return "{\"returnCode\":\"1\"}";
}
@PostMapping("UpsertClientDevelopApi")
String upsertClientDevelop(@ModelAttribute Map<String, Object> request) {
return "{\"returnCode\":\"1\"}";
}
@PostMapping("UpsertClientErrorApi")
String upsertClientError(@ModelAttribute Map<String, Object> request) {
return "{\"returnCode\":\"1\"}";
}
@PostMapping("UpsertClientSettingApi")
String upsertClientSetting(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return upsertClientSettingHandler.handle(request);
}
@PostMapping("UpsertClientTestmodeApi")
String upsertClientTestmode(@ModelAttribute Map<String, Object> request) {
return "{\"returnCode\":\"1\"}";
}
@PostMapping("UpsertUserAllApi")
String upsertUserAll(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return upsertUserAllHandler.handle(request);
}
@PostMapping("UpsertUserChargelogApi")
String upsertUserChargelog(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return upsertUserChargelogHandler.handle(request);
}
@PostMapping("Ping")
String ping(@ModelAttribute Map<String, Object> request) {
return "{\"returnCode\":\"1\"}";
}
}

View File

@@ -0,0 +1,47 @@
package icu.samnyan.aqua.sega.chunithm.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.HandlerMapping;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static icu.samnyan.aqua.sega.util.AquaConst.*;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@RestControllerAdvice(basePackages = "icu.samnyan.aqua.sega.chunithm")
public class ChuniServletControllerAdvice {
private static final Logger logger = LoggerFactory.getLogger(ChuniServletControllerAdvice.class);
/**
* Get the map object from json string
*
* @param request HttpServletRequest
*/
@ModelAttribute
public Map<String, Object> preHandle(HttpServletRequest request) throws IOException {
var pathVar = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
byte[] src = request.getInputStream().readAllBytes();
String outputString = new String(src, StandardCharsets.UTF_8).trim();
logger.info("Request " + request.getRequestURI() + ": " + outputString);
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> result = mapper.readValue(outputString, new TypeReference<>() {
});
result.put(SERIAL_KEY, pathVar.getOrDefault(SERIAL_KEY, DEFAULT_KEYCHIP_ID));
result.put(VERSION_KEY, pathVar.getOrDefault(VERSION_KEY, CHUNI_DEFAULT_VERSION));
return result;
}
}

View File

@@ -0,0 +1,12 @@
package icu.samnyan.aqua.sega.chunithm.dao.gamedata;
import icu.samnyan.aqua.sega.chunithm.model.gamedata.Character;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository("ChuniGameCharacterRepository")
public interface GameCharacterRepository extends JpaRepository<Character, Long> {
}

View File

@@ -0,0 +1,12 @@
package icu.samnyan.aqua.sega.chunithm.dao.gamedata;
import icu.samnyan.aqua.sega.chunithm.model.gamedata.CharacterSkill;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository("ChuniGameCharacterSkillRepository")
public interface GameCharacterSkillRepository extends JpaRepository<CharacterSkill, Long> {
}

View File

@@ -0,0 +1,12 @@
package icu.samnyan.aqua.sega.chunithm.dao.gamedata;
import icu.samnyan.aqua.sega.chunithm.model.gamedata.GameCharge;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository("ChuniGameChargeRepository")
public interface GameChargeRepository extends JpaRepository<GameCharge, Long> {
}

View File

@@ -0,0 +1,16 @@
package icu.samnyan.aqua.sega.chunithm.dao.gamedata;
import icu.samnyan.aqua.sega.chunithm.model.gamedata.GameEvent;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository("ChuniGameEventRepository")
public interface GameEventRepository extends JpaRepository<GameEvent, Integer> {
List<GameEvent> findByEnable(boolean enable);
}

View File

@@ -0,0 +1,12 @@
package icu.samnyan.aqua.sega.chunithm.dao.gamedata;
import icu.samnyan.aqua.sega.chunithm.model.gamedata.GameMessage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository("ChuniGameMessageRepository")
public interface GameMessageRepository extends JpaRepository<GameMessage, Integer> {
}

View File

@@ -0,0 +1,16 @@
package icu.samnyan.aqua.sega.chunithm.dao.gamedata;
import icu.samnyan.aqua.sega.chunithm.model.gamedata.Music;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository("ChuniGameMusicRepository")
public interface GameMusicRepository extends JpaRepository<Music, Long> {
Optional<Music> findByMusicId(int musicId);
}

View File

@@ -0,0 +1,22 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserActivity;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserActivityRepository extends JpaRepository<UserActivity, Long> {
Optional<UserActivity> findTopByUserAndActivityIdAndKindOrderByIdDesc(UserData user, int activityId, int kind);
List<UserActivity> findAllByUser_Card_ExtIdAndKindOrderBySortNumberDesc(Long extId, int kind);
List<UserActivity> findAllByUser_Card_ExtId(Long extId);
}

View File

@@ -0,0 +1,27 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserCharacter;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserCharacterRepository extends JpaRepository<UserCharacter, Long> {
Page<UserCharacter> findByUser_Card_ExtId(Long extId, Pageable pageable);
List<UserCharacter> findByUser_Card_ExtId(Long extId);
Optional<UserCharacter> findTopByUserAndCharacterIdOrderByIdDesc(UserData user, int characterId);
Optional<UserCharacter> findByUser_Card_ExtIdAndCharacterId(Long extId, int characterId);
}

View File

@@ -0,0 +1,19 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserCharge;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserChargeRepository extends JpaRepository<UserCharge, Long> {
List<UserCharge> findByUser_Card_ExtId(Long extId);
Optional<UserCharge> findByUserAndChargeId(UserData extId, int chargeId);
}

View File

@@ -0,0 +1,23 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserCourse;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserCourseRepository extends JpaRepository<UserCourse, Long> {
Optional<UserCourse> findTopByUserAndCourseIdOrderByIdDesc(UserData user, int courseId);
Page<UserCourse> findByUser_Card_ExtId(Long extId, Pageable page);
List<UserCourse> findByUser_Card_ExtId(Long extId);
}

View File

@@ -0,0 +1,19 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserDataEx;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserDataExRepository extends JpaRepository<UserDataEx, Long> {
Optional<UserDataEx> findByUser(UserData user);
Optional<UserDataEx> findByUser_Card_ExtId(Long extId);
}

View File

@@ -0,0 +1,19 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.general.model.Card;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserDataRepository extends JpaRepository<UserData, Long> {
Optional<UserData> findByCard(Card card);
Optional<UserData> findByCard_ExtId(Long extId);
}

View File

@@ -0,0 +1,20 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserDuel;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserDuelRepository extends JpaRepository<UserDuel, Long> {
Optional<UserDuel> findTopByUserAndDuelIdOrderByIdDesc(UserData user, int duelId);
List<UserDuel> findByUser_Card_ExtId(Long extId);
}

View File

@@ -0,0 +1,18 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserGameOptionEx;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserGameOptionExRepository extends JpaRepository<UserGameOptionEx, Long> {
Optional<UserGameOptionEx> findByUser(UserData user);
Optional<UserGameOptionEx> findByUser_Card_ExtId(Long extId);
}

View File

@@ -0,0 +1,19 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserGameOption;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserGameOptionRepository extends JpaRepository<UserGameOption, Long> {
Optional<UserGameOption> findByUser(UserData user);
Optional<UserGameOption> findByUser_Card_ExtId(Long extId);
}

View File

@@ -0,0 +1,20 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserGeneralData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository("ChuniUserGeneralDataRepository")
public interface UserGeneralDataRepository extends JpaRepository<UserGeneralData, Long> {
Optional<UserGeneralData> findByUserAndPropertyKey(UserData user, String key);
Optional<UserGeneralData> findByUser_Card_ExtIdAndPropertyKey(Long extId, String key);
}

View File

@@ -0,0 +1,26 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserItem;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserItemRepository extends JpaRepository<UserItem, Long> {
Optional<UserItem> findTopByUserAndItemIdAndItemKindOrderByIdDesc(UserData user, int itemId, int itemKind);
Page<UserItem> findAllByUser_Card_ExtIdAndItemKind(Long extId, int itemKind, Pageable pageable);
List<UserItem> findAllByUser_Card_ExtId(Long extId);
Page<UserItem> findByUser_Card_ExtId(Long extId, Pageable pageable);
}

View File

@@ -0,0 +1,21 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserMap;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserMapRepository extends JpaRepository<UserMap, Long> {
List<UserMap> findAllByUser(UserData user);
List<UserMap> findAllByUser_Card_ExtId(Long extId);
Optional<UserMap> findTopByUserAndMapIdOrderByIdDesc(UserData user, int mapId);
}

View File

@@ -0,0 +1,26 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserMusicDetail;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserMusicDetailRepository extends JpaRepository<UserMusicDetail, Long> {
Optional<UserMusicDetail> findTopByUserAndMusicIdAndLevelOrderByIdDesc(UserData user, int musicId, int level);
List<UserMusicDetail> findByUser_Card_ExtId(Long extId);
List<UserMusicDetail> findByUser_Card_ExtIdAndMusicId(Long extId, int musicId);
Page<UserMusicDetail> findByUser_Card_ExtId(Long extId, Pageable page);
}

View File

@@ -0,0 +1,28 @@
package icu.samnyan.aqua.sega.chunithm.dao.userdata;
import icu.samnyan.aqua.sega.chunithm.model.response.data.GameRanking;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserPlaylog;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Repository
public interface UserPlaylogRepository extends JpaRepository<UserPlaylog, Long> {
List<UserPlaylog> findByUser_Card_ExtIdAndLevelNot(Long extId, int levelNot, Pageable page);
Page<UserPlaylog> findByUser_Card_ExtId(Long extId, Pageable page);
List<UserPlaylog> findByUser_Card_ExtIdAndMusicIdAndLevel(Long extId, int musicId, int level);
List<UserPlaylog> findByUser_Card_ExtId(Long extId);
@Query("SELECT NEW icu.samnyan.aqua.sega.chunithm.model.response.data.GameRanking(c.musicId, COUNT(c.musicId)) FROM ChuniUserPlaylog c WHERE NOT c.level = 4 GROUP BY c.musicId ORDER BY COUNT(c.musicId) DESC")
Page<GameRanking> findGameRankingByPlaylog(Pageable page);
}

View File

@@ -0,0 +1,43 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.response.CodeResp;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.chunithm.service.UserDataService;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Optional;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GameLoginHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GameLoginHandler.class);
private final StringMapper mapper;
private final UserDataService userDataService;
public GameLoginHandler(StringMapper mapper, UserDataService userDataService) {
this.mapper = mapper;
this.userDataService = userDataService;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String userId = (String) request.get("userId");
Optional<UserData> userDataOptional = userDataService.getUserByExtId(userId);
userDataOptional.ifPresent(userDataService::updateLoginTime);
String json = mapper.write(new CodeResp(1));
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,34 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.response.CodeResp;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GameLogoutHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GameLogoutHandler.class);
private final StringMapper mapper;
public GameLogoutHandler(StringMapper mapper) {
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String json = mapper.write(new CodeResp(1));
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,46 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.chunithm.dao.gamedata.GameChargeRepository;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.gamedata.GameCharge;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GetGameChargeHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetGameChargeHandler.class);
private final GameChargeRepository gameChargeRepository;
private final StringMapper mapper;
@Autowired
public GetGameChargeHandler(GameChargeRepository gameChargeRepository, StringMapper mapper) {
this.gameChargeRepository = gameChargeRepository;
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
List<GameCharge> gameChargeList = gameChargeRepository.findAll();
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("length", gameChargeList.size());
resultMap.put("gameChargeList", gameChargeList);
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,50 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.chunithm.dao.gamedata.GameEventRepository;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.gamedata.GameEvent;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GetGameEventHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetGameEventHandler.class);
private final GameEventRepository gameEventRepository;
private final StringMapper mapper;
@Autowired
public GetGameEventHandler(GameEventRepository gameEventRepository, StringMapper mapper) {
this.gameEventRepository = gameEventRepository;
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String type = (String) request.get("type");
List<GameEvent> gameEventList = gameEventRepository.findByEnable(true);
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("type", type);
resultMap.put("length", gameEventList.size());
resultMap.put("gameEventList", gameEventList);
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,47 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GetGameIdlistHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetGameIdlistHandler.class);
private final StringMapper mapper;
@Autowired
public GetGameIdlistHandler(StringMapper mapper) {
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String type = (String) request.get("type");
List<Object> gameIdlistList = new ArrayList<>();
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("type", type);
resultMap.put("length", 0);
resultMap.put("gameIdlistList", gameIdlistList);
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,50 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.chunithm.dao.gamedata.GameMessageRepository;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.gamedata.GameMessage;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GetGameMessageHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetGameMessageHandler.class);
private final GameMessageRepository gameMessageRepository;
private final StringMapper mapper;
@Autowired
public GetGameMessageHandler(GameMessageRepository gameMessageRepository, StringMapper mapper) {
this.gameMessageRepository = gameMessageRepository;
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String type = (String) request.get("type");
List<GameMessage> gameMessageList = gameMessageRepository.findAll();
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("type", type);
resultMap.put("length", gameMessageList.size());
resultMap.put("gameMessageList", gameMessageList);
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,51 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.chunithm.dao.userdata.UserPlaylogRepository;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.response.data.GameRanking;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GetGameRankingHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetGameRankingHandler.class);
private final UserPlaylogRepository userPlaylogRepository;
private final StringMapper mapper;
@Autowired
public GetGameRankingHandler(StringMapper mapper, UserPlaylogRepository userPlaylogRepository) {
this.userPlaylogRepository = userPlaylogRepository;
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String type = (String) request.get("type");
Page<GameRanking> rankingPage = userPlaylogRepository.findGameRankingByPlaylog(PageRequest.of(0, 10));
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("type", type);
resultMap.put("gameRankingList", rankingPage.getContent());
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,47 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.response.data.GameSale;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GetGameSaleHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetGameSaleHandler.class);
private final StringMapper mapper;
@Autowired
public GetGameSaleHandler(StringMapper mapper) {
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String type = (String) request.get("type");
List<GameSale> gameSaleList = new ArrayList<>();
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("type", type);
resultMap.put("length", 0);
resultMap.put("gameSaleList", gameSaleList);
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,66 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.response.GetGameSettingResp;
import icu.samnyan.aqua.sega.chunithm.model.response.data.GameSetting;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GetGameSettingHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetGameSettingHandler.class);
private final StringMapper mapper;
@Autowired
public GetGameSettingHandler(StringMapper mapper) {
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
// Fixed reboot time triggers chunithm maintenance lockout, so let's try minime method which sets it dynamically
// Special thanks to skogaby
// Hardcode so that the reboot time always started 3 hours ago and ended 2 hours ago
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
LocalDateTime rebootStartTime = LocalDateTime.now().minusHours(3);
LocalDateTime rebootEndTime = LocalDateTime.now().minusHours(2);
GameSetting gameSetting = new GameSetting(
1,
false,
10,
rebootStartTime.format(formatter),
rebootEndTime.format(formatter),
false,
300,
300,
300);
GetGameSettingResp resp = new GetGameSettingResp(
gameSetting,
false,
true
);
String json = mapper.write(resp);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,42 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.response.CodeResp;
import icu.samnyan.aqua.sega.chunithm.model.response.data.GameSale;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserData;
import icu.samnyan.aqua.sega.chunithm.service.UserDataService;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
public class GetTeamCourseRuleHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetTeamCourseRuleHandler.class);
private final StringMapper mapper;
public GetTeamCourseRuleHandler(StringMapper mapper) {
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String userId = (String) request.get("userId");
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("userId", userId);
resultMap.put("length", 0);
resultMap.put("nextIndex", 0);
resultMap.put("teamCourseRuleList", List.of());
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,41 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Component
public class GetTeamCourseSettingHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetTeamCourseSettingHandler.class);
private final StringMapper mapper;
public GetTeamCourseSettingHandler(StringMapper mapper) {
this.mapper = mapper;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String userId = (String) request.get("userId");
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("userId", userId);
resultMap.put("length", 0);
resultMap.put("nextIndex", 0);
resultMap.put("teamCourseSettingList", List.of());
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,53 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserActivity;
import icu.samnyan.aqua.sega.chunithm.service.UserActivityService;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GetUserActivityHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetUserActivityHandler.class);
private final StringMapper mapper;
private final UserActivityService userActivityService;
@Autowired
public GetUserActivityHandler(StringMapper mapper, UserActivityService userActivityService) {
this.mapper = mapper;
this.userActivityService = userActivityService;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String userId = (String) request.get("userId");
String kind = (String) request.get("kind");
List<UserActivity> userActivityList = userActivityService.getAllByUserIdAndKind(userId, kind);
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("userId", userId);
resultMap.put("length", userActivityList.size());
resultMap.put("kind", kind);
resultMap.put("userActivityList", userActivityList);
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

View File

@@ -0,0 +1,60 @@
package icu.samnyan.aqua.sega.chunithm.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.chunithm.model.userdata.UserCharacter;
import icu.samnyan.aqua.sega.chunithm.service.UserCharacterService;
import icu.samnyan.aqua.sega.util.jackson.StringMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Handle getUserCharacter request
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
public class GetUserCharacterHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetUserCharacterHandler.class);
private final StringMapper mapper;
private final UserCharacterService userCharacterService;
@Autowired
public GetUserCharacterHandler(StringMapper mapper, UserCharacterService userCharacterService) {
this.mapper = mapper;
this.userCharacterService = userCharacterService;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
String userId = (String) request.get("userId");
int nextIndex = Integer.parseInt((String) request.get("nextIndex"));
int maxCount = Integer.parseInt((String) request.get("maxCount"));
int pageNum = nextIndex / maxCount;
Page<UserCharacter> dbPage = userCharacterService.getByUserId(userId, pageNum, maxCount);
long currentIndex = maxCount * pageNum + dbPage.getNumberOfElements();
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("userId", userId);
resultMap.put("length", dbPage.getNumberOfElements());
resultMap.put("nextIndex", dbPage.getNumberOfElements() < maxCount ? -1 : currentIndex);
resultMap.put("userCharacterList", dbPage.getContent());
String json = mapper.write(resultMap);
logger.info("Response: " + json);
return json;
}
}

Some files were not shown because too many files have changed in this diff Show More