merge upstream
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled

This commit is contained in:
2025-09-12 16:46:24 +08:00
53 changed files with 1939 additions and 331 deletions

View File

@@ -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} />

View File

@@ -0,0 +1,32 @@
<script lang="ts">
export let username: string;
export let email: string;
let shouldShow = navigator.language.startsWith('zh');
// 会导致瞬间出现,但是不知道为什么 svelte 的 transition 动画不工作
// if (!shouldShow) {
// fetch('https://47.122.72.135/ip/isChina')
// .then(it => it.json())
// .then(it => shouldShow = it)
// .catch(() => shouldShow = false);
// }
const jump = () => {
const params = new URLSearchParams();
if (username) params.set('username', username);
if (email) params.set('email', email);
location.href = `https://portal.mumur.net/register?${params.toString()}`;
}
</script>
{#if shouldShow}
<div class="cursor-pointer" on:click={jump}>
<h2>MuNET 了解一下!</h2>
<div>
<p>MuNET 是 AquaDX 的继任者,提供更适合中国用户的服务器和更好的游戏体验。</p>
<p>如果你还没有游戏数据,建议在 MuNET 上创建账号并开始游戏。点击立即前往</p>
</div>
</div>
{/if}

View File

@@ -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}

View File

@@ -4,6 +4,7 @@
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>
@@ -22,6 +23,11 @@
</label>
</div>
</div>
<div class="divider"></div>
<blockquote>
{ts("settings.regionNotice")}
</blockquote>
<RegionSelector/>
</div>
<style lang="sass">
@@ -44,19 +50,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>

View 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>

View File

