forked from Cookies_Github_mirror/AquaDX
Compare commits
89 Commits
68f99aa840
...
v1-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ddb72e28e | ||
|
|
25dbd5c874 | ||
|
|
50a9a2bdd0 | ||
|
|
6c4c9337e7 | ||
|
|
6776353556 | ||
|
|
3b19257ab1 | ||
|
|
a0cd7456ee | ||
|
|
aeafa6a396 | ||
|
|
448426a96d | ||
|
|
dfa6176689 | ||
|
|
d996fba291 | ||
|
|
0cb2a95ff3 | ||
|
|
be5220fd51 | ||
|
|
f23c0d6fe1 | ||
|
|
5aca650602 | ||
|
|
7c72348016 | ||
|
|
5eee6505f9 | ||
|
|
43b7ea65a5 | ||
|
|
85149dcd03 | ||
|
|
a767c8949c | ||
|
|
bbeb476a62 | ||
|
|
c444350cef | ||
|
|
13a318d519 | ||
|
|
e744d96c96 | ||
|
|
491044d37a | ||
|
|
9d30cf1e7d | ||
|
|
d2608472d8 | ||
|
|
34aae0c87a | ||
|
|
69bd35a579 | ||
|
|
3e6c0b4159 | ||
|
|
a33ec8b11c | ||
|
|
dd03ca38a1 | ||
|
|
1cac5e451a | ||
|
|
010d4592e4 | ||
|
|
b0d0f8ef7d | ||
|
|
967d311ee4 | ||
|
|
d5b777d720 | ||
|
|
2ab2666ad0 | ||
|
|
4971f2be78 | ||
|
|
b0a49d6626 | ||
|
|
d830854eaa | ||
|
|
68820d5a86 | ||
|
|
8b079bc40b | ||
|
|
b0dd9b845f | ||
|
|
b3d0670e1d | ||
|
|
e4734924f3 | ||
|
|
6ca419dd5b | ||
|
|
fc3f2171ee | ||
|
|
3d95a84739 | ||
|
|
15412911a9 | ||
|
|
9dc7a790cc | ||
|
|
d0b67c37f6 | ||
|
|
f6aa7d1fe3 | ||
|
|
2dc53cfbd7 | ||
|
|
db43e18b16 | ||
|
|
1a54527428 | ||
|
|
73026911da | ||
|
|
86558cd07e | ||
|
|
218d2788e8 | ||
|
|
0a37c2a854 | ||
|
|
7eda890473 | ||
|
|
2431bd09af | ||
|
|
7b21a38e17 | ||
|
|
bf51f48961 | ||
|
|
92868201a3 | ||
|
|
c01c40fe45 | ||
|
|
39ed8af840 | ||
|
|
82adf5c138 | ||
|
|
e0d12acf61 | ||
|
|
955743aecd | ||
|
|
4fb815a184 | ||
|
|
13ffe45dc6 | ||
|
|
5b699a2c3c | ||
|
|
bd32677e9e | ||
|
|
a98db63bec | ||
|
|
2430b8c448 | ||
|
|
e3486042a5 | ||
|
|
d79a4e5499 | ||
|
|
068b6179e5 | ||
|
|
3b90ac3c77 | ||
|
|
42b8eabb3a | ||
|
|
11dbe849cf | ||
|
|
ac6cbb9dd3 | ||
|
|
5c1f659437 | ||
|
|
155202dab9 | ||
|
|
71512bdad4 | ||
|
|
88d4a3d298 | ||
|
|
2563a31d15 | ||
|
|
9c91d730b4 |
49
.github/workflows/build.yml
vendored
49
.github/workflows/build.yml
vendored
@@ -1,49 +0,0 @@
|
||||
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
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
@@ -79,6 +79,8 @@
|
||||
|
||||
<Router {url}>
|
||||
<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="/ranking" component={Ranking} />
|
||||
<Route path="/ranking/:game" component={Ranking} />
|
||||
|
||||
60
AquaNet/src/components/Pagination.svelte
Normal file
60
AquaNet/src/components/Pagination.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<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>
|
||||
@@ -70,7 +70,7 @@
|
||||
if (ubKey == 'namePlateId') ubKey = 'nameplateId'
|
||||
if (ubKey == 'systemVoiceId') ubKey = 'voiceId'
|
||||
return [{ iKey, ubKey: ubKey as keyof UserBox,
|
||||
items: profile.items.filter(x => x.itemKind === iKind)
|
||||
items: profile.items.filter(x => x.itemKind === iKind || (iKey == "trophy" && x.itemKind == 3))
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -106,6 +106,133 @@
|
||||
.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) {
|
||||
const blob = new Blob([data]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -301,6 +428,10 @@
|
||||
<Icon icon="bxs:file-export"/>
|
||||
{t('settings.export')}
|
||||
</button>
|
||||
<button class="exportBatchManualButton" on:click={exportBatchManual}>
|
||||
<Icon icon="bxs:file-export"/>
|
||||
{t('settings.batchManualExport')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script>
|
||||
import { fade } from "svelte/transition";
|
||||
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
||||
import GameSettingFields from "./GameSettingFields.svelte";
|
||||
import { t, ts } from "../../libs/i18n";
|
||||
import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte";
|
||||
import RegionSelector from "./RegionSelector.svelte";
|
||||
|
||||
const rounding = useLocalStorage("rounding", true);
|
||||
</script>
|
||||
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
|
||||
<blockquote>
|
||||
{ts("settings.gameNotice")}
|
||||
{ts("settings.siteNotice")}
|
||||
</blockquote>
|
||||
<GameSettingFields game="general"/>
|
||||
<div class="field">
|
||||
<div class="bool">
|
||||
<input id="rounding" type="checkbox" bind:checked={rounding.value}/>
|
||||
@@ -22,6 +21,11 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<blockquote>
|
||||
{ts("settings.regionNotice")}
|
||||
</blockquote>
|
||||
<RegionSelector/>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@@ -44,19 +48,10 @@
|
||||
.desc
|
||||
opacity: 0.6
|
||||
|
||||
.field
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
label
|
||||
max-width: max-content
|
||||
|
||||
> div:not(.bool)
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
margin-top: 0.5rem
|
||||
|
||||
> input
|
||||
flex: 1
|
||||
.divider
|
||||
width: 100%
|
||||
height: 0.5px
|
||||
background: white
|
||||
opacity: 0.2
|
||||
margin: 0.4rem 0
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { slide, fade } from "svelte/transition";
|
||||
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
||||
import { FADE_IN, FADE_OUT, DATA_HOST } from "../../libs/config";
|
||||
import { t } from "../../libs/i18n.js";
|
||||
import Icon from "@iconify/svelte";
|
||||
import StatusOverlays from "../StatusOverlays.svelte";
|
||||
@@ -35,6 +35,169 @@
|
||||
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() {
|
||||
submitting = "export"
|
||||
@@ -70,6 +233,10 @@
|
||||
<Icon icon="bxs:file-export"/>
|
||||
{t('settings.export')}
|
||||
</button>
|
||||
<button class="exportBatchManualButton" on:click={exportBatchManual}>
|
||||
<Icon icon="bxs:file-export"/>
|
||||
{t('settings.batchManualExport')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<StatusOverlays {error} loading={!values[0] || !!submitting}/>
|
||||
|
||||
59
AquaNet/src/components/settings/RegionSelector.svelte
Normal file
59
AquaNet/src/components/settings/RegionSelector.svelte
Normal file
@@ -0,0 +1,59 @@
|
||||
<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>
|
||||
@@ -19,6 +19,7 @@ export interface AquaNetUser {
|
||||
email: string
|
||||
displayName: string
|
||||
country: string
|
||||
region:string
|
||||
lastLogin: number
|
||||
regTime: number
|
||||
profileLocation: string
|
||||
@@ -106,7 +107,8 @@ export interface GenericGameSummary {
|
||||
lastVersion: string
|
||||
ratingComposition: { [key: string]: any }
|
||||
recent: GenericGamePlaylog[]
|
||||
rival?: boolean
|
||||
rival?: boolean,
|
||||
favorites?: number[]
|
||||
}
|
||||
|
||||
export interface MusicMeta {
|
||||
|
||||
@@ -28,27 +28,38 @@ export const EN_REF_USER = {
|
||||
'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.ShowMoreRecent': 'Show more',
|
||||
'UserHome.FavoriteSongs': 'Favorite Songs'
|
||||
}
|
||||
|
||||
export const EN_REF_Welcome = {
|
||||
'back': 'Back',
|
||||
'email': 'Email',
|
||||
'password': 'Password',
|
||||
'new-password': 'New password',
|
||||
'username': 'Username',
|
||||
'welcome.btn-login': 'Log in',
|
||||
'welcome.btn-signup': 'Sign up',
|
||||
'welcome.email-password-missing': 'Email and password are required',
|
||||
'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.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-timeout': 'Network verification timed out. Please try again.',
|
||||
'welcome.verification-sent': 'A verification 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-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.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 has been sent to your inbox just now. 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.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.verified': 'Your email has been verified! You can now log in now.',
|
||||
'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 = {
|
||||
@@ -133,23 +144,35 @@ export const EN_REF_HOME = {
|
||||
export const EN_REF_SETTINGS = {
|
||||
'settings.title': 'Settings',
|
||||
'settings.tabs.profile': 'Profile',
|
||||
'settings.tabs.game': 'Game',
|
||||
'settings.tabs.global': 'Global',
|
||||
'settings.tabs.chu3': 'Chuni',
|
||||
'settings.tabs.mai2': 'Mai',
|
||||
'settings.tabs.ongeki': 'Ongeki',
|
||||
'settings.tabs.wacca': 'Wacca',
|
||||
'settings.fields.unlockMusic.name': 'Unlock All Music',
|
||||
'settings.fields.unlockMusic.desc': 'Unlock all music and master difficulty in game.',
|
||||
'settings.fields.unlockChara.name': 'Unlock All Characters',
|
||||
'settings.fields.unlockChara.desc': 'Unlock all characters, voices, and partners in game.',
|
||||
'settings.fields.unlockCollectables.name': 'Unlock All Collectables',
|
||||
'settings.fields.unlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame) in game.',
|
||||
'settings.fields.unlockTickets.name': 'Unlock All Tickets',
|
||||
'settings.fields.unlockTickets.desc': 'Infinite map/ex tickets (Note: maimai still limits which tickets can be used).',
|
||||
'settings.fields.waccaInfiniteWp.name': 'Wacca: Infinite WP',
|
||||
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999',
|
||||
'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP',
|
||||
'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01',
|
||||
'settings.fields.mai2UnlockMusic.name': 'Unlock All Music',
|
||||
'settings.fields.mai2UnlockMusic.desc': 'Unlock all music and master difficulty.',
|
||||
'settings.fields.mai2UnlockChara.name': 'Unlock All Characters',
|
||||
'settings.fields.mai2UnlockChara.desc': 'Unlock all characters (new characters start at level 1).',
|
||||
'settings.fields.mai2UnlockCharaMaxLevel.name': 'Max Character Level',
|
||||
'settings.fields.mai2UnlockCharaMaxLevel.desc': 'Set all characters to max level.',
|
||||
'settings.fields.mai2UnlockPartners.name': 'Unlock All Partners',
|
||||
'settings.fields.mai2UnlockPartners.desc': 'Unlock all partners.',
|
||||
'settings.fields.mai2UnlockCollectables.name': 'Unlock All Collectables',
|
||||
'settings.fields.mai2UnlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame).',
|
||||
'settings.fields.mai2UnlockTickets.name': 'Unlock All Tickets',
|
||||
'settings.fields.mai2UnlockTickets.desc': 'Infinite tickets (Note: client still limits which tickets can be used).',
|
||||
'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.desc': 'Customize the text displayed on the top of your profile.',
|
||||
'settings.fields.chusanInfinitePenguins.name': 'Infinite Penguins',
|
||||
@@ -183,8 +206,13 @@ export const EN_REF_SETTINGS = {
|
||||
'settings.profile.logout': 'Log out',
|
||||
'settings.profile.unchanged': 'Unchanged',
|
||||
'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.gameNotice': "These only apply to Mai and Wacca."
|
||||
'settings.siteNotice': "These settings only apply to the website.",
|
||||
'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 = {
|
||||
|
||||
@@ -40,27 +40,38 @@ const zhUser: typeof EN_REF_USER = {
|
||||
'UserHome.RemoveRival': "移除劲敌",
|
||||
'UserHome.InvalidGame': "游戏 ${game} 还不支持网页端查看。我们目前只支持舞萌、中二、华卡和音击。",
|
||||
'UserHome.ShowMoreRecent': "显示更多",
|
||||
'UserHome.FavoriteSongs': "收藏歌曲"
|
||||
}
|
||||
|
||||
const zhWelcome: typeof EN_REF_Welcome = {
|
||||
'back': '返回',
|
||||
'email': '邮箱',
|
||||
'password': '密码',
|
||||
'new-password': '新密码',
|
||||
'username': '用户名',
|
||||
'welcome.btn-login': '登录',
|
||||
'welcome.btn-signup': '注册',
|
||||
'welcome.email-password-missing': '邮箱和密码必须填哦',
|
||||
'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.waiting-turnstile': '正在验证网络环境…',
|
||||
'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试',
|
||||
'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
|
||||
'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
|
||||
'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱',
|
||||
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
|
||||
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱',
|
||||
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
|
||||
'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!',
|
||||
'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了',
|
||||
'welcome.verifying': '正在验证邮箱…请稍等',
|
||||
'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了',
|
||||
'welcome.verification-failed': '验证失败:${message}。请重试',
|
||||
'welcome.password-reset-done': '您的密码已更新!请重新登录',
|
||||
}
|
||||
|
||||
const zhLeaderboard: typeof EN_REF_LEADERBOARD = {
|
||||
@@ -145,23 +156,35 @@ const zhHome: typeof EN_REF_HOME = {
|
||||
const zhSettings: typeof EN_REF_SETTINGS = {
|
||||
'settings.title': '用户设置',
|
||||
'settings.tabs.profile': '个人资料',
|
||||
'settings.tabs.game': '游戏设置',
|
||||
'settings.tabs.global': '全局',
|
||||
'settings.tabs.chu3': '中二',
|
||||
'settings.tabs.mai2': '舞萌',
|
||||
'settings.tabs.ongeki': '音击',
|
||||
'settings.tabs.wacca': '华卡',
|
||||
'settings.fields.unlockMusic.name': '解锁谱面',
|
||||
'settings.fields.unlockMusic.desc': '在游戏中解锁所有曲目和大师难度谱面。',
|
||||
'settings.fields.unlockChara.name': '解锁角色',
|
||||
'settings.fields.unlockChara.desc': '在游戏中解锁所有角色、语音和伙伴。',
|
||||
'settings.fields.unlockCollectables.name': '解锁收藏品',
|
||||
'settings.fields.unlockCollectables.desc': '在游戏中解锁所有收藏品(名牌、称号、图标、背景图)。',
|
||||
'settings.fields.unlockTickets.name': '解锁游戏券',
|
||||
'settings.fields.unlockTickets.desc': '无限跑图券/解锁券(注:maimai 客户端仍限制一些券不能使用)。',
|
||||
'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.mai2UnlockMusic.name': '解锁谱面',
|
||||
'settings.fields.mai2UnlockMusic.desc': '解锁所有曲目和大师难度谱面。',
|
||||
'settings.fields.mai2UnlockChara.name': '解锁角色',
|
||||
'settings.fields.mai2UnlockChara.desc': '解锁所有角色(新角色从 1 级开始)。',
|
||||
'settings.fields.mai2UnlockCharaMaxLevel.name': '角色满级',
|
||||
'settings.fields.mai2UnlockCharaMaxLevel.desc': '将所有角色设置为满级。',
|
||||
'settings.fields.mai2UnlockPartners.name': '解锁搭档',
|
||||
'settings.fields.mai2UnlockPartners.desc': '解锁所有搭档。',
|
||||
'settings.fields.mai2UnlockCollectables.name': '解锁收藏品',
|
||||
'settings.fields.mai2UnlockCollectables.desc': '解锁所有收藏品(姓名框、称号、头像、背景)。',
|
||||
'settings.fields.mai2UnlockTickets.name': '解锁功能票',
|
||||
'settings.fields.mai2UnlockTickets.desc': '无限功能票(注:客户端仍限制一些功能票不能使用)。',
|
||||
'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.desc': '自定义显示在个人资料顶部的文本。',
|
||||
'settings.fields.chusanInfinitePenguins.name': '我是桐谷遥',
|
||||
@@ -195,8 +218,18 @@ const zhSettings: typeof EN_REF_SETTINGS = {
|
||||
'settings.profile.logout': '登出',
|
||||
'settings.profile.unchanged': '未更改',
|
||||
'settings.export': '导出玩家数据',
|
||||
'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi)",
|
||||
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
|
||||
'settings.gameNotice': "这些设置仅对舞萌和华卡生效。",
|
||||
// AI
|
||||
'settings.siteNotice': "这些设置仅适用于网站。",
|
||||
// AI
|
||||
'settings.regionNotice': "这些设置在舞萌、音击和中二节奏之间共享。",
|
||||
// AI
|
||||
'settings.regionSelector.title': "地区选择器",
|
||||
// AI
|
||||
'settings.regionSelector.desc': "选择游戏中显示的地区",
|
||||
// AI
|
||||
'settings.regionSelector.select': "选择地区",
|
||||
}
|
||||
|
||||
export const zhUserbox: typeof EN_REF_USERBOX = {
|
||||
|
||||
@@ -163,12 +163,22 @@ async function login(user: { email: string, password: string, turnstile: string
|
||||
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 ensureLoggedIn = () => !isLoggedIn() && (window.location.href = '/')
|
||||
|
||||
export const USER = {
|
||||
register,
|
||||
login,
|
||||
resetPassword,
|
||||
changePassword,
|
||||
confirmEmail: (token: string) =>
|
||||
post('/api/v2/user/confirm-email', { token }),
|
||||
me: (): Promise<AquaNetUser> => {
|
||||
@@ -186,6 +196,8 @@ export const USER = {
|
||||
},
|
||||
isLoggedIn,
|
||||
ensureLoggedIn,
|
||||
changeRegion: (regionId: number) =>
|
||||
post('/api/v2/user/change-region', { regionId }),
|
||||
}
|
||||
|
||||
export const USERBOX = {
|
||||
@@ -254,5 +266,14 @@ export const TRANSFER = {
|
||||
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
|
||||
window.sdk = { USER, USERBOX, CARD, GAME, DATA, SETTING, TRANSFER }
|
||||
window.sdk = { USER, USERBOX, CARD, GAME, DATA, SETTING, TRANSFER, FEDY }
|
||||
|
||||
@@ -186,8 +186,6 @@ export function initializeDb() : Promise<void> {
|
||||
export async function userboxFileProcess(folder: FileSystemEntry, progressUpdate: (progress: number, progressString: string) => void): Promise<string | null> {
|
||||
if (!isDirectory(folder))
|
||||
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();
|
||||
const optionFolder = await scanRecursive(folder, "A001");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { title } from "../libs/ui";
|
||||
import { GAME } from "../libs/sdk";
|
||||
import type { GenericRanking } from "../libs/generalTypes";
|
||||
@@ -8,6 +9,7 @@
|
||||
import { t } from "../libs/i18n";
|
||||
import UserCard from "../components/UserCard.svelte";
|
||||
import Tooltip from "../components/Tooltip.svelte";
|
||||
import Pagination from "../components/Pagination.svelte";
|
||||
|
||||
export let game: GameName = 'mai2';
|
||||
|
||||
@@ -15,15 +17,45 @@
|
||||
|
||||
let d: { users: GenericRanking[] };
|
||||
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)])
|
||||
.then(([users]) => {
|
||||
console.log(users)
|
||||
d = { users };
|
||||
d = { users }
|
||||
totalPages = Math.ceil(users.length / perPage)
|
||||
})
|
||||
.catch((e) => error = e.message);
|
||||
|
||||
let hoveringUser = "";
|
||||
let hoverLoading = false;
|
||||
|
||||
$: paginatedUsers = d ? d.users.slice((page - 1) * perPage, page * perPage) : []
|
||||
</script>
|
||||
|
||||
<main class="content leaderboard">
|
||||
@@ -37,8 +69,12 @@
|
||||
</div>
|
||||
|
||||
{#if d}
|
||||
{#if page > 1}
|
||||
<Pagination {page} {totalPages} on:updatePage={handleUpdatePage} />
|
||||
{/if}
|
||||
|
||||
<div class="leaderboard-container">
|
||||
<div class="lb-user" on:mouseenter={() => hoveringUser = d.users[0].username} role="heading" aria-level="2">
|
||||
<div class="lb-user" on:mouseenter={() => hoveringUser = paginatedUsers[0]?.username} role="heading" aria-level="2">
|
||||
<span class="rank">{t("Leaderboard.Rank")}</span>
|
||||
<span class="name"></span>
|
||||
<span class="rating">{t("Leaderboard.Rating")}</span>
|
||||
@@ -46,7 +82,7 @@
|
||||
<span class="fc">{t("Leaderboard.FC")}</span>
|
||||
<span class="ap">{t("Leaderboard.AP")}</span>
|
||||
</div>
|
||||
{#each d.users as user, i (user.rank)}
|
||||
{#each paginatedUsers as user, i (user.rank)}
|
||||
<div class="lb-user" class:alternate={i % 2 === 1} role="listitem"
|
||||
on:mouseover={() => hoveringUser = user.username} on:focus={() => {}}>
|
||||
|
||||
@@ -70,6 +106,8 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Pagination {page} {totalPages} on:updatePage={handleUpdatePage} />
|
||||
|
||||
<Tooltip triggeredBy=".name" loading={hoverLoading}>
|
||||
<UserCard username={hoveringUser} {game} setLoading={l => hoverLoading = l} />
|
||||
</Tooltip>
|
||||
@@ -132,5 +170,4 @@
|
||||
&.alternate
|
||||
background-color: vars.$ov-light
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
}).catch(err => error = err.message).finally(() => loading = false)
|
||||
}
|
||||
|
||||
$: isBlacklist = !!blacklist.filter(x => src.dns.includes(x))
|
||||
$: isBlacklist = blacklist.filter(x => src.dns.includes(x)).length > 0
|
||||
</script>
|
||||
|
||||
<StatusOverlays {loading} />
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
let error: string;
|
||||
let submitting = ""
|
||||
let tab = 0
|
||||
let tabs = [ 'profile', 'game' ]
|
||||
let tabs = ['profile']
|
||||
|
||||
const profileFields = [
|
||||
[ 'displayName', t('settings.profile.name') ],
|
||||
@@ -45,18 +45,11 @@
|
||||
me = m
|
||||
|
||||
CARD.userGames(m.username).then(games => {
|
||||
if (games.chu3 && !tabs.includes('chu3')) {
|
||||
tabs = [...tabs, 'chu3']
|
||||
}
|
||||
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']
|
||||
}
|
||||
tabs = [
|
||||
...tabs,
|
||||
...['chu3', 'mai2','wacca', 'ongeki'].filter(v => games[v as keyof typeof games]), // :xdx:
|
||||
'global'
|
||||
]
|
||||
})
|
||||
}).catch(e => error = e.message)
|
||||
getMe()
|
||||
@@ -80,11 +73,12 @@
|
||||
// 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 ctx = canvas.getContext("2d");
|
||||
canvas.width = 256;
|
||||
canvas.height = 256;
|
||||
const size = Math.round(Math.min(pfpCrop.width, pfpCrop.height, 1024));
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
let img = document.createElement("img");
|
||||
img.onload = () => {
|
||||
ctx?.drawImage(img, pfpCrop.x, pfpCrop.y, pfpCrop.width, pfpCrop.height, 0, 0, 256, 256);
|
||||
ctx?.drawImage(img, pfpCrop.x, pfpCrop.y, pfpCrop.width, pfpCrop.height, 0, 0, size, size);
|
||||
canvas.toBlob(blob => {
|
||||
if (!blob) return;
|
||||
submitting = 'profilePicture'
|
||||
@@ -216,7 +210,7 @@
|
||||
<WaccaSettings />
|
||||
{:else if tabs[tab] === 'ongeki'}
|
||||
<OngekiSettings />
|
||||
{:else if tabs[tab] === 'game'}
|
||||
{:else if tabs[tab] === 'global'}
|
||||
<GeneralGameSettings />
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -325,7 +325,6 @@
|
||||
<RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} game={game != "auto" ? game : "mai2"} top={10}/>
|
||||
{/if}
|
||||
|
||||
|
||||
<div class="recent">
|
||||
<h2>{t('UserHome.RecentScores')}</h2>
|
||||
<div class="scores">
|
||||
@@ -368,6 +367,22 @@
|
||||
{/if}
|
||||
</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}
|
||||
|
||||
<StatusOverlays {error} loading={!d || isLoading} />
|
||||
@@ -554,6 +569,57 @@
|
||||
flex-direction: row
|
||||
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
|
||||
|
||||
@@ -20,18 +20,20 @@
|
||||
|
||||
let error = ""
|
||||
let verifyMsg = ""
|
||||
let token = ""
|
||||
|
||||
if (USER.isLoggedIn()) {
|
||||
window.location.href = "/home"
|
||||
}
|
||||
|
||||
if (params.get('confirm-email')) {
|
||||
if (params.get('code')) {
|
||||
token = params.get('code')!
|
||||
if (location.pathname === '/verify') {
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verifying")
|
||||
submitting = true
|
||||
|
||||
// Send request to server
|
||||
USER.confirmEmail(params.get('confirm-email')!)
|
||||
USER.confirmEmail(token)
|
||||
.then(() => {
|
||||
verifyMsg = t('welcome.verified')
|
||||
submitting = false
|
||||
@@ -41,7 +43,10 @@
|
||||
})
|
||||
.catch(e => verifyMsg = t('welcome.verification-failed', { message: e.message }))
|
||||
}
|
||||
|
||||
else if (location.pathname === '/reset-password') {
|
||||
state = 'reset'
|
||||
}
|
||||
}
|
||||
async function submit(): Promise<any> {
|
||||
submitting = true
|
||||
|
||||
@@ -96,7 +101,7 @@
|
||||
}
|
||||
else {
|
||||
error = e.message
|
||||
submitting = false
|
||||
submitting = false // unnecessary? see line 113, same for both reset functions
|
||||
turnstileReset()
|
||||
}
|
||||
})
|
||||
@@ -105,6 +110,69 @@
|
||||
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 {
|
||||
error = e.message
|
||||
submitting = false
|
||||
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
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<main id="home" class="no-margin">
|
||||
@@ -120,11 +188,13 @@
|
||||
{#if error}
|
||||
<span class="error">{error}</span>
|
||||
{/if}
|
||||
{#if error != t("welcome.waiting-turnstile")}
|
||||
<div on:click={() => state = 'home'} on:keypress={() => state = 'home'}
|
||||
role="button" tabindex="0" class="clickable">
|
||||
<Icon icon="line-md:chevron-small-left" />
|
||||
<span>{t('back')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isSignup}
|
||||
<input type="text" placeholder={t('username')} bind:value={username}>
|
||||
{/if}
|
||||
@@ -137,6 +207,37 @@
|
||||
{isSignup ? t('welcome.btn-signup') : t('welcome.btn-login')}
|
||||
{/if}
|
||||
</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}
|
||||
<Turnstile siteKey={TURNSTILE_SITE_KEY} bind:reset={turnstileReset}
|
||||
on:turnstile-callback={e => console.log(turnstile = e.detail.token)}
|
||||
@@ -152,6 +253,20 @@
|
||||
<button on:click={() => state = 'home'} transition:slide>{t('back')}</button>
|
||||
{/if}
|
||||
</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}
|
||||
</div>
|
||||
|
||||
|
||||
64
README.md
64
README.md
@@ -22,59 +22,61 @@ selling it on any platform is strictly prohibited as per the CC By-NC-SA license
|
||||
|
||||
Multipurpose game server for ALL.Net games.
|
||||
|
||||
### Related Projects
|
||||
## Related Projects
|
||||
|
||||
* [AquaMai](https://github.com/MewoLab/AquaMai): A maimai DX mod that adds many features to the game.
|
||||
* [AquaNet](./AquaNet): A new web frontend for the modern age.
|
||||
* [AquaMai](https://github.com/MuNET-OSS/AquaMai): A maimai DX mod that adds many features to the game.
|
||||
* [AquaNet](./AquaNet): The primary AquaDX web frontend, hosted publically at [aquadx.net](https://aquadx.net/)
|
||||
|
||||
### Supported Games
|
||||
## Supported Games
|
||||
|
||||
Below is a list of games supported by this server.
|
||||
> [!WARNING]
|
||||
> CHUNITHM pre-NEW!! and maimai pre-DX are no longer supported after February 6th, 2026 and all associated data will be removed.
|
||||
|
||||
| Game | Ver | Codename | Thanks to |
|
||||
|------------------------|------|-------------|------------------------------------------------------|
|
||||
| SDHD: CHUNITHM | 2.30 | VERSE | |
|
||||
| SDEZ: MaiMai DX | 1.55 | PRiSM Plus | |
|
||||
| SDGA: MaiMai DX (Intl) | 1.50 | PRiSM | [@Clansty](https://github.com/clansty) |
|
||||
| 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) |
|
||||
| SBZV: Project DIVA | 7.10 | Future Tone | |
|
||||
| SDFE: Wacca (*ALPHA) | 3.07 | Reverse | |
|
||||
| Game | Latest Ver. | Initial Ver. | Notes |
|
||||
|------------------------|---------------------|---------------------|-------------------------------------------------------------|
|
||||
| SDHD: CHUNITHM | 2.40 (X-VERSE) | 2.00 (NEW) | Missing some X-VERSE features |
|
||||
| SDEZ: MaiMai DX | 1.60 (CiRCLE) | 1.00 (DX) | Missing circle (teams) support |
|
||||
| SDGA: MaiMai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) |
|
||||
| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) |
|
||||
| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) |
|
||||
| SBZV: Project DIVA | 7.10 | N/A | No web interface provided |
|
||||
| SDFE: Wacca | 3.07 (Reverse) | N/A | Later versions are EOS patches, network will not work |
|
||||
|
||||
<!-- A majority of them have been left as N/A for initial version as they do not appear to have restrictions -->
|
||||
|
||||
Check out these docs for more information.
|
||||
* [Game specific notes](docs/game_specific_notes.md)
|
||||
* [Frequently asked questions](docs/frequently_asked_questions.md)
|
||||
|
||||
> [!TIP]
|
||||
> 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
|
||||
If you own a cab or controller and just want to play the game, follow the instructions below:
|
||||
|
||||
1. Make sure you have obtained game files on your own (we will not provide them).
|
||||
2. Go to [aquadx.net](https://aquadx.net) and make an account.
|
||||
3. Click on "Setup Connection" in the home page, and follow the instructions.
|
||||
4. Play a coin with your card.
|
||||
(Either a physical card or the `aime.txt` / `felica.txt` in your segatools)
|
||||
5. Pet your cat 🐱
|
||||
6. Link your card on the website.
|
||||
### Public Instance
|
||||
|
||||
If you encounter any issue, please report in the [issue tracker](https://MewoLab/AquaDX/issues).
|
||||
1. Ensure your game can boot to title screen.
|
||||
2. Go to [https://aquadx.net](https://aquadx.net) and sign up (or log in).
|
||||
3. Access the Setup Connection page and follow the instructions provided.
|
||||
|
||||
If you encounter any issue, please report via Discord, QQ, (both available on the website) or the GitHub issue tracker.
|
||||
|
||||
> [!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.
|
||||
> Your card's access code can be identified in all supported games on their title screen.<br>
|
||||
> Press the "access code" and scan your card to retrieve it.
|
||||
|
||||
## Self Hosting (Advanced)
|
||||
### Self Hosting (Advanced)
|
||||
|
||||
Please read the [self-hosting guide](docs/self-hosting.md) if you want to host your own server. This is only for advanced users and developers. Do not ask for support if you are not familiar with programming or networking.
|
||||
Please read the [self-hosting guide](docs/self-hosting.md) if you want to host your own server.
|
||||
This is only for advanced users and developers.
|
||||
Do not ask for support if you are not familiar with programming or networking.
|
||||
|
||||
## License: [CC By-NC-SA](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
|
||||
## License
|
||||
|
||||
AquaDX uses the [CC By-NC-SA](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en) license:
|
||||
|
||||
* **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
||||
* **NonCommercial** — You may not use the material for commercial purposes.
|
||||
* **ShareAlike** — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
|
||||
|
||||
### Credit
|
||||
## Attributions
|
||||
* **samnyan**: The creator and developer of the original Aqua server
|
||||
* **Akasaka Ryuunosuke**: providing all the DIVA protocol information
|
||||
* **Dom Eori**: Developer of forked Aqua server, from v0.0.17 and up
|
||||
|
||||
@@ -7,14 +7,12 @@ plugins {
|
||||
val ktVer = "2.1.10"
|
||||
|
||||
java
|
||||
kotlin("plugin.lombok") version ktVer
|
||||
kotlin("jvm") version ktVer
|
||||
kotlin("plugin.spring") version ktVer
|
||||
kotlin("plugin.jpa") version ktVer
|
||||
kotlin("plugin.serialization") version ktVer
|
||||
kotlin("plugin.allopen") version ktVer
|
||||
kotlin("kapt") version ktVer
|
||||
id("io.freefair.lombok") version "8.6"
|
||||
id("org.springframework.boot") version "3.2.3"
|
||||
id("com.github.ben-manes.versions") version "0.51.0"
|
||||
id("org.hibernate.orm") version "6.4.4.Final"
|
||||
@@ -171,3 +169,21 @@ sourceSets {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -131,6 +131,11 @@ server.error.whitelabel.enabled=false
|
||||
aqua-net.frontier.enabled=false
|
||||
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
|
||||
aqua-net.bot.enabled=true
|
||||
aqua-net.bot.secret=hunter2
|
||||
|
||||
@@ -59,7 +59,7 @@ Located at: [icu.samnyan.aqua.net.UserRegistrar](icu/samnyan/aqua/net/UserRegist
|
||||
* token: String
|
||||
* **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
|
||||
* password: String
|
||||
@@ -74,6 +74,18 @@ Located at: [icu.samnyan.aqua.net.UserRegistrar](icu/samnyan/aqua/net/UserRegist
|
||||
* turnstile: String
|
||||
* **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.
|
||||
|
||||
* token: String
|
||||
|
||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.withContext
|
||||
import org.apache.tika.Tika
|
||||
import org.apache.tika.mime.MimeTypes
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity.BodyBuilder
|
||||
@@ -34,12 +35,15 @@ import java.util.concurrent.locks.Lock
|
||||
import kotlin.reflect.KCallable
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
typealias RP = RequestParam
|
||||
typealias RB = RequestBody
|
||||
typealias RT = RequestPart
|
||||
typealias RH = RequestHeader
|
||||
typealias PV = PathVariable
|
||||
typealias API = RequestMapping
|
||||
@@ -79,7 +83,9 @@ annotation class SettingField(
|
||||
|
||||
// Reflection
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> KClass<T>.vars() = memberProperties.mapNotNull { it as? Var<T, Any> }
|
||||
fun <T : Any> KClass<T>.ownVars() = declaredMemberProperties.sortedBy { it.javaField?.declaringClass?.declaredFields?.indexOf(it.javaField) ?: Int.MAX_VALUE }.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>.getters() = java.methods.filter { it.name.startsWith("get") }
|
||||
fun <T : Any> KClass<T>.gettersMap() = getters().associateBy { it.name.removePrefix("get").firstCharLower() }
|
||||
@@ -211,6 +217,8 @@ val <K, V> Map<K, V>.mut get() = toMutableMap()
|
||||
val <T> Set<T>.mut get() = toMutableSet()
|
||||
|
||||
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
|
||||
operator fun <T> Optional<T>.invoke(): T? = orElse(null)
|
||||
@@ -227,6 +235,7 @@ fun Str.fromChusanUsername() = String(this.toByteArray(StandardCharsets.ISO_8859
|
||||
fun Str.truncate(len: Int) = if (this.length > len) this.take(len) + "..." else this
|
||||
val Str.some get() = ifBlank { null }
|
||||
val ByteArray.hexStr get() = toHexString()
|
||||
operator fun StringBuilder.plusAssign(other: String) { this.append(other) }
|
||||
|
||||
// Coroutine
|
||||
suspend fun <T> async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() }
|
||||
@@ -255,6 +264,7 @@ operator fun <E> List<E>.component13(): E = get(12)
|
||||
|
||||
inline operator fun <reified E> List<Any?>.invoke(i: Int) = get(i) as E
|
||||
val empty = emptyList<Any>()
|
||||
val emptyMap = emptyMap<Any, Any>()
|
||||
|
||||
val <F> Pair<F, *>.l get() = component1()
|
||||
val <S> Pair<*, S>.r get() = component2()
|
||||
@@ -263,3 +273,6 @@ val <S> Pair<*, S>.r get() = component2()
|
||||
val Query.exec get() = resultList.map { (it as Array<*>).toList() }
|
||||
fun List<List<Any?>>.numCsv(vararg head: Str) = head.joinToString(",") + "\n" +
|
||||
joinToString("\n") { it.joinToString(",") }
|
||||
|
||||
// DI
|
||||
inline fun <reified T> ApplicationContext.lazy() = lazy { getBean(T::class.java) }
|
||||
|
||||
@@ -8,6 +8,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
// Jackson
|
||||
val ACCEPTABLE_FALSE = setOf("0", "false", "no", "off", "False", "None", "null")
|
||||
@@ -19,9 +21,15 @@ val JSON_FUZZY_BOOLEAN = SimpleModule().addDeserializer(Boolean::class.java, obj
|
||||
else -> 400 - "Invalid boolean value ${parser.text}"
|
||||
}
|
||||
})
|
||||
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<java.time.LocalDateTime>() {
|
||||
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<LocalDateTime>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext) =
|
||||
parser.text.asDateTime() ?: (400 - "Invalid date time value ${parser.text}")
|
||||
// First try standard formats via asDateTime() method
|
||||
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 {
|
||||
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
|
||||
@@ -4,6 +4,7 @@ import ext.*
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
import icu.samnyan.aqua.net.utils.SUCCESS
|
||||
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.sensitiveInfo
|
||||
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
|
||||
@@ -52,7 +53,7 @@ class BotController(
|
||||
secret.checkSecret()
|
||||
|
||||
// 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
|
||||
us.cardRepo.save(oc.apply {
|
||||
@@ -66,7 +67,7 @@ class BotController(
|
||||
fun clearMigrateFlag(@RP secret: Str, @RP card: Str): Any {
|
||||
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 {
|
||||
status = CardStatus.NORMAL
|
||||
@@ -82,14 +83,14 @@ class BotController(
|
||||
secret.checkSecret()
|
||||
|
||||
// 1. Check if the card exist
|
||||
var cards = listOfNotNull(
|
||||
us.cardRepo.findByLuid(cardId)(),
|
||||
var cards: MutableList<Card> = listOfNotNull(
|
||||
us.cardRepo.findByLuid(cardId),
|
||||
).mut
|
||||
|
||||
cardId.toLongOrNull()?.let {
|
||||
cards += listOfNotNull(
|
||||
us.cardRepo.findById(it)(),
|
||||
us.cardRepo.findByExtId(it)(),
|
||||
us.cardRepo.findByExtId(it),
|
||||
)
|
||||
|
||||
cards += listOfNotNull(
|
||||
@@ -110,8 +111,8 @@ class BotController(
|
||||
|
||||
return cards.map { card ->
|
||||
// Find all games played by this card
|
||||
val chu3 = chu3Db.userData.findByCard_ExtId(card.extId)()
|
||||
val mai2 = mai2Db.userData.findByCard_ExtId(card.extId)()
|
||||
val chu3 = chu3Db.userData.findByCard_ExtId(card.extId)
|
||||
val mai2 = mai2Db.userData.findByCard_ExtId(card.extId)
|
||||
val gamesDict = listOfNotNull(chu3, mai2).map {
|
||||
// Find the keychip owner
|
||||
val keychip = it.lastClientId
|
||||
|
||||
@@ -8,6 +8,7 @@ import icu.samnyan.aqua.net.games.IUserData
|
||||
import icu.samnyan.aqua.net.utils.AquaNetProps
|
||||
import icu.samnyan.aqua.net.utils.SUCCESS
|
||||
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.model.Card
|
||||
import icu.samnyan.aqua.sega.general.service.CardService
|
||||
@@ -19,7 +20,6 @@ import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.random.Random
|
||||
|
||||
@RestController
|
||||
@@ -30,7 +30,8 @@ class CardController(
|
||||
val cardService: CardService,
|
||||
val cardGameService: CardGameService,
|
||||
val cardRepository: CardRepository,
|
||||
val props: AquaNetProps
|
||||
val props: AquaNetProps,
|
||||
val fedy: Fedy
|
||||
) {
|
||||
companion object {
|
||||
val log = logger()
|
||||
@@ -80,10 +81,12 @@ class CardController(
|
||||
val id = cardService.sanitizeCardId(cardId)
|
||||
|
||||
// Create a new card
|
||||
cardService.registerByAccessCode(id, u)
|
||||
val newCard = cardService.registerByAccessCode(id, u)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -98,6 +101,8 @@ class CardController(
|
||||
val games = migrate.split(',')
|
||||
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()}")
|
||||
|
||||
SUCCESS
|
||||
@@ -115,10 +120,14 @@ class CardController(
|
||||
// Ghost cards cannot be unlinked
|
||||
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
|
||||
|
||||
val luid = card.luid
|
||||
|
||||
// Unbind the card
|
||||
card.aquaUser = null
|
||||
async { cardRepository.save(card) }
|
||||
|
||||
fedy.onCardUnlinked(luid)
|
||||
|
||||
log.info("Net /card/unlink : Unlinked card ${card.id} from user ${u.username}")
|
||||
|
||||
SUCCESS
|
||||
@@ -136,7 +145,7 @@ class CardController(
|
||||
*
|
||||
* Assumption: The card is already linked to the user.
|
||||
*/
|
||||
suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card): Bool {
|
||||
suspend fun <T : IUserData> migrateCard(gameName: Str, repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card): Bool {
|
||||
val ghost = card.aquaUser!!.ghostCard
|
||||
|
||||
// Check if data already exists in the user's ghost card
|
||||
@@ -144,7 +153,7 @@ suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, cardRepo:
|
||||
// Create a new dummy card for deleted data
|
||||
it.card = async {
|
||||
cardRepo.save(Card().apply {
|
||||
luid = "Migrated data of ghost card ${ghost.id} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()}"
|
||||
luid = "Migrated data of ghost card ${ghost.id} 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()
|
||||
@@ -162,6 +171,23 @@ suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, cardRepo:
|
||||
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>? {
|
||||
val data = async { repo.findByCard(card) } ?: return null
|
||||
return mapOf(
|
||||
@@ -177,10 +203,11 @@ class CardGameService(
|
||||
val chusan: Chu3UserDataRepo,
|
||||
val wacca: WcUserRepo,
|
||||
val ongeki: OgkUserDataRepo,
|
||||
val diva: icu.samnyan.aqua.sega.diva.dao.userdata.PlayerProfileRepository,
|
||||
val diva: PlayerProfileRepository,
|
||||
val safety: AquaNetSafetyService,
|
||||
val cardRepo: CardRepository,
|
||||
val em: EntityManager
|
||||
val em: EntityManager,
|
||||
val cardService: CardService
|
||||
) {
|
||||
companion object {
|
||||
val log = logger()
|
||||
@@ -189,18 +216,22 @@ class CardGameService(
|
||||
suspend fun migrate(crd: Card, games: List<String>) = async {
|
||||
// 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
|
||||
val dataRepos = mapOf(
|
||||
"mai2" to maimai2,
|
||||
"chu3" to chusan,
|
||||
"ongeki" to ongeki,
|
||||
"wacca" to wacca,
|
||||
)
|
||||
val remainingGames = dataRepos.keys.toMutableSet()
|
||||
games.forEach { game ->
|
||||
when (game) {
|
||||
"mai2" -> migrateCard(maimai2, cardRepo, crd)
|
||||
"chu3" -> migrateCard(chusan, cardRepo, crd)
|
||||
"ongeki" -> migrateCard(ongeki, cardRepo, crd)
|
||||
"wacca" -> migrateCard(wacca, cardRepo, crd)
|
||||
// TODO: diva
|
||||
// "diva" -> diva.findByPdId(card.extId.toInt()).getOrNull()?.let {
|
||||
// it.pdId = card.aquaUser!!.ghostCard
|
||||
// }
|
||||
}
|
||||
val dataRepo = dataRepos[game] ?: return@forEach
|
||||
if (migrateCard(game, dataRepo, cardRepo, crd))
|
||||
// Update timestamp for the ghost card (data migrated in)
|
||||
cardService.updateCardTimestamp(crd.aquaUser!!.ghostCard, game, resetCreatedAt = true)
|
||||
remainingGames.remove(game)
|
||||
}
|
||||
// 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 {
|
||||
@@ -209,7 +240,7 @@ class CardGameService(
|
||||
"chu3" to getSummaryFor(chusan, card),
|
||||
"ongeki" to getSummaryFor(ongeki, card),
|
||||
"wacca" to getSummaryFor(wacca, card),
|
||||
"diva" to diva.findByPdId(card.extId).getOrNull()?.let {
|
||||
"diva" to diva.findByPdId(card.extId)()?.let {
|
||||
mapOf(
|
||||
"name" to it.playerName,
|
||||
"rating" to it.level,
|
||||
|
||||
370
src/main/java/icu/samnyan/aqua/net/Fedy.kt
Normal file
370
src/main/java/icu/samnyan/aqua/net/Fedy.kt
Normal file
@@ -0,0 +1,370 @@
|
||||
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
|
||||
@@ -19,7 +19,8 @@ class FrontierProps {
|
||||
@API("/api/v2/frontier")
|
||||
class Frontier(
|
||||
val cardService: CardService,
|
||||
val props: FrontierProps
|
||||
val props: FrontierProps,
|
||||
val fedy: Fedy
|
||||
) {
|
||||
fun Str.checkFtk() {
|
||||
if (this != props.ftk) 403 - "Invalid FTK"
|
||||
@@ -32,9 +33,12 @@ class Frontier(
|
||||
|
||||
if (accessCode.length != 20) 400 - "Invalid access code"
|
||||
// if (!accessCode.startsWith("9900")) 400 - "Frontier access code must start with 9900"
|
||||
if (async { cardService.cardRepo.findByLuid(accessCode) }.isPresent) 400 - "Card already registered"
|
||||
if (async { cardService.cardRepo.findByLuid(accessCode) } != null) 400 - "Card already registered"
|
||||
|
||||
val card = async { cardService.registerByAccessCode(accessCode) }
|
||||
|
||||
fedy.onCardCreated(accessCode, card.extId)
|
||||
|
||||
return mapOf(
|
||||
"card" to card,
|
||||
"id" to card.extId // Expose hidden ID
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package icu.samnyan.aqua.net
|
||||
|
||||
import ext.JACKSON
|
||||
import ext.invoke
|
||||
import ext.logger
|
||||
import ext.parse
|
||||
import icu.samnyan.aqua.net.db.AquaNetUserRepo
|
||||
@@ -24,7 +23,7 @@ class Migrations(
|
||||
|
||||
@PostConstruct
|
||||
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 old = p.size
|
||||
|
||||
@@ -47,7 +46,7 @@ class Migrations(
|
||||
if (c.extId > max) {
|
||||
var new = c.extId and max
|
||||
log.info("Removing signed bit: {${c.extId} -> $new} for ${c.luid}")
|
||||
while (cardRepo.findByExtId(new).isPresent) {
|
||||
while (cardRepo.findByExtId(new) != null) {
|
||||
log.error("> Conflicting card found for ${c.luid}: $new")
|
||||
new++
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import kotlin.reflect.jvm.jvmErasure
|
||||
class SettingsApi(
|
||||
val us: AquaUserServices,
|
||||
val userRepo: AquaNetUserRepo,
|
||||
val goRepo: AquaGameOptionsRepo
|
||||
val goRepo: AquaGameOptionsRepo,
|
||||
val fedy: Fedy
|
||||
) {
|
||||
// Get all params with SettingField annotation
|
||||
val fields = AquaGameOptions::class.vars()
|
||||
@@ -41,6 +42,6 @@ class SettingsApi(
|
||||
}
|
||||
// Check field type
|
||||
field.setCast(options, value)
|
||||
goRepo.save(options)
|
||||
goRepo.save(options).also { fedy.onUserUpdated(u) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,20 @@ package icu.samnyan.aqua.net
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.net.components.*
|
||||
import icu.samnyan.aqua.net.db.*
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices.Companion.SETTING_FIELDS
|
||||
import icu.samnyan.aqua.net.db.AquaNetUserRepo
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
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.SUCCESS
|
||||
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.service.CardService
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.io.path.writeBytes
|
||||
|
||||
@RestController
|
||||
@@ -26,11 +25,12 @@ class UserRegistrar(
|
||||
val hasher: PasswordEncoder,
|
||||
val turnstileService: TurnstileService,
|
||||
val emailService: EmailService,
|
||||
val fedy: Fedy,
|
||||
val geoIP: GeoIP,
|
||||
val jwt: JWT,
|
||||
val confirmationRepo: EmailConfirmationRepo,
|
||||
val resetPasswordRepo: ResetPasswordRepo,
|
||||
val cardRepo: CardRepository,
|
||||
val cardService: CardService,
|
||||
val validator: AquaUserServices,
|
||||
val emailProps: EmailProperties,
|
||||
final val paths: PathProps
|
||||
@@ -66,29 +66,7 @@ class UserRegistrar(
|
||||
val country = geoIP.getCountry(ip)
|
||||
|
||||
// Create user
|
||||
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)
|
||||
}
|
||||
val u = async { validator.create(username, email, password, country) }
|
||||
|
||||
// Send confirmation email
|
||||
emailService.sendConfirmation(u)
|
||||
@@ -112,8 +90,6 @@ class UserRegistrar(
|
||||
?: (400 - "User not found")
|
||||
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
|
||||
if (!user.emailConfirmed && emailProps.enable) {
|
||||
// Check if last confirmation email was sent within a minute
|
||||
@@ -144,6 +120,70 @@ class UserRegistrar(
|
||||
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")
|
||||
@Doc("Confirm email address with a token sent through email to the user.", "Success message")
|
||||
suspend fun confirmEmail(@RP token: Str): Any {
|
||||
@@ -158,8 +198,13 @@ class UserRegistrar(
|
||||
// Check if the token is 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
|
||||
async { userRepo.save(confirmation.aquaNetUser.apply { emailConfirmed = true }) }
|
||||
async { userRepo.save(u.apply { emailConfirmed = true }) }
|
||||
fedy.onUserUpdated(u, isNew = true)
|
||||
|
||||
return SUCCESS
|
||||
}
|
||||
@@ -176,16 +221,16 @@ class UserRegistrar(
|
||||
@API("/setting")
|
||||
@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 ->
|
||||
// Check if the key is a settable field
|
||||
val field = SETTING_FIELDS.find { it.name == key } ?: (400 - "Invalid setting")
|
||||
|
||||
async {
|
||||
// Set the validated field
|
||||
field.setter.call(u, field.checker.call(validator, value))
|
||||
validator.update(u, key, value)
|
||||
|
||||
// Save the user
|
||||
userRepo.save(u)
|
||||
|
||||
// Clear all tokens if changing password
|
||||
if (key == "pwHash") validator.clearAllSessions(u)
|
||||
}
|
||||
fedy.onUserUpdated(u)
|
||||
|
||||
SUCCESS
|
||||
}
|
||||
@@ -224,6 +269,20 @@ class UserRegistrar(
|
||||
(portraitPath / name).writeBytes(bytes)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ package icu.samnyan.aqua.net.components
|
||||
import ext.Bool
|
||||
import ext.Str
|
||||
import ext.logger
|
||||
import icu.samnyan.aqua.net.db.AquaNetUser
|
||||
import icu.samnyan.aqua.net.db.EmailConfirmation
|
||||
import icu.samnyan.aqua.net.db.EmailConfirmationRepo
|
||||
import icu.samnyan.aqua.net.db.*
|
||||
import org.simplejavamail.api.mailer.Mailer
|
||||
import org.simplejavamail.email.EmailBuilder
|
||||
import org.simplejavamail.springsupport.SimpleJavaMailSpringSupport
|
||||
@@ -38,10 +36,13 @@ class EmailService(
|
||||
val mailer: Mailer,
|
||||
val props: EmailProperties,
|
||||
val confirmationRepo: EmailConfirmationRepo,
|
||||
val resetPasswordRepo: ResetPasswordRepo,
|
||||
) {
|
||||
val log = logger()
|
||||
val confirmTemplate: Str = this::class.java.getResource("/email/confirm.html")?.readText()
|
||||
?: throw Exception("Email Template Not Found")
|
||||
?: throw Exception("Email Confirm Template Not Found")
|
||||
val resetTemplate: Str = this::class.java.getResource("/email/reset.html")?.readText()
|
||||
?: throw Exception("Password Reset Template Not Found")
|
||||
|
||||
@Async
|
||||
@EventListener(ApplicationStartedEvent::class)
|
||||
@@ -69,15 +70,38 @@ class EmailService(
|
||||
confirmationRepo.save(confirmation)
|
||||
|
||||
// Send email
|
||||
log.info("Sending confirmation email to ${user.email}")
|
||||
log.info("Sending verification email to ${user.email}")
|
||||
mailer.sendMail(EmailBuilder.startingBlank()
|
||||
.from(props.senderName, props.senderAddr)
|
||||
.to(user.computedName, user.email)
|
||||
.withSubject("Confirm Your Email Address for AquaNet")
|
||||
.withSubject("Verify Your Email Address for AquaNet")
|
||||
.withHTMLText(confirmTemplate
|
||||
.replace("{{name}}", user.computedName)
|
||||
.replace("{{url}}", "https://${props.webHost}?confirm-email=$token"))
|
||||
.buildEmail()).thenRun { log.info("Confirmation email sent to ${user.email}") }
|
||||
.replace("{{url}}", "https://${props.webHost}/verify?code=$token"))
|
||||
.buildEmail()).thenRun { log.info("Verification 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) {
|
||||
|
||||
@@ -2,16 +2,17 @@ package icu.samnyan.aqua.net.components
|
||||
|
||||
import ext.Str
|
||||
import ext.minus
|
||||
import icu.samnyan.aqua.net.db.AquaNetUser
|
||||
import icu.samnyan.aqua.net.db.AquaNetUserRepo
|
||||
import icu.samnyan.aqua.net.db.*
|
||||
import io.jsonwebtoken.JwtParser
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.transaction.Transactional
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@@ -24,7 +25,8 @@ class JWTProperties {
|
||||
@Service
|
||||
class JWT(
|
||||
val props: JWTProperties,
|
||||
val userRepo: AquaNetUserRepo
|
||||
val userRepo: AquaNetUserRepo,
|
||||
val sessionRepo: SessionTokenRepo
|
||||
) {
|
||||
val log = LoggerFactory.getLogger(JWT::class.java)!!
|
||||
lateinit var key: SecretKey
|
||||
@@ -55,20 +57,53 @@ class JWT(
|
||||
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)
|
||||
|
||||
fun gen(user: AquaNetUser): Str = Jwts.builder().header()
|
||||
return Jwts.builder().header()
|
||||
.keyId("aqua-net")
|
||||
.and()
|
||||
.subject(user.auId.toString())
|
||||
.subject(token.token)
|
||||
.issuedAt(Date())
|
||||
.signWith(key)
|
||||
.compact()
|
||||
}
|
||||
|
||||
fun parse(token: Str): AquaNetUser? = try {
|
||||
userRepo.findByAuId(parser.parseSignedClaims(token).payload.subject.toLong())
|
||||
@Transactional
|
||||
fun parse(token: Str): AquaNetUser? {
|
||||
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) {
|
||||
log.debug("Failed to parse JWT", e)
|
||||
null
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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>
|
||||
}
|
||||
@@ -2,10 +2,7 @@ package icu.samnyan.aqua.net.db
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import ext.SettingField
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
@Entity
|
||||
@@ -14,21 +11,29 @@ class AquaGameOptions(
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@SettingField("general")
|
||||
var unlockMusic: Boolean = false,
|
||||
|
||||
@SettingField("general")
|
||||
var unlockChara: Boolean = false,
|
||||
|
||||
@SettingField("general")
|
||||
var unlockCollectables: Boolean = false,
|
||||
|
||||
@SettingField("general")
|
||||
var unlockTickets: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_music")
|
||||
var mai2UnlockMusic: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_chara")
|
||||
var mai2UnlockChara: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_chara_max_level")
|
||||
var mai2UnlockCharaMaxLevel: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_partners")
|
||||
var mai2UnlockPartners: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_collectables")
|
||||
var mai2UnlockCollectables: Boolean = false,
|
||||
@SettingField("mai2") @Column(name = "mai2_unlock_tickets")
|
||||
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")
|
||||
var waccaInfiniteWp: Boolean = false,
|
||||
|
||||
@SettingField("wacca")
|
||||
var waccaAlwaysVip: Boolean = false,
|
||||
|
||||
|
||||
33
src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt
Normal file
33
src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
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>
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package icu.samnyan.aqua.net.db
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
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.sega.allnet.AllNetProps
|
||||
import icu.samnyan.aqua.sega.allnet.KeyChipRepo
|
||||
@@ -9,11 +11,13 @@ import icu.samnyan.aqua.sega.allnet.KeychipSession
|
||||
import icu.samnyan.aqua.sega.general.GameMusicPopularity
|
||||
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 jakarta.persistence.*
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.Serializable
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.KMutableProperty
|
||||
@@ -43,6 +47,10 @@ class AquaNetUser(
|
||||
@Column(length = 3)
|
||||
var country: String = "",
|
||||
|
||||
// Region code at most 2 characters
|
||||
@Column(length = 2)
|
||||
var region: String = "",
|
||||
|
||||
// Last login time
|
||||
var lastLogin: Long = 0L,
|
||||
|
||||
@@ -98,6 +106,7 @@ interface AquaNetUserRepo : JpaRepository<AquaNetUser, Long> {
|
||||
fun findByEmailIgnoreCase(email: String): AquaNetUser?
|
||||
fun findByUsernameIgnoreCase(username: String): AquaNetUser?
|
||||
fun findByKeychip(keychip: String): AquaNetUser?
|
||||
fun findByGhostCardExtId(extId: Long): AquaNetUser?
|
||||
}
|
||||
|
||||
data class SettingField(
|
||||
@@ -119,7 +128,9 @@ class AquaUserServices(
|
||||
val allNetProps: AllNetProps,
|
||||
val jwt: JWT,
|
||||
val em: EntityManager,
|
||||
val pop: GameMusicPopularity
|
||||
val pop: GameMusicPopularity,
|
||||
val cardService: CardService,
|
||||
val sessionRepo: SessionTokenRepo,
|
||||
) {
|
||||
companion object {
|
||||
val SETTING_FIELDS = AquaUserServices::class.functions
|
||||
@@ -131,12 +142,49 @@ 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) =
|
||||
async { userRepo.findByUsernameIgnoreCase(username) }?.let { callback(it) } ?: (404 - "User not found")
|
||||
|
||||
suspend fun cardByName(username: Str) =
|
||||
if (username.startsWith("user")) username.substring(4).toLongOrNull()
|
||||
?.let { cardRepo.findById(it).getOrNull() } ?: (404 - "Card not found")
|
||||
?.let { cardRepo.findById(it)() } ?: (404 - "Card not found")
|
||||
else byName(username) { it.ghostCard }
|
||||
|
||||
suspend fun <T> cardByName(username: Str, callback: suspend (Card) -> T) = callback(cardByName(username))
|
||||
@@ -168,7 +216,7 @@ class AquaUserServices(
|
||||
400 - "User with username `$this` already exists"
|
||||
}
|
||||
|
||||
fun checkEmail(email: Str) = email.apply {
|
||||
fun validateEmail(email: Str) = email.apply {
|
||||
// Check if email is valid
|
||||
if (!isValidEmail()) 400 - "Invalid email"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import icu.samnyan.aqua.net.BotProps
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
import icu.samnyan.aqua.net.utils.SUCCESS
|
||||
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 jakarta.annotation.PostConstruct
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
@@ -27,9 +27,12 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
|
||||
abstract val playlogRepo: GenericPlaylogRepo<*>
|
||||
abstract val userMusicRepo: GenericUserMusicRepo<*>
|
||||
abstract val shownRanks: List<Pair<Int, String>>
|
||||
|
||||
abstract val settableFields: Map<String, (T, String) -> Unit>
|
||||
open val gettableFields: Set<String> = setOf()
|
||||
|
||||
@Autowired lateinit var cardService: CardService
|
||||
|
||||
@API("trend")
|
||||
abstract suspend fun trend(@RP username: String): List<TrendOut>
|
||||
@API("user-summary")
|
||||
@@ -56,7 +59,7 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
|
||||
|
||||
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
|
||||
if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned } && u.ghostCard.status == CardStatus.NORMAL) null
|
||||
if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned } && u.ghostCard.status.isNormal) null
|
||||
else u
|
||||
}
|
||||
|
||||
@@ -92,7 +95,7 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
|
||||
AVG(p.achievement) / 10000.0 AS acc,
|
||||
SUM(p.is_full_combo) AS fc,
|
||||
SUM(p.is_all_perfect) AS ap,
|
||||
c.ranking_banned or a.opt_out_of_leaderboard AS hide,
|
||||
c.ranking_banned or a.opt_out_of_leaderboard or c.status = 12 AS hide,
|
||||
a.username
|
||||
FROM ${tableName}_user_playlog_view p
|
||||
JOIN ${tableName}_user_data_view u ON p.user_id = u.id
|
||||
@@ -118,19 +121,17 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
|
||||
}
|
||||
|
||||
@API("playlog")
|
||||
fun playlog(@RP id: Long): IGenericGamePlaylog = playlogRepo.findById(id).getOrNull() ?: (404 - "Playlog not found")
|
||||
fun playlog(@RP id: Long): IGenericGamePlaylog = playlogRepo.findById(id)() ?: (404 - "Playlog not found")
|
||||
|
||||
val userDetailFields by lazy { userDataClass.gettersMap().let { vm ->
|
||||
(settableFields.keys.toSet() + gettableFields)
|
||||
.associateWith { k -> (vm[k] ?: error("Field $k not found")) }
|
||||
} }
|
||||
|
||||
@API("user-detail")
|
||||
suspend fun userDetail(@RP username: String) = us.cardByName(username) { card ->
|
||||
val u = userDataRepo.findByCard(card) ?: (404 - "User not found")
|
||||
userDetailFields.toList().associate { (k, f) -> k to f.invoke(u) }
|
||||
}
|
||||
|
||||
@API("user-detail-set")
|
||||
suspend fun userDetailSet(@RP token: String, @RP field: String, @RP value: String): Any {
|
||||
val prop = settableFields[field] ?: (400 - "Invalid field $field")
|
||||
@@ -139,16 +140,22 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
|
||||
val user = async { userDataRepo.findByCard(u.ghostCard) } ?: (404 - "User not found")
|
||||
prop(user, value)
|
||||
async { userDataRepo.save(user) }
|
||||
cardService.updateCardTimestamp(u.ghostCard, name)
|
||||
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")
|
||||
suspend fun userMusicFromList(@RP username: Str, @RB musicList: List<Int>) = us.cardByName(username) { card ->
|
||||
userMusicRepo.findByUser_Card_ExtIdAndMusicIdIn(card.extId, musicList)
|
||||
}
|
||||
|
||||
fun genericUserSummary(card: Card, ratingComp: Map<String, String>, rival: Boolean? = null): GenericGameSummary {
|
||||
fun genericUserSummary(card: Card, ratingComp: Map<String, String>, rival: Boolean? = null, favorites: List<Int>? = null): GenericGameSummary {
|
||||
// Summary values: total plays, player rating, server-wide ranking
|
||||
// number of each rank, max combo, number of full combo, number of all perfect
|
||||
val user = userDataRepo.findByCard(card) ?: (404 - "Game data not found")
|
||||
@@ -199,7 +206,8 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
|
||||
ratingComposition = ratingComp,
|
||||
recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(100).reversed(),
|
||||
lastPlayedHost = user.lastClientId?.let { us.userRepo.findByKeychip(it)?.username },
|
||||
rival = rival
|
||||
rival = rival,
|
||||
favorites = favorites
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import java.time.LocalDate
|
||||
const val LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||
"abcdefghijklmnopqrstuvwxyz" +
|
||||
"0123456789"
|
||||
const val SYMBOLS = "・:;?!~/+-×÷=♂♀∀#&*@☆○◎◇□△▽♪†‡ΣαβγθφψωДё$()._␣"
|
||||
const val SYMBOLS = "・:;?!~/+-×÷=♂♀∀#&*@☆○◎◇□△▽♪†‡ΣαβγθφψωДё$()._ "
|
||||
const val KANA = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん" +
|
||||
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン"
|
||||
const val SEGA_USERNAME_CAHRS = LETTERS + SYMBOLS + KANA
|
||||
const val SEGA_USERNAME_CHARS = LETTERS + SYMBOLS + KANA
|
||||
const val WACCA_USERNAME_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||
"abcdefghijklmnopqrstuvwxyz" +
|
||||
"0123456789" +
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
package icu.samnyan.aqua.net.games
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.net.Fedy
|
||||
import icu.samnyan.aqua.net.db.AquaNetUser
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
import icu.samnyan.aqua.net.utils.AquaNetProps
|
||||
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.context.annotation.Lazy
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.repository.NoRepositoryBean
|
||||
import org.springframework.transaction.PlatformTransactionManager
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.writeText
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
data class ExportOptions(
|
||||
val playlogAfter: String? = null
|
||||
)
|
||||
|
||||
// Import class with renaming
|
||||
data class ImportClass<T : Any>(
|
||||
val type: KClass<T>,
|
||||
@@ -36,13 +43,14 @@ interface IExportClass<UserModel: IUserData> {
|
||||
@NoRepositoryBean
|
||||
interface IUserRepo<UserModel, ThisModel>: JpaRepository<ThisModel, Long> {
|
||||
fun findByUser(user: UserModel): List<ThisModel>
|
||||
fun findSingleByUser(user: UserModel): Optional<ThisModel>
|
||||
fun findSingleByUser(user: UserModel): ThisModel?
|
||||
}
|
||||
|
||||
/**
|
||||
* Import controller for a game
|
||||
*
|
||||
* @param game: 4-letter Game ID
|
||||
* @param gameName: mai2/chu3/ongeki
|
||||
* @param exportFields: Mapping of type names to variables in the export model
|
||||
* (e.g. "Mai2UserCharacter" -> Mai2DataExport::userCharacterList)
|
||||
* @param exportRepos: Mapping of variables to repositories that can be used to find the data
|
||||
@@ -50,10 +58,13 @@ interface IUserRepo<UserModel, ThisModel>: JpaRepository<ThisModel, Long> {
|
||||
*/
|
||||
abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel: IUserData>(
|
||||
val game: String,
|
||||
val gameName: String,
|
||||
val exportClass: KClass<ExportModel>,
|
||||
val exportFields: Map<String, Var<ExportModel, Any>>,
|
||||
val exportRepos: Map<Var<ExportModel, Any>, IUserRepo<UserModel, *>>,
|
||||
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 val userDataRepo: GenericUserDataRepo<UserModel>
|
||||
@@ -62,6 +73,7 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
|
||||
@Autowired lateinit var netProps: AquaNetProps
|
||||
@Autowired lateinit var transManager: PlatformTransactionManager
|
||||
val trans by lazy { TransactionTemplate(transManager) }
|
||||
@Autowired lateinit var cardService: CardService
|
||||
|
||||
init {
|
||||
artemisRenames.values.forEach {
|
||||
@@ -72,12 +84,17 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
|
||||
val listRepos = exportRepos.filter { it.key returns List::class }
|
||||
val singleRepos = exportRepos.filter { !(it.key returns List::class) }
|
||||
|
||||
fun export(u: AquaNetUser) = createEmpty().apply {
|
||||
fun export(u: AquaNetUser): ExportModel = export(u.ghostCard, ExportOptions())
|
||||
|
||||
fun export(c: Card, options: ExportOptions) = createEmpty().apply {
|
||||
gameId = game
|
||||
userData = userDataRepo.findByCard(u.ghostCard) ?: (404 - "User not found")
|
||||
userData = userDataRepo.findByCard(c) ?: (404 - "User not found")
|
||||
exportRepos.forEach { (f, u) ->
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +112,7 @@ 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 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
|
||||
// Check that all ids are 0 (this should be true since all ids are @JsonIgnore)
|
||||
@@ -126,7 +144,13 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
|
||||
// Save new data
|
||||
singles.forEach { (repo, single) -> (repo as IUserRepo<UserModel, Any>).save(single) }
|
||||
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
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ data class GenericGameSummary(
|
||||
|
||||
val recent: List<IGenericGamePlaylog>,
|
||||
|
||||
val rival: Boolean?
|
||||
val rival: Boolean?,
|
||||
val favorites: List<Int>?
|
||||
)
|
||||
|
||||
data class GenericRankingPlayer(
|
||||
@@ -124,7 +125,7 @@ open class BaseEntity(
|
||||
@NoRepositoryBean
|
||||
interface GenericUserDataRepo<T : IUserData> : JpaRepository<T, Long> {
|
||||
fun findByCard(card: Card): T?
|
||||
fun findByCard_ExtId(extId: Long): Optional<T>
|
||||
fun findByCard_ExtId(extId: Long): T?
|
||||
|
||||
@Query("select e from #{#entityName} e where e.card.rankingBanned = false")
|
||||
fun findAllNonBanned(): List<T>
|
||||
|
||||
@@ -17,7 +17,7 @@ import kotlin.reflect.full.declaredMembers
|
||||
class Chu3Import(
|
||||
val repos: Chu3Repos,
|
||||
) : ImportController<Chu3DataExport, Chu3UserData>(
|
||||
"SDHD", Chu3DataExport::class,
|
||||
"SDHD", "chu3", Chu3DataExport::class,
|
||||
exportFields = Chu3DataExport::class.vars().associateBy {
|
||||
it.name.replace("List", "").lowercase()
|
||||
},
|
||||
|
||||
@@ -3,10 +3,18 @@ package icu.samnyan.aqua.net.games.chu3
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
import icu.samnyan.aqua.net.games.*
|
||||
import icu.samnyan.aqua.net.utils.*
|
||||
import icu.samnyan.aqua.sega.chusan.model.*
|
||||
import icu.samnyan.aqua.net.utils.chu3Scores
|
||||
import icu.samnyan.aqua.sega.chusan.model.Chu3Repos
|
||||
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.UserGameOption
|
||||
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
|
||||
@API("api/v2/game/chu3")
|
||||
@@ -25,7 +33,7 @@ class Chusan(
|
||||
// Only show > AAA rank
|
||||
override val shownRanks = chu3Scores.filter { it.first >= 95 * 10000 }
|
||||
override val settableFields: Map<String, (Chu3UserData, String) -> Unit> by lazy { mapOf(
|
||||
"userName" to usernameCheck(SEGA_USERNAME_CAHRS),
|
||||
"userName" to usernameCheck(SEGA_USERNAME_CHARS),
|
||||
"nameplateId" to { u, v -> u.nameplateId = v.int },
|
||||
"frameId" to { u, v -> u.frameId = v.int },
|
||||
"trophyId" to { u, v -> u.trophyId = v.int },
|
||||
@@ -33,6 +41,7 @@ class Chusan(
|
||||
"trophyIdSub2" to { u, v -> u.trophyIdSub2 = v.int },
|
||||
"mapIconId" to { u, v -> u.mapIconId = 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 },
|
||||
"avatarHead" to { u, v -> u.avatarHead = v.int },
|
||||
"avatarFace" to { u, v -> u.avatarFace = v.int },
|
||||
@@ -44,7 +53,7 @@ class Chusan(
|
||||
"lastRomVersion" to { u, v -> u.lastRomVersion = v },
|
||||
"lastDataVersion" to { u, v -> u.lastDataVersion = v },
|
||||
) }
|
||||
override val gettableFields: Set<String> = setOf("level", "playerRating", "characterId")
|
||||
override val gettableFields: Set<String> = setOf("level", "playerRating")
|
||||
|
||||
override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card ->
|
||||
// Summary values: total plays, player rating, server-wide ranking
|
||||
@@ -60,7 +69,9 @@ class Chusan(
|
||||
"new" to (extra["rating_new_list"] ?: ""),
|
||||
)
|
||||
|
||||
genericUserSummary(card, ratingComposition)
|
||||
val misc = rp.userMisc.findByUser_Card_ExtId(card.extId).firstOrNull()
|
||||
|
||||
genericUserSummary(card, ratingComposition, null, misc?.favMusic)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +103,23 @@ 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
|
||||
@API("user-box")
|
||||
fun userBox(@RP token: String) = us.jwt.auth(token) {
|
||||
|
||||
@@ -3,30 +3,38 @@ package icu.samnyan.aqua.net.games.mai2
|
||||
import ext.API
|
||||
import ext.returns
|
||||
import ext.vars
|
||||
import icu.samnyan.aqua.net.games.ExportOptions
|
||||
import icu.samnyan.aqua.net.games.IExportClass
|
||||
import icu.samnyan.aqua.net.games.ImportClass
|
||||
import icu.samnyan.aqua.net.games.ImportController
|
||||
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
|
||||
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 org.springframework.web.bind.annotation.RestController
|
||||
import kotlin.reflect.full.declaredMembers
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@RestController
|
||||
@API("api/v2/game/mai2")
|
||||
class Mai2Import(
|
||||
val repos: Mai2Repos,
|
||||
) : ImportController<Maimai2DataExport, Mai2UserDetail>(
|
||||
"SDEZ", Maimai2DataExport::class,
|
||||
"SDEZ", "mai2", Maimai2DataExport::class,
|
||||
exportFields = Maimai2DataExport::class.vars().associateBy {
|
||||
it.name.replace("List", "").lowercase()
|
||||
},
|
||||
exportRepos = Maimai2DataExport::class.vars()
|
||||
.filter { f -> f.name !in setOf("gameId", "userData") }
|
||||
.associateWith { Mai2Repos::class.declaredMembers
|
||||
.filter { f -> f.name !in setOf("gameId", "userData", "userPlaylogList", "userFavoriteMusicList") }
|
||||
.associateWith { field ->
|
||||
val repoName = when (field.name) {
|
||||
"userKaleidxScopeList" -> "userKaleidx"
|
||||
else -> field.name.replace("List", "")
|
||||
}
|
||||
Mai2Repos::class.declaredMembers
|
||||
.filter { f -> f returns Mai2UserLinked::class }
|
||||
.firstOrNull { f -> f.name == it.name || f.name == it.name.replace("List", "") }
|
||||
?.call(repos) as Mai2UserLinked<*>? ?: error("No matching field found for ${it.name}")
|
||||
.firstOrNull { f -> f.name == repoName }
|
||||
?.call(repos) as Mai2UserLinked<*>? ?: error("No matching field found for ${field.name}")
|
||||
},
|
||||
artemisRenames = mapOf(
|
||||
"mai2_item_character" to ImportClass(Mai2UserCharacter::class),
|
||||
@@ -44,34 +52,71 @@ class Mai2Import(
|
||||
"mai2_profile_option" to ImportClass(Mai2UserOption::class, mapOf("version" to null)),
|
||||
"mai2_score_best" to ImportClass(Mai2UserMusicDetail::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 val userDataRepo = repos.userData
|
||||
}
|
||||
|
||||
data class Maimai2DataExport(
|
||||
override var userData: Mai2UserDetail,
|
||||
var userExtend: Mai2UserExtend,
|
||||
var userOption: Mai2UserOption,
|
||||
var userUdemae: Mai2UserUdemae,
|
||||
var mapEncountNpcList: List<Mai2MapEncountNpc>,
|
||||
var userActList: List<Mai2UserAct>,
|
||||
var userCharacterList: List<Mai2UserCharacter>,
|
||||
var userChargeList: List<Mai2UserCharge>,
|
||||
var userCourseList: List<Mai2UserCourse>,
|
||||
var userFavoriteList: List<Mai2UserFavorite>,
|
||||
var userFriendSeasonRankingList: List<Mai2UserFriendSeasonRanking>,
|
||||
var userGeneralDataList: List<Mai2UserGeneralData>,
|
||||
var userItemList: List<Mai2UserItem>,
|
||||
var userLoginBonusList: List<Mai2UserLoginBonus>,
|
||||
var userMapList: List<Mai2UserMap>,
|
||||
var userMusicDetailList: List<Mai2UserMusicDetail>,
|
||||
var userPlaylogList: List<Mai2UserPlaylog>,
|
||||
override var userData: Mai2UserDetail = Mai2UserDetail(),
|
||||
var userExtend: Mai2UserExtend = Mai2UserExtend(),
|
||||
var userOption: Mai2UserOption = Mai2UserOption(),
|
||||
var userUdemae: Mai2UserUdemae = Mai2UserUdemae(),
|
||||
var mapEncountNpcList: List<Mai2MapEncountNpc> = mutableListOf(),
|
||||
var userActList: List<Mai2UserAct> = mutableListOf(),
|
||||
var userCharacterList: List<Mai2UserCharacter> = mutableListOf(),
|
||||
var userChargeList: List<Mai2UserCharge> = mutableListOf(),
|
||||
var userCourseList: List<Mai2UserCourse> = mutableListOf(),
|
||||
var userFavoriteList: List<Mai2UserFavorite> = mutableListOf(),
|
||||
var userFriendSeasonRankingList: List<Mai2UserFriendSeasonRanking> = mutableListOf(),
|
||||
var userGeneralDataList: List<Mai2UserGeneralData> = mutableListOf(),
|
||||
var userItemList: List<Mai2UserItem> = mutableListOf(),
|
||||
var userLoginBonusList: List<Mai2UserLoginBonus> = mutableListOf(),
|
||||
var userMapList: List<Mai2UserMap> = mutableListOf(),
|
||||
var userMusicDetailList: List<Mai2UserMusicDetail> = mutableListOf(),
|
||||
var userIntimateList: List<Mai2UserIntimate> = mutableListOf(),
|
||||
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",
|
||||
): IExportClass<Mai2UserDetail> {
|
||||
constructor() : this(Mai2UserDetail(), Mai2UserExtend(), Mai2UserOption(), Mai2UserUdemae(),
|
||||
mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(),
|
||||
mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(),
|
||||
mutableListOf())
|
||||
}
|
||||
): IExportClass<Mai2UserDetail>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package icu.samnyan.aqua.net.games.mai2
|
||||
|
||||
import ext.*
|
||||
import ext.API
|
||||
import ext.RB
|
||||
import ext.RP
|
||||
import ext.minus
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
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.userdata.Mai2UserMusicDetail
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
@@ -13,32 +17,28 @@ import org.springframework.web.bind.annotation.RestController
|
||||
class Mai2MusicDetailImport(
|
||||
val us: AquaUserServices,
|
||||
val repos: Mai2Repos,
|
||||
val cardService: CardService,
|
||||
) {
|
||||
@PostMapping("import-music-detail")
|
||||
suspend fun importMusicDetail(@RP token: String, @RB data: List<Mai2UserMusicDetail>) = us.jwt.auth(token) { u ->
|
||||
us.cardByName(u.username) { card ->
|
||||
val user = repos.userData.findByCardExtId(card.extId).orElse(null) ?: (404 - "User not found")
|
||||
val user = repos.userData.findByCardExtId(card.extId) ?: (404 - "User not found")
|
||||
data.forEach { newMusic ->
|
||||
val musicRec = repos.userMusicDetail.findByUserAndMusicIdAndLevel(user, newMusic.musicId, newMusic.level)
|
||||
if (musicRec.isPresent) {
|
||||
val music = musicRec.get()
|
||||
newMusic.user = user
|
||||
repos.userMusicDetail.findByUserAndMusicIdAndLevel(user, newMusic.musicId, newMusic.level)?.let { m ->
|
||||
newMusic.apply {
|
||||
id = music.id
|
||||
this.user = user
|
||||
achievement = achievement.coerceAtLeast(music.achievement)
|
||||
scoreRank = scoreRank.coerceAtLeast(music.scoreRank)
|
||||
comboStatus = comboStatus.coerceAtLeast(music.comboStatus)
|
||||
syncStatus = syncStatus.coerceAtLeast(music.syncStatus)
|
||||
deluxscoreMax = deluxscoreMax.coerceAtLeast(music.deluxscoreMax)
|
||||
playCount = playCount.coerceAtLeast(music.playCount)
|
||||
}
|
||||
} else {
|
||||
newMusic.apply {
|
||||
this.user = user
|
||||
id = m.id
|
||||
achievement = achievement.coerceAtLeast(m.achievement)
|
||||
scoreRank = scoreRank.coerceAtLeast(m.scoreRank)
|
||||
comboStatus = comboStatus.coerceAtLeast(m.comboStatus)
|
||||
syncStatus = syncStatus.coerceAtLeast(m.syncStatus)
|
||||
deluxscoreMax = deluxscoreMax.coerceAtLeast(m.deluxscoreMax)
|
||||
playCount = playCount.coerceAtLeast(m.playCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
repos.userMusicDetail.saveAll(data)
|
||||
cardService.updateCardTimestamp(card, "mai2")
|
||||
SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,23 @@ package icu.samnyan.aqua.net.games.mai2
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
import icu.samnyan.aqua.net.games.*
|
||||
import icu.samnyan.aqua.net.utils.*
|
||||
import icu.samnyan.aqua.net.utils.SUCCESS
|
||||
import icu.samnyan.aqua.net.utils.mai2Scores
|
||||
import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPhotoHandler
|
||||
import icu.samnyan.aqua.sega.maimai2.model.*
|
||||
import icu.samnyan.aqua.sega.maimai2.model.userdata.*
|
||||
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
|
||||
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
|
||||
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.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.util.*
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
@RestController
|
||||
@API("api/v2/game/mai2")
|
||||
@@ -29,7 +38,7 @@ class Maimai2(
|
||||
// Only show > S rank
|
||||
override val shownRanks = mai2Scores.filter { it.first >= 97 * 10000 }
|
||||
override val settableFields: Map<String, (Mai2UserDetail, String) -> Unit> by lazy { mapOf(
|
||||
"userName" to usernameCheck(SEGA_USERNAME_CAHRS),
|
||||
"userName" to usernameCheck(SEGA_USERNAME_CHARS),
|
||||
"iconId" to { u, v -> u.iconId = v.int() },
|
||||
"plateId" to { u, v -> u.plateId = v.int() },
|
||||
"titleId" to { u, v -> u.titleId = v.int() },
|
||||
@@ -58,15 +67,15 @@ class Maimai2(
|
||||
us.jwt.auth(t) { u ->
|
||||
if (u.username == username) return@auth null
|
||||
us.cardByName(u.username) { myCard ->
|
||||
val user = repos.userData.findByCardExtId(card.extId).orElse(null) ?: (404 - "User not found")
|
||||
val myRival = repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival")
|
||||
.map { it.propertyValue.split(',') }.orElse(emptyList()).filter { it.isNotEmpty() }.map { it.long() }
|
||||
val user = repos.userData.findByCardExtId(card.extId) ?: (404 - "User not found")
|
||||
val myRival = (repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival")?.propertyValue?.split(',') ?: emptyList())
|
||||
.filter { it.isNotEmpty() }.map { it.long() }
|
||||
myRival.contains(user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
genericUserSummary(card, ratingComposition, isMyRival)
|
||||
genericUserSummary(card, ratingComposition, isMyRival, extra["favorite_music"]?.split(",")?.mapNotNull{it -> it.toIntOrNull()})
|
||||
}
|
||||
|
||||
@API("user-rating")
|
||||
@@ -110,6 +119,7 @@ class Maimai2(
|
||||
val user = userDataRepo.findByCard(card) ?: (404 - "User not found")
|
||||
user.userName = newNameFull
|
||||
userDataRepo.save(user)
|
||||
cardService.updateCardTimestamp(card, "mai2")
|
||||
}
|
||||
mapOf("newName" to newNameFull)
|
||||
}
|
||||
@@ -132,17 +142,36 @@ class Maimai2(
|
||||
if (loginBonus.none { it.bonusId == bonusId }) {
|
||||
// create one
|
||||
val newBonus = Mai2UserLoginBonus().apply {
|
||||
user = repos.userData.findByCardExtId(card.extId).orElse(null) ?: (404 - "User not found")
|
||||
user = repos.userData.findByCardExtId(card.extId) ?: (404 - "User not found")
|
||||
this.bonusId = bonusId
|
||||
isCurrent = true
|
||||
}
|
||||
loginBonus.add(newBonus)
|
||||
}
|
||||
repos.userLoginBonus.saveAll(loginBonus)
|
||||
cardService.updateCardTimestamp(card, "mai2")
|
||||
}
|
||||
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")
|
||||
suspend fun ownedItems(@RP token: String) = us.jwt.auth(token) { u ->
|
||||
us.cardByName(u.username) { card ->
|
||||
@@ -154,10 +183,10 @@ class Maimai2(
|
||||
suspend fun setRival(@RP token: String, @RP rivalUserName: String, @RP isAdd: Boolean) = us.jwt.auth(token) { u ->
|
||||
us.cardByName(u.username) { myCard ->
|
||||
val rivalCard = us.cardByName(rivalUserName) { it }
|
||||
val rivalUser = repos.userData.findByCardExtId(rivalCard.extId).orElse(null) ?: (404 - "User not found")
|
||||
val myRival = repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival").orElse(null)
|
||||
val rivalUser = repos.userData.findByCardExtId(rivalCard.extId) ?: (404 - "User not found")
|
||||
val myRival = repos.userGeneralData.findByUser_Card_ExtIdAndPropertyKey(myCard.extId, "favorite_rival")
|
||||
?: Mai2UserGeneralData().apply {
|
||||
user = repos.userData.findByCardExtId(myCard.extId).orElse(null) ?: (404 - "User not found")
|
||||
user = repos.userData.findByCardExtId(myCard.extId) ?: (404 - "User not found")
|
||||
propertyKey = "favorite_rival"
|
||||
}
|
||||
val myRivalList = myRival.propertyValue.split(',').filter { it.isNotEmpty() }.mut
|
||||
@@ -172,6 +201,7 @@ class Maimai2(
|
||||
|
||||
myRival.propertyValue = myRivalList.joinToString(",")
|
||||
repos.userGeneralData.save(myRival)
|
||||
cardService.updateCardTimestamp(myCard, "mai2")
|
||||
}
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package icu.samnyan.aqua.net.games.ongeki
|
||||
|
||||
import ext.API
|
||||
import ext.RP
|
||||
import ext.minus
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
import icu.samnyan.aqua.net.games.*
|
||||
import icu.samnyan.aqua.net.utils.*
|
||||
import icu.samnyan.aqua.net.utils.ongekiScores
|
||||
import icu.samnyan.aqua.sega.ongeki.OgkUserDataRepo
|
||||
import icu.samnyan.aqua.sega.ongeki.OgkUserGeneralDataRepo
|
||||
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.model.UserData
|
||||
import icu.samnyan.aqua.sega.ongeki.model.UserOption
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
@RestController
|
||||
@API("api/v2/game/ongeki")
|
||||
@@ -18,7 +25,8 @@ class Ongeki(
|
||||
override val playlogRepo: OgkUserPlaylogRepo,
|
||||
override val userDataRepo: OgkUserDataRepo,
|
||||
override val userMusicRepo: OgkUserMusicDetailRepo,
|
||||
val userGeneralDataRepository: OgkUserGeneralDataRepo
|
||||
val userGeneralDataRepository: OgkUserGeneralDataRepo,
|
||||
val userOptionRepo: OgkUserOptionRepo
|
||||
): GameApiController<UserData>("ongeki", UserData::class) {
|
||||
override suspend fun trend(username: String) = us.cardByName(username) { card ->
|
||||
findTrend(playlogRepo.findByUser_Card_ExtId(card.extId)
|
||||
@@ -27,7 +35,7 @@ class Ongeki(
|
||||
|
||||
override val shownRanks = ongekiScores.filter { it.first >= 950000 }
|
||||
override val settableFields: Map<String, (UserData, String) -> Unit> by lazy { mapOf(
|
||||
"userName" to usernameCheck(SEGA_USERNAME_CAHRS),
|
||||
"userName" to usernameCheck(SEGA_USERNAME_CHARS),
|
||||
|
||||
"lastRomVersion" to { u, v -> u.lastRomVersion = v },
|
||||
"lastDataVersion" to { u, v -> u.lastDataVersion = v },
|
||||
@@ -45,4 +53,21 @@ class Ongeki(
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package icu.samnyan.aqua.net.games.wacca
|
||||
|
||||
import ext.*
|
||||
import ext.API
|
||||
import ext.RP
|
||||
import ext.isoDate
|
||||
import ext.utc
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
import icu.samnyan.aqua.net.games.*
|
||||
import icu.samnyan.aqua.net.utils.waccaScores
|
||||
@@ -31,7 +34,10 @@ class Wacca(
|
||||
|
||||
override suspend fun userSummary(@RP username: String, @RP token: String?) = us.cardByName(username) { card ->
|
||||
// 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 }
|
||||
|
||||
@@ -3,10 +3,10 @@ package icu.samnyan.aqua.net.transfer
|
||||
import ext.header
|
||||
import ext.post
|
||||
import ext.request
|
||||
import java.net.URI
|
||||
import icu.samnyan.aqua.sega.aimedb.AimeDbClient
|
||||
import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder
|
||||
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})")
|
||||
|
||||
|
||||
@@ -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.ongeki.model.OngekiUpsertUserAll
|
||||
import icu.samnyan.aqua.sega.ongeki.model.UserItem
|
||||
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
|
||||
import icu.samnyan.aqua.sega.util.jackson.IMapper
|
||||
import icu.samnyan.aqua.sega.util.jackson.StringMapper
|
||||
import icu.samnyan.aqua.sega.util.BasicMapper
|
||||
import icu.samnyan.aqua.sega.util.IMapper
|
||||
import icu.samnyan.aqua.sega.util.StringMapper
|
||||
|
||||
|
||||
abstract class DataBroker(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package icu.samnyan.aqua.sega.aimedb
|
||||
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.net.BotProps
|
||||
import ext.logger
|
||||
import ext.toHex
|
||||
import icu.samnyan.aqua.net.Fedy
|
||||
import icu.samnyan.aqua.net.db.AquaUserServices
|
||||
import icu.samnyan.aqua.sega.allnet.AllNetProps
|
||||
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 io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.ByteBufUtil
|
||||
@@ -26,6 +26,7 @@ class AimeDB(
|
||||
val cardService: CardService,
|
||||
val us: AquaUserServices,
|
||||
val allNetProps: AllNetProps,
|
||||
val fedy: Fedy,
|
||||
): ChannelInboundHandlerAdapter() {
|
||||
val logger = logger()
|
||||
|
||||
@@ -68,9 +69,9 @@ class AimeDB(
|
||||
*/
|
||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||
if (msg !is Map<*, *>) return
|
||||
try {
|
||||
val type = msg["type"] as Int
|
||||
val data = msg["data"] as ByteBuf
|
||||
try {
|
||||
val base = data.decodeHeader()
|
||||
val handler = handlers[type] ?: return logger.error("AimeDB: Unknown request type 0x${type.toString(16)}")
|
||||
|
||||
@@ -89,6 +90,7 @@ class AimeDB(
|
||||
|
||||
handler.fn(data)?.let { ctx.write(it) }
|
||||
} finally {
|
||||
data.release()
|
||||
ctx.flush()
|
||||
ctx.close()
|
||||
}
|
||||
@@ -124,7 +126,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
|
||||
us.cardRepo.save(card.apply { accessTime = LocalDateTime.now() }).extId
|
||||
} ?: -1
|
||||
@@ -195,11 +197,13 @@ class AimeDB(
|
||||
var status = 0
|
||||
var aimeId = 0L
|
||||
|
||||
if (us.cardRepo.findByLuid(luid).isEmpty) {
|
||||
if (us.cardRepo.findByLuid(luid) == null) {
|
||||
val card: Card = cardService.registerByAccessCode(luid)
|
||||
|
||||
status = 1
|
||||
aimeId = card.extId
|
||||
|
||||
fedy.onCardCreated(luid, card.extId)
|
||||
}
|
||||
else logger.warn("> Duplicated Aime Card Register detected, access code: $luid")
|
||||
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
package icu.samnyan.aqua.sega.aimedb
|
||||
|
||||
import icu.samnyan.aqua.sega.util.ByteBufUtil
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.Unpooled.copiedBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.crypto.Cipher
|
||||
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)
|
||||
*/
|
||||
@@ -15,7 +33,6 @@ object AimeDbEncryption {
|
||||
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) }
|
||||
|
||||
fun decrypt(src: ByteBuf) = copiedBuffer(dec.doFinal(ByteBufUtil.toBytes(src)))
|
||||
|
||||
fun encrypt(src: ByteBuf) = copiedBuffer(enc.doFinal(ByteBufUtil.toAllBytes(src)))
|
||||
fun decrypt(src: ByteBuf) = copiedBuffer(dec.doFinal(src.toBytes()))
|
||||
fun encrypt(src: ByteBuf) = copiedBuffer(enc.doFinal(src.toAllBytes()))
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel
|
||||
import io.netty.handler.logging.LogLevel
|
||||
import io.netty.handler.logging.LoggingHandler
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@@ -3,7 +3,6 @@ package icu.samnyan.aqua.sega.allnet
|
||||
import ext.*
|
||||
import icu.samnyan.aqua.net.db.AquaNetUserRepo
|
||||
import icu.samnyan.aqua.sega.allnet.AllNetBillingDecoder.decodeAllNet
|
||||
import icu.samnyan.aqua.sega.util.AquaConst
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
@@ -14,7 +13,6 @@ import java.io.InputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "allnet.server")
|
||||
@@ -84,7 +82,7 @@ class AllNet(
|
||||
|
||||
logger.info("AllNet /DownloadOrder : $reqMap")
|
||||
|
||||
val serial = reqMap["serial"] ?: AquaConst.DEFAULT_KEYCHIP_ID
|
||||
val serial = reqMap["serial"] ?: "A69E01A8888"
|
||||
val resp = mapOf(
|
||||
"stat" to "1",
|
||||
"serial" to serial
|
||||
@@ -103,6 +101,7 @@ class AllNet(
|
||||
// encode UTF-8, format_ver 3, hops 1, token 2010451813
|
||||
val reqMap = decodeAllNet(dataStream.readAllBytes())
|
||||
val serial = reqMap["serial"] ?: ""
|
||||
var region = props.map.mut["region0"] ?: "1"
|
||||
logger.info("AllNet /PowerOn : $reqMap")
|
||||
|
||||
var session: String? = null
|
||||
@@ -114,6 +113,10 @@ class AllNet(
|
||||
if (u != null) {
|
||||
// Create a new session for the user
|
||||
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
|
||||
}
|
||||
|
||||
@@ -140,6 +143,7 @@ class AllNet(
|
||||
val resp = props.map.mut + mapOf(
|
||||
"uri" to switchUri(here, localPort, gameId, ver, session),
|
||||
"host" to props.host.ifBlank { here },
|
||||
"region0" to region
|
||||
)
|
||||
|
||||
// Different responses for different versions
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
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\"}"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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>>() {})
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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\"}";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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<>() {
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
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\"}";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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> {
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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> {
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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> {
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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> {
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
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
Reference in New Issue
Block a user