@@ -19,6 +19,7 @@ export interface AquaNetUser {
email: string
displayName: string
country: string
region:string
lastLogin: number
regTime: number
profileLocation: string

View File

@@ -34,21 +34,31 @@ 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 = {
@@ -183,8 +193,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.gameNotice': "These only apply to Mai and Wacca.",
'settings.regionNotice': "These only apply to Mai, Ongeki and Chuni.",
'settings.regionSelector.title': "Prefecture Selector",
'settings.regionSelector.desc': "Select the region where you want the game to think you are playing",
'settings.regionSelector.select': "Select Prefecture",
}
export const EN_REF_USERBOX = {

View File

@@ -46,21 +46,31 @@ 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 = {
@@ -195,8 +205,17 @@ 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.regionNotice': "这些设置仅适用于舞萌、音击和中二。",
// AI
'settings.regionSelector.title': "地区选择器",
// AI
'settings.regionSelector.desc': "选择游戏中显示的地区",
// AI
'settings.regionSelector.select': "选择地区",
}
export const zhUserbox: typeof EN_REF_USERBOX = {

View File

@@ -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 }

View File

@@ -10,6 +10,7 @@
import { t } from "../libs/i18n";
import ImportDataAction from "./Home/ImportDataAction.svelte";
import Communities from "./Home/Communities.svelte";
import MigrateAction from "./Home/MigrateAction.svelte";
USER.ensureLoggedIn();
@@ -58,6 +59,9 @@
</ActionCard>
<ImportDataAction/>
{#if me}
<MigrateAction username={me.username}/>
{/if}
</div>
{:else if tab === 1}
<div out:fade={FADE_OUT} in:fade={FADE_IN}>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import { fade } from "svelte/transition"
import { t } from "../../libs/i18n";
import ActionCard from "../../components/ActionCard.svelte";
import { CARD, GAME, USER } from "../../libs/sdk";
export let username: string;
let shouldShow = navigator.language.startsWith('zh');
let showWarning = false;
let isCardBindIssue = false;
if (!shouldShow) {
fetch('https://47.122.72.135/ip/isChina')
.then(it => it.json())
.then(it => shouldShow = it)
.catch(() => shouldShow = false);
}
CARD.userGames(username).then(games => {
if (!Object.values(games).some(it => it)) {
isCardBindIssue = true;
}
})
const handleClick = () => {
if (isCardBindIssue) {
showWarning = true;
return
}
jump()
}
const jump = () => {
const token = localStorage.getItem('token')
location.href = `https://portal.mumur.net/migrateFromAquaDx/${token}`
}
</script>
{#if shouldShow}
<ActionCard color="190, 149, 255" icon="system-uicons:jump-up" on:click={handleClick}>
<h3>迁移到 MuNET</h3>
<span>更适合中国宝宝体质的服务器AquaDX 的继任者。点击查看详情</span>
</ActionCard>
{/if}
{#if showWarning}
<div class="overlay" transition:fade>
<div>
<h2>提示</h2>
<p>看起来你在 AquaDX 还没有游戏数据,也许是因为没有绑卡或者绑定的卡不是在游戏中点击“查看卡号”获取的…</p>
<p>现在迁移的话,大概会导致你的游戏数据无法被正确的迁移。建议你先去检查一下吧</p>
<div class="buttons">
<button on:click={() => showWarning = false}>{t('action.cancel')}</button>
<button on:click={jump}>继续</button>
</div>
</div>
</div>
{/if}
<style lang="sass">
h3
font-size: 1.3rem
margin: 0
.buttons
display: grid
grid-template-columns: 1fr 1fr
gap: 1rem
</style>

View File

@@ -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} />

View File

@@ -5,6 +5,7 @@
import Icon from "@iconify/svelte";
import { USER } from "../libs/sdk";
import { t } from "../libs/i18n"
import MunetRegisterBanner from "../components/MunetRegisterBanner.svelte";
let params = new URLSearchParams(window.location.search)
@@ -20,28 +21,33 @@
let error = ""
let verifyMsg = ""
let token = ""
if (USER.isLoggedIn()) {
window.location.href = "/home"
}
if (params.get('code')) {
token = params.get('code')!
if (location.pathname === '/verify') {
state = 'verify'
verifyMsg = t("welcome.verifying")
submitting = true
if (params.get('confirm-email')) {
state = 'verify'
verifyMsg = t("welcome.verifying")
submitting = true
// Send request to server
USER.confirmEmail(params.get('confirm-email')!)
.then(() => {
verifyMsg = t('welcome.verified')
submitting = false
// Send request to server
USER.confirmEmail(token)
.then(() => {
verifyMsg = t('welcome.verified')
submitting = false
// Clear the query param
window.history.replaceState({}, document.title, window.location.pathname)
})
.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
@@ -94,14 +100,80 @@
state = 'verify'
verifyMsg = t("welcome.verify-state-2")
}
else if (e.message === 'Login not allowed: Card has been migrated to Minato.') {
location.href = `https://portal.mumur.net/login?username=${encodeURIComponent(email)}`
}
else {
error = e.message
submitting = false // unnecessary? see line 113, same for both reset functions
turnstileReset()
}
})
}
submitting = false
}
async function resetPassword(): Promise<any> {
submitting = true;
if (email === "") {
error = t("welcome.email-missing")
return submitting = false
}
if (TURNSTILE_SITE_KEY && turnstile === "") {
// Sleep for 100ms to allow Turnstile to finish
error = t("welcome.waiting-turnstile")
return setTimeout(resetPassword, 100)
}
// Send request to server
await USER.resetPassword({ email, turnstile })
.then(() => {
// Show email sent message, reusing email verify page
state = 'verify'
verifyMsg = t("welcome.reset-password-sent", { email })
})
.catch(e => {
if (e.message === "Reset request rejected - STATE_0") {
state = 'verify'
verifyMsg = t("welcome.reset-state-0")
}
else if (e.message === "Reset request rejected - STATE_1") {
state = 'verify'
verifyMsg = t("welcome.reset-state-1")
}
else {
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
}
@@ -120,11 +192,13 @@
{#if error}
<span class="error">{error}</span>
{/if}
<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 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 +211,9 @@
{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)}
@@ -144,6 +221,37 @@
on:turnstile-expired={_ => window.location.reload()}
on:turnstile-timeout={_ => console.log(error = t('welcome.turnstile-timeout'))} />
{/if}
{#if isSignup}
<MunetRegisterBanner username={username} email={email}/>
{/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)}
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 === "verify"}
<div class="login-form" transition:slide>
@@ -152,6 +260,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>

View File

@@ -171,3 +171,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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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")
@@ -21,7 +23,13 @@ val JSON_FUZZY_BOOLEAN = SimpleModule().addDeserializer(Boolean::class.java, obj
})
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<java.time.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.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)
@@ -73,4 +81,4 @@ val JSON = Json {
// fun objectMapper(): ObjectMapper {
// return JACKSON
// }
//}
//}

View File

@@ -0,0 +1,204 @@
package icu.samnyan.aqua.net
import ext.*
import icu.samnyan.aqua.sega.general.service.CardService
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import org.springframework.web.bind.annotation.RestController
import java.security.MessageDigest
import icu.samnyan.aqua.net.db.AquaNetUserRepo
import icu.samnyan.aqua.net.db.AquaNetUserFedyRepo
import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.net.components.JWT
import icu.samnyan.aqua.net.db.AquaNetUserFedy
import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.net.games.ImportController
import icu.samnyan.aqua.net.games.mai2.Mai2Import
import icu.samnyan.aqua.net.games.ExportOptions
import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler as Mai2UploadUserPlaylogHandler
import icu.samnyan.aqua.sega.maimai2.handler.UpsertUserAllHandler as Mai2UpsertUserAllHandler
import icu.samnyan.aqua.net.utils.ApiException
import java.util.Arrays
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
import icu.samnyan.aqua.net.games.GenericUserDataRepo
import icu.samnyan.aqua.net.games.IUserData
import java.util.concurrent.CompletableFuture
@Configuration
@ConfigurationProperties(prefix = "aqua-net.fedy")
class FedyProps {
var enabled: Boolean = false
var key: String = ""
var remote: String = ""
}
enum class FedyEvent {
Linked,
Unlinked,
Upserted,
Imported,
}
@RestController
@API("/api/v2/fedy")
class Fedy(
val jwt: JWT,
val userRepo: AquaNetUserRepo,
val userFedyRepo: AquaNetUserFedyRepo,
val mai2Import: Mai2Import,
val mai2UserDataRepo: Mai2UserDataRepo,
val mai2UploadUserPlaylog: Mai2UploadUserPlaylogHandler,
val mai2UpsertUserAll: Mai2UpsertUserAllHandler,
val props: FedyProps,
val transactionManager: PlatformTransactionManager
) {
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"
}
@API("/status")
fun handleStatus(@RP token: Str): Any {
val user = jwt.auth(token)
val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId)
return mapOf("linkedAt" to (userFedy?.createdAt?.toEpochMilli() ?: 0))
}
@API("/link")
fun handleLink(@RP token: Str, @RP nonce: Str): Any {
val user = jwt.auth(token)
if (userFedyRepo.findByAquaNetUserAuId(user.auId) != null) 412 - "User already linked"
val userFedy = AquaNetUserFedy(aquaNetUser = user)
userFedyRepo.save(userFedy)
notify(FedyEvent.Linked, mapOf("auId" to user.auId, "nonce" to nonce))
return mapOf("linkedAt" to userFedy.createdAt.toEpochMilli())
}
@API("/unlink")
fun handleUnlink(@RP token: Str): Any {
val user = jwt.auth(token)
val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) ?: 412 - "User not linked"
userFedyRepo.delete(userFedy)
notify(FedyEvent.Unlinked, mapOf("auId" to user.auId))
return SUCCESS
}
private fun ensureUser(auId: Long): AquaNetUser {
val userFedy = userFedyRepo.findByAquaNetUserAuId(auId) ?: 404 - "User not linked"
val user = userRepo.findByAuId(auId) ?: 404 - "User not found"
return user
}
data class UnlinkByRemoteReq(val auId: Long)
@API("/unlink-by-remote")
fun handleUnlinkByRemote(@RH(KEY_HEADER) key: Str, @RB req: UnlinkByRemoteReq): Any {
key.checkKey()
val user = ensureUser(req.auId)
userFedyRepo.deleteByAquaNetUserAuId(user.auId)
// No need to notify remote, because initiated by remote
return SUCCESS
}
data class PullReq(val auId: Long, val game: Str, val exportOptions: ExportOptions)
@API("/pull")
fun handlePull(@RH(KEY_HEADER) key: Str, @RB req: PullReq): Any {
key.checkKey()
val user = ensureUser(req.auId)
fun catched(block: () -> Any) =
try { mapOf("result" to block()) }
catch (e: ApiException) { mapOf("error" to mapOf("code" to e.code, "message" to e.message.toString())) }
return when (req.game) {
"mai2" -> catched { mai2Import.export(user, req.exportOptions) }
else -> 406 - "Unsupported game"
}
}
data class PushReq(val auId: Long, val game: Str, val data: JDict, val removeOldData: Bool)
@Suppress("UNCHECKED_CAST")
@API("/push")
fun handlePush(@RH(KEY_HEADER) key: Str, @RB req: PushReq): Any {
key.checkKey()
val user = ensureUser(req.auId)
val extId = user.ghostCard.extId
fun<UserData : IUserData, UserRepo : GenericUserDataRepo<UserData>> removeOldData(repo: UserRepo) {
val oldData = repo.findByCard_ExtId(extId)
if (oldData.isPresent) {
log.info("Fedy: Deleting old data for $extId (${req.game})")
repo.delete(oldData.get());
repo.flush()
}
}
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"
} }
return SUCCESS
}
fun onUpserted(game: Str, maybeExtId: Any?) = maybeNotifyAsync(FedyEvent.Upserted, game, maybeExtId)
fun onImported(game: Str, maybeExtId: Any?) = maybeNotifyAsync(FedyEvent.Imported, game, maybeExtId)
private fun maybeNotifyAsync(event: FedyEvent, game: Str, maybeExtId: Any?) = if (!props.enabled) {} else CompletableFuture.runAsync { try {
val extId = maybeExtId?.long ?: return@runAsync
val user = userRepo.findByGhostCardExtId(extId) ?: return@runAsync
val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) ?: return@runAsync
notify(event, mapOf("auId" to user.auId, "game" to game))
} catch (e: Exception) {
log.error("Error handling Fedy on maybeNotifyAsync($event, $game, $maybeExtId)", e)
} }
private fun notify(event: FedyEvent, body: Any?) {
val MAX_RETRY = 3
val body = body?.toJson() ?: "{}"
var retry = 0
var shouldRetry = true
while (retry < MAX_RETRY) {
try {
val response = "${props.remote.trimEnd('/')}/notify/${event.name}".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)
}
}
}
companion object
{
const val KEY_HEADER = "X-Fedy-Key"
val log = logger()
fun getGameName(gameId: Str) = when (gameId) {
"SDEZ" -> "mai2"
else -> null // Not supported
}
}
}

View File

@@ -29,10 +29,12 @@ class UserRegistrar(
val geoIP: GeoIP,
val jwt: JWT,
val confirmationRepo: EmailConfirmationRepo,
val resetPasswordRepo: ResetPasswordRepo,
val cardRepo: CardRepository,
val cardService: CardService,
val validator: AquaUserServices,
val emailProps: EmailProperties,
val sessionRepo: SessionTokenRepo,
final val paths: PathProps
) {
val portraitPath = paths.aquaNetPortrait.path()
@@ -144,6 +146,73 @@ 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
async { userRepo.save(reset.aquaNetUser.apply { pwHash = validator.checkPwHash(password) }) }
// Remove the token from the list
resetPasswordRepo.delete(reset)
// Clear all sessions
sessionRepo.deleteAll(
sessionRepo.findByAquaNetUserAuId(reset.aquaNetUser.auId)
)
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 {
@@ -185,6 +254,12 @@ class UserRegistrar(
// Save the user
userRepo.save(u)
// Clear all tokens if changing password
if (key == "pwHash")
sessionRepo.deleteAll(
sessionRepo.findByAquaNetUserAuId(u.auId)
)
}
SUCCESS
@@ -227,4 +302,17 @@ class UserRegistrar(
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
}
}

View File

@@ -6,6 +6,8 @@ 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.ResetPassword
import icu.samnyan.aqua.net.db.ResetPasswordRepo
import org.simplejavamail.api.mailer.Mailer
import org.simplejavamail.email.EmailBuilder
import org.simplejavamail.springsupport.SimpleJavaMailSpringSupport
@@ -38,10 +40,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 +74,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) {
@@ -93,4 +121,4 @@ class EmailService(
}
}
}
}

View File

@@ -1,77 +1,116 @@
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 io.jsonwebtoken.JwtParser
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import jakarta.annotation.PostConstruct
import org.slf4j.LoggerFactory
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import org.springframework.stereotype.Service
import java.util.*
import javax.crypto.SecretKey
@Configuration
@ConfigurationProperties(prefix = "aqua-net.jwt")
class JWTProperties {
var secret: Str = "Open Sesame!"
}
@Service
class JWT(
val props: JWTProperties,
val userRepo: AquaNetUserRepo
) {
val log = LoggerFactory.getLogger(JWT::class.java)!!
lateinit var key: SecretKey
lateinit var parser: JwtParser
@PostConstruct
fun onLoad() {
// Check secret
if (props.secret == "Open Sesame!") {
log.warn("USING DEFAULT JWT SECRET, PLEASE SET aqua-net.jwt IN CONFIGURATION")
}
// Pad byte array to 256 bits
var ba = props.secret.toByteArray()
if (ba.size < 32) {
log.warn("JWT Secret is less than 256 bits, padding with 0. PLEASE USE A STRONGER SECRET!")
ba = ByteArray(32).also { ba.copyInto(it) }
}
// Initialize key
key = Keys.hmacShaKeyFor(ba)
// Create parser
parser = Jwts.parser()
.verifyWith(key)
.build()
log.info("JWT Service Enabled")
}
fun gen(user: AquaNetUser): Str = Jwts.builder().header()
.keyId("aqua-net")
.and()
.subject(user.auId.toString())
.issuedAt(Date())
.signWith(key)
.compact()
fun parse(token: Str): AquaNetUser? = try {
userRepo.findByAuId(parser.parseSignedClaims(token).payload.subject.toLong())
} catch (e: Exception) {
log.debug("Failed to parse JWT", e)
null
}
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")
final inline fun <T> auth(token: Str, block: (AquaNetUser) -> T) = block(auth(token))
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.SessionToken
import icu.samnyan.aqua.net.db.SessionTokenRepo
import icu.samnyan.aqua.net.db.getTokenExpiry
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
@Configuration
@ConfigurationProperties(prefix = "aqua-net.jwt")
class JWTProperties {
var secret: Str = "Open Sesame!"
}
@Service
class JWT(
val props: JWTProperties,
val userRepo: AquaNetUserRepo,
val sessionRepo: SessionTokenRepo
) {
val log = LoggerFactory.getLogger(JWT::class.java)!!
lateinit var key: SecretKey
lateinit var parser: JwtParser
@PostConstruct
fun onLoad() {
// Check secret
if (props.secret == "Open Sesame!") {
log.warn("USING DEFAULT JWT SECRET, PLEASE SET aqua-net.jwt IN CONFIGURATION")
}
// Pad byte array to 256 bits
var ba = props.secret.toByteArray()
if (ba.size < 32) {
log.warn("JWT Secret is less than 256 bits, padding with 0. PLEASE USE A STRONGER SECRET!")
ba = ByteArray(32).also { ba.copyInto(it) }
}
// Initialize key
key = Keys.hmacShaKeyFor(ba)
// Create parser
parser = Jwts.parser()
.verifyWith(key)
.build()
log.info("JWT Service Enabled")
}
@Transactional
fun gen(user: AquaNetUser): Str {
val activeTokens = sessionRepo.findByAquaNetUserAuId(user.auId)
.sortedByDescending { it.expiry }.drop(9) // the cap is 10, but we append a new token after the fact
if (activeTokens.isNotEmpty()) {
sessionRepo.deleteAll(activeTokens)
}
val token = SessionToken().apply {
aquaNetUser = user
}
sessionRepo.save(token)
return Jwts.builder().header()
.keyId("aqua-net")
.and()
.subject(token.token)
.issuedAt(Date())
.signWith(key)
.compact()
}
@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)
return null
}
}
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")
final inline fun <T> auth(token: Str, block: (AquaNetUser) -> T) = block(auth(token))
}

View 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
@Entity
@Table(name = "aqua_net_email_reset_password")
class ResetPassword(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
@Column(nullable = false)
var token: String = "",
// Token creation time
@Column(nullable = false)
var createdAt: Instant = Instant.now(),
// Linking to the AquaNetUser
@ManyToOne
@JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser = AquaNetUser()
) : Serializable
@Repository
interface ResetPasswordRepo : JpaRepository<ResetPassword, Long> {
fun findByToken(token: String): ResetPassword?
fun findByAquaNetUserAuId(auId: Long): List<ResetPassword>
}

View File

@@ -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.UUID
fun getTokenExpiry() = Instant.now().plusSeconds(7 * 86400)
@Entity
@Table(name = "aqua_net_session")
class SessionToken(
@Id
@Column(nullable = false)
var token: String = UUID.randomUUID().toString(),
// Token creation time
@Column(nullable = false)
var expiry: Instant = getTokenExpiry(),
// Linking to the AquaNetUser
@ManyToOne
@JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser = AquaNetUser()
) : Serializable
@Repository
interface SessionTokenRepo : JpaRepository<SessionToken, String> {
fun findByToken(token: String): SessionToken?
fun findByAquaNetUserAuId(auId: Long): List<SessionToken>
}

View File

@@ -43,6 +43,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 +102,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(

View File

@@ -0,0 +1,29 @@
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_user_fedy")
class AquaNetUserFedy(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
@Column(nullable = false)
var createdAt: Instant = Instant.now(),
// Linking to the AquaNetUser
@OneToOne
@JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser,
) : Serializable
@Repository
interface AquaNetUserFedyRepo : JpaRepository<AquaNetUserFedy, Long> {
fun findByAquaNetUserAuId(auId: Long): AquaNetUserFedy?
fun deleteByAquaNetUserAuId(auId: Long): Unit
}

View File

@@ -92,7 +92,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

View File

@@ -3,6 +3,7 @@ package icu.samnyan.aqua.net.games
import ext.*
import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.utils.AquaNetProps
import icu.samnyan.aqua.net.utils.SUCCESS
import org.springframework.beans.factory.annotation.Autowired
@@ -15,6 +16,11 @@ import java.util.*
import kotlin.io.path.Path
import kotlin.io.path.writeText
import kotlin.reflect.KClass
import org.springframework.context.annotation.Lazy
data class ExportOptions(
val playlogAfter: String? = null
)
// Import class with renaming
data class ImportClass<T : Any>(
@@ -54,6 +60,8 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
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 +70,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 @Lazy lateinit var fedy: Fedy
init {
artemisRenames.values.forEach {
@@ -72,13 +81,18 @@ 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, ExportOptions())
fun export(u: AquaNetUser, options: ExportOptions) = createEmpty().apply {
gameId = game
userData = userDataRepo.findByCard(u.ghostCard) ?: (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) }
}
customExporters.forEach { (f, exporter) ->
exporter(userData, options)?.let { f.set(this, it) }
}
}
@API("export")
@@ -95,6 +109,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,8 +141,14 @@ 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)
}
}
Fedy.getGameName(game)?.let { fedy.onImported(it, u.ghostCard.extId) }
SUCCESS
}

View File

@@ -3,11 +3,13 @@ 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
@@ -22,11 +24,16 @@ class Mai2Import(
it.name.replace("List", "").lowercase()
},
exportRepos = Maimai2DataExport::class.vars()
.filter { f -> f.name !in setOf("gameId", "userData") }
.associateWith { 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}")
.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 == repoName }
?.call(repos) as Mai2UserLinked<*>? ?: error("No matching field found for ${field.name}")
},
artemisRenames = mapOf(
"mai2_item_character" to ImportClass(Mai2UserCharacter::class),
@@ -44,34 +51,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").orElse(null)
?.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).orElse(null)
?: 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>

View File

@@ -103,6 +103,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 +115,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 +145,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

View File

@@ -83,7 +83,6 @@ fun ChusanController.chusanInit() {
"GetUserCtoCPlay" { """{"userId":"${data["userId"]}","orderBy":"0","count":"0","userCtoCPlayList":[]}""" }
"GetUserRivalMusic" { """{"userId":"${data["userId"]}","rivalId":"0","length":"0","nextIndex":"0","userRivalMusicList":[]}""" }
"GetUserRivalData" { """{"userId":"${data["userId"]}","length":"0","userRivalData":[]}""" }
"GetUserRegion" { """{"userId":"${data["userId"]}","length":"0","userRegionList":[]}""" }
"GetUserPrintedCard" { """{"userId":"${data["userId"]}","length":0,"nextIndex":-1,"userPrintedCardList":[]}""" }
// Net battle data
@@ -237,7 +236,7 @@ fun ChusanController.chusanInit() {
) + userDict
if (user.card?.status == CardStatus.MIGRATED_TO_MINATO) {
res["userName"] = "Migrated"
res["userName"] = "JiaQQqun / CardMigrated"
res["rating"] = 0
res["playerLevel"] = 0
}
@@ -288,6 +287,12 @@ fun ChusanController.chusanInit() {
)
}
"GetUserRegion" {
db.userRegions.findByUser_Card_ExtId(uid)
.map { mapOf("regionId" to it.regionId, "playCount" to it.playCount) }
.let { mapOf("userId" to uid, "userRegionList" to it) }
}
// Game settings
"GetGameSetting" {
val version = data["version"].toString()

View File

@@ -29,6 +29,17 @@ fun ChusanController.upsertApiInit() {
userNameEx = ""
}.also { db.userData.saveAndFlush(it) }
// Only save if it is a valid region and the user has played at least a song
req.userPlaylogList?.firstOrNull()?.regionId?.let { rid ->
val region = db.userRegions.findByUserAndRegionId(u, rid)?.apply {
playCount += 1
} ?: UserRegions().apply {
user = u
regionId = rid
}
db.userRegions.save(region)
}
versionHelper[u.lastClientId] = u.lastDataVersion
// Set users

View File

@@ -174,6 +174,10 @@ interface Chu3GameLoginBonusRepo : JpaRepository<GameLoginBonus, Int> {
fun findByRequiredDays(version: Int, presetId: Int, requiredDays: Int): Optional<GameLoginBonus>
}
interface Chu3UserRegionsRepo: Chu3UserLinked<UserRegions> {
fun findByUserAndRegionId(user: Chu3UserData, regionId: Int): UserRegions?
}
@Component
class Chu3Repos(
val userLoginBonus: Chu3UserLoginBonusRepo,
@@ -191,6 +195,7 @@ class Chu3Repos(
val userMap: Chu3UserMapRepo,
val userMusicDetail: Chu3UserMusicDetailRepo,
val userPlaylog: Chu3UserPlaylogRepo,
val userRegions: Chu3UserRegionsRepo,
val userCMission: Chu3UserCMissionRepo,
val userCMissionProgress: Chu3UserCMissionProgressRepo,
val netBattleLog: Chu3NetBattleLogRepo,

View File

@@ -0,0 +1,14 @@
package icu.samnyan.aqua.sega.chusan.model.userdata
import jakarta.persistence.Entity
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import java.time.LocalDate
@Entity(name = "ChusanUserRegions")
@Table(name = "chusan_user_regions", uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "region_id"])])
class UserRegions : Chu3UserEntity() {
var regionId = 0
var playCount = 1
var created: String = LocalDate.now().toString()
}

View File

@@ -33,7 +33,10 @@ data class PagedProcessor(val add: JDict?, val fn: PagedHandler, var post: PageP
// A very :3 way of declaring APIs
abstract class MeowApi(val serialize: (String, Any) -> String) {
val initH = mutableMapOf<String, SpecialHandler>()
infix operator fun String.invoke(fn: SpecialHandler) = initH.set("${this}Api", fn)
infix operator fun String.invoke(fn: SpecialHandler) {
if (initH.containsKey("${this}Api")) error("Duplicate API $this found! Someone is not smart 👀")
initH["${this}Api"] = fn
}
infix fun String.static(fn: () -> Any) = serialize(this, fn()).let { resp -> this { resp } }
// Page Cache: {cache key: (timestamp, full list)}

View File

@@ -7,9 +7,12 @@ import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusic
import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusicDetail
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserKaleidx
import icu.samnyan.aqua.sega.maimai2.model.userdata.UserRegions
import java.time.LocalDate
fun Maimai2ServletController.initApis() {
val log = logger()
"GetUserExtend" { mapOf(
"userId" to uid,
"userExtend" to (db.userExtend.findSingleByUser_Card_ExtId(uid)() ?: (404 - "User not found"))
@@ -111,7 +114,7 @@ fun Maimai2ServletController.initApis() {
)
if (d.card?.status == CardStatus.MIGRATED_TO_MINATO) {
res["userName"] = "Migrated"
res["userName"] = "JiaQQqun / CardMigrated"
res["dispRate"] = 1
res["playerRating"] = 66564
res["totalAwake"] = 7114
@@ -134,6 +137,20 @@ fun Maimai2ServletController.initApis() {
res["returnCode"] = 0
}
// Get regionId from request
val region = data["regionId"] as? Int
// Only save if it is a valid region and the user has played at least a song
if (region != null && region > 0 && d != null) {
val region = db.userRegions.findByUserAndRegionId(d, region)?.apply {
playCount += 1
} ?: UserRegions().apply {
user = d
regionId = region
}
db.userRegions.save(region)
}
res
}
@@ -178,13 +195,19 @@ fun Maimai2ServletController.initApis() {
mapOf("userId" to uid, "rivalId" to rivalId, "nextIndex" to 0, "userRivalMusicList" to res.values)
}
"GetUserRegion" {
logger().info("Getting user regions for user $uid")
db.userRegions.findByUser_Card_ExtId(uid)
.map { mapOf("regionId" to it.regionId, "playCount" to it.playCount) }
.let { mapOf("userId" to uid, "length" to it.size, "userRegionList" to it) }
}
"GetUserIntimate".unpaged {
val u = db.userData.findByCardExtId(uid)() ?: (404 - "User not found")
db.userIntimate.findByUser(u)
}
// Empty List Handlers
"GetUserRegion".unpaged { empty }
"GetUserGhost".unpaged { empty }
"GetUserFriendBonus" { mapOf("userId" to uid, "returnCode" to 0, "getMiles" to 0) }
"GetTransferFriend" { mapOf("userId" to uid, "transferFriendList" to empty) }
@@ -339,4 +362,4 @@ fun Maimai2ServletController.initApis() {
"userRecommendSelectionMusicIdList" to (net.recommendedMusic[user.id] ?: empty)
)
}
}
}

View File

@@ -14,6 +14,10 @@ import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.bind.annotation.*
import java.time.format.DateTimeFormatter
import kotlin.reflect.full.declaredMemberProperties
import icu.samnyan.aqua.net.Fedy
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy
import org.springframework.beans.factory.ObjectProvider
/**
* @author samnyan (privateamusement@protonmail.com)
@@ -37,6 +41,8 @@ class Maimai2ServletController(
val net: Maimai2,
): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) {
@Autowired @Lazy lateinit var fedy: Fedy
companion object {
private val log = logger()
private val empty = listOf<Any>()
@@ -89,6 +95,7 @@ class Maimai2ServletController(
val ctx = RequestContext(req, data.mut)
serialize(api, handlers[api]!!(ctx) ?: noop).also {
log.info("$token : $api > ${it.truncate(500)}")
if (api == "UpsertUserAllApi") { fedy.onUpserted("mai2", data["userId"]) }
}
}
} catch (e: Exception) {

View File

@@ -57,8 +57,6 @@ interface Mai2UserExtendRepo : Mai2UserLinked<Mai2UserExtend>
interface Mai2UserFavoriteRepo : Mai2UserLinked<Mai2UserFavorite> {
fun findByUserAndItemKind(user: Mai2UserDetail, kind: Int): Optional<Mai2UserFavorite>
fun findByUserIdAndItemKind(userId: Long, kind: Int): List<Mai2UserFavorite>
fun findByUser_Card_ExtIdAndItemKind(userId: Long, kind: Int): Optional<Mai2UserFavorite>
}
@@ -104,6 +102,7 @@ interface Mai2UserPlaylogRepo : GenericPlaylogRepo<Mai2UserPlaylog>, Mai2UserLin
musicId: Int,
userPlayDate: String
): MutableList<Mai2UserPlaylog>
fun findByUserAndUserPlayDateAfter(user: Mai2UserDetail, userPlayDate: String): List<Mai2UserPlaylog>
}
interface Mai2UserPrintDetailRepo : JpaRepository<Mai2UserPrintDetail, Long>
@@ -126,6 +125,10 @@ interface Mai2GameEventRepo : JpaRepository<Mai2GameEvent, Int> {
interface Mai2GameSellingCardRepo : JpaRepository<Mai2GameSellingCard, Long>
interface Mai2UserRegionsRepo: Mai2UserLinked<UserRegions> {
fun findByUserAndRegionId(user: Mai2UserDetail, regionId: Int): UserRegions?
}
@Component
class Mai2Repos(
val mapEncountNpc: Mai2MapEncountNpcRepo,
@@ -151,5 +154,6 @@ class Mai2Repos(
val userIntimate: MAi2UserIntimateRepo,
val gameCharge: Mai2GameChargeRepo,
val gameEvent: Mai2GameEventRepo,
val gameSellingCard: Mai2GameSellingCardRepo
val gameSellingCard: Mai2GameSellingCardRepo,
val userRegions: Mai2UserRegionsRepo,
)

View File

@@ -16,6 +16,12 @@ import lombok.AllArgsConstructor
import lombok.Data
import lombok.NoArgsConstructor
import java.time.LocalDateTime
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import java.time.format.DateTimeFormatter
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.core.JsonGenerator
import java.time.LocalDate
@MappedSuperclass
open class Mai2UserEntity : BaseEntity(), IUserEntity<Mai2UserDetail> {
@@ -446,9 +452,9 @@ class Mai2UserPlaylog : Mai2UserEntity(), IGenericGamePlaylog {
get() = maxCombo == totalCombo
override val isAllPerfect: Boolean
get() = tapMiss + tapGood + tapGreat == 0 &&
holdMiss + holdGood + holdGreat == 0 &&
slideMiss + slideGood + slideGreat == 0 &&
get() = tapMiss + tapGood + tapGreat == 0 &&
holdMiss + holdGood + holdGreat == 0 &&
slideMiss + slideGood + slideGreat == 0 &&
touchMiss + touchGood + touchGreat == 0 &&
breakMiss + breakGood + breakGreat == 0
}
@@ -526,10 +532,14 @@ class Mai2UserKaleidx : Mai2UserEntity() {
var totalDeluxscore = 0
var bestAchievement = 0
var bestDeluxscore = 0
@JsonSerialize(using = MaimaiDateSerializer::class)
var bestAchievementDate: LocalDateTime? = null
@JsonSerialize(using = MaimaiDateSerializer::class)
var bestDeluxscoreDate: LocalDateTime? = null
var playCount = 0
@JsonSerialize(using = MaimaiDateSerializer::class)
var clearDate: LocalDateTime? = null
@JsonSerialize(using = MaimaiDateSerializer::class)
var lastPlayDate: LocalDateTime? = null
var isInfoWatched = false
}
@@ -541,3 +551,21 @@ class Mai2UserIntimate : Mai2UserEntity() {
var intimateLevel = 0;
var intimateCountRewarded = 0;
}
@Entity(name = "Maimai2UserRegions")
@Table(
name = "maimai2_user_regions",
uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "region_id"])]
)
class UserRegions : Mai2UserEntity() {
var regionId = 0
var playCount = 1
var created: String = LocalDate.now().toString()
}
val MAIMAI_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")
class MaimaiDateSerializer : JsonSerializer<LocalDateTime>() {
override fun serialize(v: LocalDateTime, j: JsonGenerator, s: SerializerProvider) {
j.writeString(v.format(MAIMAI_DATETIME))
}
}

View File

@@ -147,6 +147,10 @@ interface OgkUserTrainingRoomRepo : OngekiUserLinked<UserTrainingRoom> {
fun findByUserAndRoomId(user: UserData, roomId: Int): Optional<UserTrainingRoom>
}
interface OgkUserRegionsRepo: OngekiUserLinked<UserRegions> {
fun findByUserAndRegionId(user: UserData, regionId: Int): UserRegions?
}
// Re:Fresh
interface OgkUserEventMapRepo : OngekiUserLinked<UserEventMap>
interface OgkUserSkinRepo : OngekiUserLinked<UserSkin>
@@ -190,6 +194,7 @@ class OngekiUserRepos(
val trainingRoom: OgkUserTrainingRoomRepo,
val eventMap: OgkUserEventMapRepo,
val skin: OgkUserSkinRepo,
val regions: OgkUserRegionsRepo,
)
@Component

View File

@@ -1,11 +1,13 @@
package icu.samnyan.aqua.sega.ongeki
import ext.int
import ext.invoke
import ext.mapApply
import ext.minus
import icu.samnyan.aqua.sega.ongeki.model.OngekiUpsertUserAll
import icu.samnyan.aqua.sega.ongeki.model.UserData
import icu.samnyan.aqua.sega.ongeki.model.UserGeneralData
import icu.samnyan.aqua.sega.ongeki.model.UserRegions
fun OngekiController.initUpsertAll() {
@@ -33,6 +35,20 @@ fun OngekiController.initUpsertAll() {
db.data.save(this)
} ?: oldUser ?: return@api null
// User region
val region = data["regionId"]?.int ?: 0
// Only save if it is a valid region and the user has played at least a song
if (region > 0 && all.userPlaylogList?.isNotEmpty() == true) {
val region = db.regions.findByUserAndRegionId(u, region)?.apply {
playCount += 1
} ?:UserRegions().apply {
user = u
regionId = region
}
db.regions.save(region)
}
all.run {
// Set users
listOfNotNull(

View File

@@ -41,7 +41,10 @@ fun OngekiController.initUser() {
"GetUserBpBase".unpaged { empty }
"GetUserRatinglog".unpaged { empty }
"GetUserRegion".unpaged { empty }
"GetUserRegion".unpaged {
db.regions.findByUser_Card_ExtId(uid)
.map { mapOf("regionId" to it.regionId, "playCount" to it.playCount) }
}
"GetUserTradeItem".unpaged {
val start = parsing { data["startChapterId"]!!.int }
@@ -112,7 +115,29 @@ fun OngekiController.initUser() {
}
"GetUserPreview" api@ {
val u = db.data.findByCard_ExtId(uid)() ?: return@api mapOf("userId" to uid, "lastPlayDate" to null)
val u = db.data.findByCard_ExtId(uid)() ?: return@api mapOf(
"userId" to uid,
"isLogin" to false,
"lastLoginDate" to "0000-00-00 00:00:00",
"userName" to "",
"reincarnationNum" to 0,
"level" to 0,
"exp" to 0,
"playerRating" to 0,
"lastGameId" to "",
"lastRomVersion" to "",
"lastDataVersion" to "",
"lastPlayDate" to "",
"nameplateId" to 0,
"trophyId" to 0,
"cardId" to 0,
"dispPlayerLv" to 0,
"dispRating" to 0,
"dispBP" to 0,
"headphone" to 0,
"banStatus" to 0,
"isWarningConfirmed" to true
)
val o = db.option.findSingleByUser(u)()
val res = mutableMapOf(
@@ -137,7 +162,7 @@ fun OngekiController.initUser() {
)
if (u.card?.status == CardStatus.MIGRATED_TO_MINATO) {
res["userName"] = "Migrated"
res["userName"] = "JiaQQqun / CardMigrated"
res["level"] = 0
res["exp"] = 0
res["playerRating"] = 0
@@ -186,4 +211,4 @@ fun OngekiController.initUser() {
l to mapOf("rivalUserId" to rivalUserId)
}
}
}

View File

@@ -7,6 +7,7 @@ import icu.samnyan.aqua.net.games.*
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.util.jackson.AccessCodeSerializer
import jakarta.persistence.*
import java.time.LocalDate
@MappedSuperclass
class OngekiUserEntity : BaseEntity(), IUserEntity<UserData> {
@@ -511,4 +512,15 @@ class UserSkin : OngekiUserEntity() {
var cardId1 = 0
var cardId2 = 0
var cardId3 = 0
}
@Entity(name = "OngekiUserRegions")
@Table(
name = "ongeki_user_regions",
uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "regionId"])]
)
class UserRegions : OngekiUserEntity() {
var regionId = 0
var playCount = 1
var created: String = LocalDate.now().toString()
}

View File

@@ -225,7 +225,7 @@ fun WaccaServer.init() {
val status = u.lStatus().toMutableList()
if (u.card?.status == CardStatus.MIGRATED_TO_MINATO) {
status[1] = "Migrated"
status[1] = "JiaQQqun / CardMigrated"
}
u.run { ls(

View File

@@ -0,0 +1,9 @@
CREATE TABLE aqua_net_user_fedy
(
id BIGINT AUTO_INCREMENT NOT NULL,
created_at datetime NOT NULL,
au_id BIGINT NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_fedy_on_aqua_net_user FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT unq_fedy_on_aqua_net_user UNIQUE (au_id)
);

View File

@@ -1,157 +1,157 @@
INSERT INTO chusan_game_event (id, type, end_date, start_date, enable)
VALUES
(51,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(52,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(53,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(1021,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3027,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3217,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3309,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3412,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3514,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3623,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3726,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3808,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3912,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4010,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4111,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4210,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4323,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4513,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4614,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4710,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4808,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4909,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4911,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5026,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5112,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5216,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5311,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5360,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5410,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5513,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5630,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5708,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5819,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5920,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6020,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6130,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6221,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6319,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6409,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6511,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(11159,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12580,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12582,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12584,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12586,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12587,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12602,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12611,12,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12613,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13060,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13451,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13453,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13504,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13506,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13507,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13513,12,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13552,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13553,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13616,7,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13617,7,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13651,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15150,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15151,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15152,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15156,14,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15157,7,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15158,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15200,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15201,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15202,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15203,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15204,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15205,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15206,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15207,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15208,14,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15209,12,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15210,10,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15211,4,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15212,5,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15213,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15250,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15251,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15252,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15253,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15254,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15255,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15256,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15480,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15481,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15482,7,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15483,7,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15560,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16100,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16101,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16102,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16103,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16104,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16105,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16106,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16107,16,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16108,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16109,4,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16110,5,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16111,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16150,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16151,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16152,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16153,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16154,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16155,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16156,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16157,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16158,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16159,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16160,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16161,14,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16162,10,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16163,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16164,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16165,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16200,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16201,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16202,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16203,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16204,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16205,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16206,14,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16207,5,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16208,4,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16209,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16250,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16251,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16252,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16253,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16254,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16255,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16256,12,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16257,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16258,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16300,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16301,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16302,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16303,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16304,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16305,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16306,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16307,16,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16308,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16309,10,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16310,5,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16311,4,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16312,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(99000,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
INSERT INTO chusan_game_event (id, type, end_date, start_date, enable)
VALUES
(51,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(52,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(53,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(1021,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3027,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3217,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3309,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3412,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3514,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3623,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3726,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3808,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(3912,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4010,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4111,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4210,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4323,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4513,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4614,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4710,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4808,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4909,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(4911,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5026,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5112,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5216,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5311,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5360,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5410,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5513,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5630,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5708,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5819,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(5920,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6020,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6130,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6221,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6319,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6409,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(6511,9,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(11159,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12580,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12582,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12584,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12586,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12587,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12602,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12611,12,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(12613,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13060,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13451,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13453,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13504,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13506,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13507,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13513,12,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13552,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13553,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13616,7,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13617,7,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(13651,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15150,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15151,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15152,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15156,14,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15157,7,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15158,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15200,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15201,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15202,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15203,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15204,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15205,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15206,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15207,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15208,14,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15209,12,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15210,10,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15211,4,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15212,5,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15213,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15250,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15251,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15252,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15253,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15254,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15255,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15256,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15480,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15481,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15482,7,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15483,7,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(15560,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16100,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16101,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16102,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16103,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16104,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16105,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16106,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16107,16,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16108,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16109,4,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16110,5,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16111,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16150,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16151,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16152,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16153,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16154,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16155,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16156,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16157,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16158,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16159,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16160,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16161,14,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16162,10,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16163,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16164,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16165,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16200,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16201,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16202,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16203,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16204,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16205,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16206,14,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16207,5,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16208,4,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16209,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16250,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16251,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16252,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16253,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16254,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16255,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16256,12,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16257,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16258,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16300,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16301,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16302,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16303,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16304,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16305,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16306,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16307,16,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16308,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16309,10,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16310,5,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16311,4,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16312,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(99000,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(99001,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true);

View File

@@ -0,0 +1,21 @@
INSERT INTO `maimai2_game_selling_card` (`card_id`,`start_date`, `end_date`, `notice_start_date`, `notice_end_date`) VALUES
(5504014, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5504016, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5503014, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5503016, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5502014, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5502016, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5501014, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5501016, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5500014, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5500016, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5003014, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5003016, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5002014, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5002016, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5001014, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5001016, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5000014, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5000016, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5505014, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000'),
(5505016, '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000');

View File

@@ -0,0 +1,27 @@
INSERT INTO chusan_game_event (id, type, end_date, start_date, enable)
VALUES
(16600,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16601,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16602,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16603,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16604,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16605,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16606,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16607,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16608,16,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16609,5,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16610,4,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16611,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16612,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16650,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16651,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16652,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16653,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16654,8,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16655,11,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16700,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16701,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16702,1,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16703,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16704,2,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16705,5,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true);

View File

@@ -0,0 +1,16 @@
INSERT INTO chusan_game_event (id, type, end_date, start_date, enable)
VALUES
(16550, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16551, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16552, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16553, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16554, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16555, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16556, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16557, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16558, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16559, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16560, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16561, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16562, 7, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(16563, 10, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true);

View File

@@ -0,0 +1,22 @@
CREATE TABLE aqua_net_session
(
token VARCHAR(36) NOT NULL,
expiry datetime NOT NULL,
au_id BIGINT NULL,
CONSTRAINT pk_session PRIMARY KEY (token)
);
ALTER TABLE aqua_net_session
ADD CONSTRAINT FK_SESSION FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id);
CREATE TABLE aqua_net_email_reset_password
(
id BIGINT AUTO_INCREMENT NOT NULL,
token VARCHAR(255) NOT NULL,
created_at datetime NOT NULL,
au_id BIGINT NULL,
CONSTRAINT pk_email_reset_password PRIMARY KEY (id)
);
ALTER TABLE aqua_net_email_reset_password
ADD CONSTRAINT FK_EMAIL_RESET_PASSWORD_ON_AQUA_USER FOREIGN KEY (au_id) REFERENCES aqua_net_user (au_id);

View File

@@ -0,0 +1,38 @@
CREATE TABLE chusan_user_regions
(
id BIGINT AUTO_INCREMENT NOT NULL,
user_id BIGINT NULL,
region_id INT NOT NULL,
play_count INT NOT NULL DEFAULT 1,
created VARCHAR(355),
PRIMARY KEY (id),
CONSTRAINT fk_chusanregions_on_chusan_user_Data FOREIGN KEY (user_id) REFERENCES chusan_user_data (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT unq_chusanregions_on_region_user UNIQUE (user_id, region_id)
);
CREATE TABLE ongeki_user_regions
(
id BIGINT AUTO_INCREMENT NOT NULL,
user_id BIGINT NULL,
region_id INT NOT NULL,
play_count INT NOT NULL DEFAULT 1,
created VARCHAR(355),
PRIMARY KEY (id),
CONSTRAINT fk_ongekiregions_on_aqua_net_user FOREIGN KEY (user_id) REFERENCES aqua_net_user (au_id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT unq_ongekiregions_on_region_user UNIQUE (user_id, region_id)
);
CREATE TABLE maimai2_user_regions
(
id BIGINT AUTO_INCREMENT NOT NULL,
user_id BIGINT NULL,
region_id INT NOT NULL,
play_count INT NOT NULL DEFAULT 1,
created VARCHAR(355),
PRIMARY KEY (id),
CONSTRAINT fk_maimai2regions_on_user_Details FOREIGN KEY (user_id) REFERENCES maimai2_user_detail (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT unq_maimai2regions_on_region_user UNIQUE (user_id, region_id)
);
ALTER TABLE aqua_net_user
ADD COLUMN region VARCHAR(2) NOT NULL DEFAULT '1';

View File

@@ -212,7 +212,7 @@
<div style="color:#101112;direction:ltr;font-family:'Montserrat', 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;font-size:16px;font-weight:400;letter-spacing:0px;line-height:120%;text-align:left;mso-line-height-alt:19.2px;">
<p style="margin: 0; margin-bottom: 16px;">Dear {{name}},</p>
<p style="margin: 0; margin-bottom: 16px;">Thank you for registering with AquaDX! We're excited to have you on board. To complete your registration and verify your email address, please click the link below.</p>
<p style="margin: 0;">This link will confirm your email address, and it is valid for 24 hours. If you did not initiate this request, please ignore this email.</p>
<p style="margin: 0;">This link will verify your email address, and it is valid for 24 hours. If you did not initiate this request, please ignore this email.</p>
</div>
</td>
</tr>
@@ -225,7 +225,7 @@
<w:anchorlock/>
<v:textbox inset="0px,0px,0px,0px">
<center style="color:#ffffff; font-family:'Trebuchet MS', Tahoma, sans-serif; font-size:16px">
<![endif]--><a href="{{url}}" style="text-decoration:none;display:inline-block;color:#ffffff;background-color:#646cff;border-radius:8px;width:auto;border-top:0px solid transparent;font-weight:400;border-right:0px solid transparent;border-bottom:0px solid transparent;border-left:0px solid transparent;padding-top:8px;padding-bottom:8px;font-family:'Montserrat', 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;font-size:16px;text-align:center;mso-border-alt:none;word-break:keep-all;" target="_blank"><span style="padding-left:16px;padding-right:16px;font-size:16px;display:inline-block;letter-spacing:normal;"><span style="word-break: break-word; line-height: 32px;">Confirm email</span></span></a><!--[if mso]></center></v:textbox></v:roundrect><![endif]--></div>
<![endif]--><a href="{{url}}" style="text-decoration:none;display:inline-block;color:#ffffff;background-color:#646cff;border-radius:8px;width:auto;border-top:0px solid transparent;font-weight:400;border-right:0px solid transparent;border-bottom:0px solid transparent;border-left:0px solid transparent;padding-top:8px;padding-bottom:8px;font-family:'Montserrat', 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;font-size:16px;text-align:center;mso-border-alt:none;word-break:keep-all;" target="_blank"><span style="padding-left:16px;padding-right:16px;font-size:16px;display:inline-block;letter-spacing:normal;"><span style="word-break: break-word; line-height: 32px;">Verify email</span></span></a><!--[if mso]></center></v:textbox></v:roundrect><![endif]--></div>
</td>
</tr>
</table>

View File

@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="en" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<title></title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/><!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch><o:AllowPNG/></o:OfficeDocumentSettings></xml><![endif]--><!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200;300;400;500;600;700;800;900" rel="stylesheet" type="text/css"/><!--<![endif]-->
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
}
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: inherit !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
}
p {
line-height: inherit
}
.desktop_hide,
.desktop_hide table {
mso-hide: all;
display: none;
max-height: 0px;
overflow: hidden;
}
.image_block img+div {
display: none;
}
@media (max-width:700px) {
.desktop_hide table.icons-inner,
.row-3 .column-1 .block-3.button_block .alignment a,
.row-3 .column-1 .block-3.button_block .alignment div {
display: inline-block !important;
}
.icons-inner {
text-align: center;
}
.icons-inner td {
margin: 0 auto;
}
.image_block div.fullWidth {
max-width: 100% !important;
}
.mobile_hide {
display: none;
}
.row-content {
width: 100% !important;
}
.stack .column {
width: 100%;
display: block;
}
.mobile_hide {
min-height: 0;
max-height: 0;
max-width: 0;
overflow: hidden;
font-size: 0px;
}
.desktop_hide,
.desktop_hide table {
display: table !important;
max-height: none !important;
}
.row-1 .column-1 .block-1.icons_block .alignment,
.row-3 .column-1 .block-3.button_block .alignment {
text-align: center !important;
}
.row-3 .column-1 .block-2.paragraph_block td.pad>div {
text-align: left !important;
font-size: 14px !important;
}
.row-3 .column-1 .block-1.heading_block h1 {
text-align: center !important;
font-size: 24px !important;
}
.row-3 .column-1 .block-1.heading_block td.pad {
padding: 15px 0 !important;
}
.row-3 .column-1 .block-4.paragraph_block td.pad>div {
text-align: justify !important;
font-size: 10px !important;
}
.row-3 .column-1 .block-3.button_block a,
.row-3 .column-1 .block-3.button_block div,
.row-3 .column-1 .block-3.button_block span {
font-size: 14px !important;
line-height: 28px !important;
}
.row-3 .column-1 {
padding: 0 24px 48px !important;
}
}
</style>
</head>
<body style="background-color: #f8f6ff; margin: 0; padding: 0; -webkit-text-size-adjust: none; text-size-adjust: none;">
<table border="0" cellpadding="0" cellspacing="0" class="nl-container" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f8f6ff; background-image: none; background-position: top left; background-size: auto; background-repeat: no-repeat;" width="100%">
<tbody>
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row row-1" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;" width="100%">
<tbody>
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row-content stack" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #99b2ff; color: #000000; width: 680px; margin: 0 auto;" width="680">
<tbody>
<tr>
<td class="column column-1" style="font-weight: 400; text-align: left; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding-bottom: 32px; padding-left: 48px; padding-right: 48px; padding-top: 32px; vertical-align: top; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;" width="100%">
<table border="0" cellpadding="0" cellspacing="0" class="icons_block block-1" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;" width="100%">
<tr>
<td class="pad" style="vertical-align: middle; color: white; font-family: inherit; font-size: 24px; font-weight: 400; letter-spacing: 6px; text-align: left;">
<table cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;" width="100%">
<tr>
<td class="alignment" style="vertical-align: middle; text-align: left;"><!--[if vml]><table align="left" cellpadding="0" cellspacing="0" role="presentation" style="display:inline-block;padding-left:0px;padding-right:0px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;"><![endif]-->
<!--[if !vml]><!-->
<table cellpadding="0" cellspacing="0" class="icons-inner" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; display: inline-block; margin-right: -4px; padding-left: 0px; padding-right: 0px;"><!--<![endif]-->
<tr>
<td style="vertical-align: middle; text-align: center; padding-top: 0px; padding-bottom: 0px; padding-left: 0px; padding-right: 15px;"><img align="center" class="icon" height="32" src="https://aquadx.net/assets/icons/android-chrome-192x192.png" style="display: block; height: auto; margin: 0 auto; border: 0;" width="32"/></td>
<td style="font-family: 'Montserrat', 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif; font-size: 24px; font-weight: 400; color: white; vertical-align: middle; letter-spacing: 6px; text-align: left;">AquaDX</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row row-2" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;" width="100%">
<tbody>
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row-content stack" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #99b2ff; color: #000000; border-radius: 0; width: 680px; margin: 0 auto;" width="680">
<tbody>
<tr>
<td class="column column-1" style="font-weight: 400; text-align: left; mso-table-lspace: 0pt; mso-table-rspace: 0pt; vertical-align: top; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;" width="100%">
<table border="0" cellpadding="0" cellspacing="0" class="image_block block-1" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;" width="100%">
<tr>
<td class="pad" style="width:100%;padding-right:0px;padding-left:0px;">
<div align="center" class="alignment" style="line-height:10px">
<div class="fullWidth" style="max-width: 646px;"><img alt="" src="https://aquadx.net/assets/email/border.png" style="display: block; height: auto; border: 0; width: 100%;" title="An open email illustration" width="646"/></div>
</div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row row-3" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;" width="100%">
<tbody>
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row-content stack" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: white; border-radius: 0; color: #000000; width: 680px; margin: 0 auto;" width="680">
<tbody>
<tr>
<td class="column column-1" style="font-weight: 400; text-align: left; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding-bottom: 48px; padding-left: 48px; padding-right: 48px; vertical-align: top; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;" width="100%">
<table border="0" cellpadding="0" cellspacing="0" class="heading_block block-1" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;" width="100%">
<tr>
<td class="pad" style="padding-bottom:12px;text-align:center;width:100%;">
<h1 style="margin: 0; color: #292929; direction: ltr; font-family: 'Montserrat', 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif; font-size: 32px; font-weight: 700; letter-spacing: normal; line-height: 120%; text-align: left; margin-top: 0; margin-bottom: 0; mso-line-height-alt: 38.4px;"><span class="tinyMce-placeholder">Reset your password!</span></h1>
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" class="paragraph_block block-2" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;" width="100%">
<tr>
<td class="pad" style="padding-bottom:10px;padding-top:10px;">
<div style="color:#101112;direction:ltr;font-family:'Montserrat', 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;font-size:16px;font-weight:400;letter-spacing:0px;line-height:120%;text-align:left;mso-line-height-alt:19.2px;">
<p style="margin: 0; margin-bottom: 16px;">Dear {{name}},</p>
<p style="margin: 0; margin-bottom: 16px;">You recently requested to reset your AquaDX password. To reset your password, please click the link below.</p>
<p style="margin: 0;">This link will allow you to reset your password, and it is valid for 24 hours. If you did not initiate this request, please ignore this email.</p>
</div>
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" class="button_block block-3" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;" width="100%">
<tr>
<td class="pad" style="padding-bottom:15px;padding-top:15px;text-align:left;">
<div align="left" class="alignment"><!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{url}}" style="height:48px;width:168px;v-text-anchor:middle;" arcsize="17%" stroke="false" fillcolor="#646cff">
<w:anchorlock/>
<v:textbox inset="0px,0px,0px,0px">
<center style="color:#ffffff; font-family:'Trebuchet MS', Tahoma, sans-serif; font-size:16px">
<![endif]--><a href="{{url}}" style="text-decoration:none;display:inline-block;color:#ffffff;background-color:#646cff;border-radius:8px;width:auto;border-top:0px solid transparent;font-weight:400;border-right:0px solid transparent;border-bottom:0px solid transparent;border-left:0px solid transparent;padding-top:8px;padding-bottom:8px;font-family:'Montserrat', 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;font-size:16px;text-align:center;mso-border-alt:none;word-break:keep-all;" target="_blank"><span style="padding-left:16px;padding-right:16px;font-size:16px;display:inline-block;letter-spacing:normal;"><span style="word-break: break-word; line-height: 32px;">Reset password</span></span></a><!--[if mso]></center></v:textbox></v:roundrect><![endif]--></div>
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" class="paragraph_block block-4" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;" width="100%">
<tr>
<td class="pad" style="padding-top:16px;">
<div style="color:#666666;direction:ltr;font-family:'Montserrat', 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;font-size:12px;font-weight:400;letter-spacing:0px;line-height:120%;text-align:left;mso-line-height-alt:14.399999999999999px;">
<p style="margin: 0; margin-bottom: 12px;">If you're having trouble clicking the link, you can also copy and paste the URL below into your web browser:</p>
<p style="margin: 0;">{{url}}</p>
</div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row row-4" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;" width="100%">
<tbody>
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row-content stack" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #99b2ff; color: #000000; width: 680px; margin: 0 auto;" width="680">
<tbody>
<tr>
<td class="column column-1" style="font-weight: 400; text-align: left; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding-bottom: 32px; padding-left: 48px; padding-right: 48px; padding-top: 32px; vertical-align: top; border-top: 0px; border-right: 0px; border-bottom: 0px; border-left: 0px;" width="100%">
<div class="spacer_block block-1" style="height:60px;line-height:60px;font-size:1px;"></div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table><!-- End -->
</body>
</html>

View File

@@ -22,7 +22,7 @@
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>
<maxHistory>7</maxHistory>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>