diff --git a/AquaNet/src/App.svelte b/AquaNet/src/App.svelte index cc9d363e..64268330 100644 --- a/AquaNet/src/App.svelte +++ b/AquaNet/src/App.svelte @@ -79,6 +79,8 @@ + + diff --git a/AquaNet/src/components/MunetRegisterBanner.svelte b/AquaNet/src/components/MunetRegisterBanner.svelte new file mode 100644 index 00000000..187483d7 --- /dev/null +++ b/AquaNet/src/components/MunetRegisterBanner.svelte @@ -0,0 +1,32 @@ + + +{#if shouldShow} +
+

MuNET 了解一下!

+
+

MuNET 是 AquaDX 的继任者,提供更适合中国用户的服务器和更好的游戏体验。

+

如果你还没有游戏数据,建议在 MuNET 上创建账号并开始游戏。点击立即前往

+
+
+{/if} + diff --git a/AquaNet/src/components/settings/ChuniSettings.svelte b/AquaNet/src/components/settings/ChuniSettings.svelte index ba472700..97524d78 100644 --- a/AquaNet/src/components/settings/ChuniSettings.svelte +++ b/AquaNet/src/components/settings/ChuniSettings.svelte @@ -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 = { + 0: "BASIC", + 1: "ADVANCED", + 2: "EXPERT", + 3: "MASTER", + 4: "ULTIMA" + } as const // WORLD'S END scores not supported by Tachi + const DAN_MAP: Record = { + 1: "DAN_I", + 2: "DAN_II", + 3: "DAN_III", + 4: "DAN_IV", + 5: "DAN_V", + 6: "DAN_INFINITE" + } as const + const SKILL_IDS: Record = { + 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 @@ {t('settings.export')} + {/if} diff --git a/AquaNet/src/components/settings/GeneralGameSettings.svelte b/AquaNet/src/components/settings/GeneralGameSettings.svelte index 07146fd1..d400792e 100644 --- a/AquaNet/src/components/settings/GeneralGameSettings.svelte +++ b/AquaNet/src/components/settings/GeneralGameSettings.svelte @@ -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); @@ -22,6 +23,11 @@ +
+
+ {ts("settings.regionNotice")} +
+ diff --git a/AquaNet/src/components/settings/RegionSelector.svelte b/AquaNet/src/components/settings/RegionSelector.svelte new file mode 100644 index 00000000..3b761dd2 --- /dev/null +++ b/AquaNet/src/components/settings/RegionSelector.svelte @@ -0,0 +1,59 @@ + + +
+ + +
+ + + + diff --git a/AquaNet/src/libs/generalTypes.ts b/AquaNet/src/libs/generalTypes.ts index 5dd4cd12..715722b0 100644 --- a/AquaNet/src/libs/generalTypes.ts +++ b/AquaNet/src/libs/generalTypes.ts @@ -19,6 +19,7 @@ export interface AquaNetUser { email: string displayName: string country: string + region:string lastLogin: number regTime: number profileLocation: string diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts index 1b8cab27..eb9b9ab5 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -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 = { diff --git a/AquaNet/src/libs/i18n/zh.ts b/AquaNet/src/libs/i18n/zh.ts index fa37cf36..a56eefda 100644 --- a/AquaNet/src/libs/i18n/zh.ts +++ b/AquaNet/src/libs/i18n/zh.ts @@ -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 = { diff --git a/AquaNet/src/libs/sdk.ts b/AquaNet/src/libs/sdk.ts index 9cf7d173..c6e36a72 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -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 => { @@ -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 } diff --git a/AquaNet/src/pages/Home.svelte b/AquaNet/src/pages/Home.svelte index 1faa9162..bd5b275f 100644 --- a/AquaNet/src/pages/Home.svelte +++ b/AquaNet/src/pages/Home.svelte @@ -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 @@ + {#if me} + + {/if} {:else if tab === 1}
diff --git a/AquaNet/src/pages/Home/MigrateAction.svelte b/AquaNet/src/pages/Home/MigrateAction.svelte new file mode 100644 index 00000000..530d2e28 --- /dev/null +++ b/AquaNet/src/pages/Home/MigrateAction.svelte @@ -0,0 +1,69 @@ + + +{#if shouldShow} + +

迁移到 MuNET

+ 更适合中国宝宝体质的服务器,AquaDX 的继任者。点击查看详情 +
+{/if} + +{#if showWarning} +
+
+

提示

+

看起来你在 AquaDX 还没有游戏数据,也许是因为没有绑卡或者绑定的卡不是在游戏中点击“查看卡号”获取的…

+

现在迁移的话,大概会导致你的游戏数据无法被正确的迁移。建议你先去检查一下吧

+
+ + +
+
+
+{/if} + + diff --git a/AquaNet/src/pages/Transfer/TransferServer.svelte b/AquaNet/src/pages/Transfer/TransferServer.svelte index 9e0d6b7d..5160c872 100644 --- a/AquaNet/src/pages/Transfer/TransferServer.svelte +++ b/AquaNet/src/pages/Transfer/TransferServer.svelte @@ -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 diff --git a/AquaNet/src/pages/Welcome.svelte b/AquaNet/src/pages/Welcome.svelte index 669c56eb..aeebd01d 100644 --- a/AquaNet/src/pages/Welcome.svelte +++ b/AquaNet/src/pages/Welcome.svelte @@ -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 { 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 { + 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 { + 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} {error} {/if} -
state = 'home'} on:keypress={() => state = 'home'} - role="button" tabindex="0" class="clickable"> - - {t('back')} -
+ {#if error != t("welcome.waiting-turnstile")} +
state = 'home'} on:keypress={() => state = 'home'} + role="button" tabindex="0" class="clickable"> + + {t('back')} +
+ {/if} {#if isSignup} {/if} @@ -137,6 +211,9 @@ {isSignup ? t('welcome.btn-signup') : t('welcome.btn-login')} {/if} + {#if state === "login" && !submitting} + + {/if} {#if TURNSTILE_SITE_KEY} 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} + + {/if} +
+ {:else if state === "submitreset"} + {:else if state === "verify"} + {:else if state === "reset"} + {#if error} + {error} + {/if} + {/if} diff --git a/build.gradle.kts b/build.gradle.kts index 510a4674..ccbe7f91 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) +} diff --git a/config/application.properties b/config/application.properties index b191d170..bcda3649 100644 --- a/config/application.properties +++ b/config/application.properties @@ -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 diff --git a/docs/api-v2.md b/docs/api-v2.md index 8589b3a3..01ffa9b0 100644 --- a/docs/api-v2.md +++ b/docs/api-v2.md @@ -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 diff --git a/src/main/java/ext/Json.kt b/src/main/java/ext/Json.kt index 61cd6545..d8a7e4c5 100644 --- a/src/main/java/ext/Json.kt +++ b/src/main/java/ext/Json.kt @@ -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() { 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 // } -//} \ No newline at end of file +//} diff --git a/src/main/java/icu/samnyan/aqua/net/Fedy.kt b/src/main/java/icu/samnyan/aqua/net/Fedy.kt new file mode 100644 index 00000000..b6a89470 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/Fedy.kt @@ -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> 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 + 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 + } + } +} diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index a1e6c519..bf9aed47 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -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 + } } diff --git a/src/main/java/icu/samnyan/aqua/net/components/Email.kt b/src/main/java/icu/samnyan/aqua/net/components/Email.kt index 0129dfa6..0dce7d5d 100644 --- a/src/main/java/icu/samnyan/aqua/net/components/Email.kt +++ b/src/main/java/icu/samnyan/aqua/net/components/Email.kt @@ -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( } } -} \ No newline at end of file +} diff --git a/src/main/java/icu/samnyan/aqua/net/components/JWT.kt b/src/main/java/icu/samnyan/aqua/net/components/JWT.kt index 4023e751..1524b7a1 100644 --- a/src/main/java/icu/samnyan/aqua/net/components/JWT.kt +++ b/src/main/java/icu/samnyan/aqua/net/components/JWT.kt @@ -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 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 auth(token: Str, block: (AquaNetUser) -> T) = block(auth(token)) } \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt new file mode 100644 index 00000000..98a0281c --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt @@ -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 { + fun findByToken(token: String): ResetPassword? + fun findByAquaNetUserAuId(auId: Long): List +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt new file mode 100644 index 00000000..f4f3c724 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt @@ -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 { + fun findByToken(token: String): SessionToken? + fun findByAquaNetUserAuId(auId: Long): List +} diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt index 70ff544c..8651f41a 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt @@ -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 { fun findByEmailIgnoreCase(email: String): AquaNetUser? fun findByUsernameIgnoreCase(username: String): AquaNetUser? fun findByKeychip(keychip: String): AquaNetUser? + fun findByGhostCardExtId(extId: Long): AquaNetUser? } data class SettingField( diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUserFedy.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUserFedy.kt new file mode 100644 index 00000000..b7833f7b --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUserFedy.kt @@ -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 { + fun findByAquaNetUserAuId(auId: Long): AquaNetUserFedy? + fun deleteByAquaNetUserAuId(auId: Long): Unit +} diff --git a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt index 521d316c..b1b43a4d 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt @@ -92,7 +92,7 @@ abstract class GameApiController(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 diff --git a/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt b/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt index 1156eaeb..f9e822b3 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt @@ -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( @@ -54,6 +60,8 @@ abstract class ImportController, UserModel: val exportFields: Map>, val exportRepos: Map, IUserRepo>, val artemisRenames: Map>, + val customExporters: Map, (UserModel, ExportOptions) -> Any?> = emptyMap(), + val customImporters: Map, (ExportModel, UserModel) -> Unit> = emptyMap() ) { abstract fun createEmpty(): ExportModel abstract val userDataRepo: GenericUserDataRepo @@ -62,6 +70,7 @@ abstract class ImportController, 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, 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, UserModel: val lists = listRepos.toList().associate { (f, r) -> r to f.get(export) as List> }.vNotNull() val singles = singleRepos.toList().associate { (f, r) -> r to f.get(export) as IUserEntity }.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, UserModel: // Save new data singles.forEach { (repo, single) -> (repo as IUserRepo).save(single) } lists.forEach { (repo, list) -> (repo as IUserRepo).saveAll(list) } + // Handle custom importers + customImporters.forEach { (field, importer) -> + importer(export, nu) + } } + Fedy.getGameName(game)?.let { fedy.onImported(it, u.ghostCard.extId) } + SUCCESS } diff --git a/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt b/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt index ee5dc278..f53da34c 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt @@ -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, (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, (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, - var userActList: List, - var userCharacterList: List, - var userChargeList: List, - var userCourseList: List, - var userFavoriteList: List, - var userFriendSeasonRankingList: List, - var userGeneralDataList: List, - var userItemList: List, - var userLoginBonusList: List, - var userMapList: List, - var userMusicDetailList: List, - var userPlaylogList: List, + override var userData: Mai2UserDetail = Mai2UserDetail(), + var userExtend: Mai2UserExtend = Mai2UserExtend(), + var userOption: Mai2UserOption = Mai2UserOption(), + var userUdemae: Mai2UserUdemae = Mai2UserUdemae(), + var mapEncountNpcList: List = mutableListOf(), + var userActList: List = mutableListOf(), + var userCharacterList: List = mutableListOf(), + var userChargeList: List = mutableListOf(), + var userCourseList: List = mutableListOf(), + var userFavoriteList: List = mutableListOf(), + var userFriendSeasonRankingList: List = mutableListOf(), + var userGeneralDataList: List = mutableListOf(), + var userItemList: List = mutableListOf(), + var userLoginBonusList: List = mutableListOf(), + var userMapList: List = mutableListOf(), + var userMusicDetailList: List = mutableListOf(), + var userIntimateList: List = mutableListOf(), + var userFavoriteMusicList: List = mutableListOf(), + var userKaleidxScopeList: List = mutableListOf(), + var userPlaylogList: List = mutableListOf(), + // Not supported yet: + // var userWeeklyData + // var userMissionDataList + // var userShopStockList + // var userTradeItemList override var gameId: String = "SDEZ", -): IExportClass { - constructor() : this(Mai2UserDetail(), Mai2UserExtend(), Mai2UserOption(), Mai2UserUdemae(), - mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), - mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), - mutableListOf()) -} \ No newline at end of file +): IExportClass diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt index 00a88c86..c0c6a6b4 100644 --- a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt @@ -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 diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt index b8a55e4a..76215d10 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt @@ -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() diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanUpsertApis.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanUpsertApis.kt index ccbe9f45..eb54f709 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanUpsertApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanUpsertApis.kt @@ -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 diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt index 936c316f..b2a10d4a 100644 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/Chu3Repos.kt @@ -174,6 +174,10 @@ interface Chu3GameLoginBonusRepo : JpaRepository { fun findByRequiredDays(version: Int, presetId: Int, requiredDays: Int): Optional } +interface Chu3UserRegionsRepo: Chu3UserLinked { + 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, diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserRegions.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserRegions.kt new file mode 100644 index 00000000..43c18e82 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserRegions.kt @@ -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() +} diff --git a/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt b/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt index 8459652b..6bda26e1 100644 --- a/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt +++ b/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt @@ -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() - 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)} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt index b5e4609b..0e068447 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt @@ -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) ) } -} \ No newline at end of file +} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt index abf15797..f1dfe3d4 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt @@ -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() @@ -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) { diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt index 309b95ce..5883101a 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt @@ -57,8 +57,6 @@ interface Mai2UserExtendRepo : Mai2UserLinked interface Mai2UserFavoriteRepo : Mai2UserLinked { fun findByUserAndItemKind(user: Mai2UserDetail, kind: Int): Optional - fun findByUserIdAndItemKind(userId: Long, kind: Int): List - fun findByUser_Card_ExtIdAndItemKind(userId: Long, kind: Int): Optional } @@ -104,6 +102,7 @@ interface Mai2UserPlaylogRepo : GenericPlaylogRepo, Mai2UserLin musicId: Int, userPlayDate: String ): MutableList + fun findByUserAndUserPlayDateAfter(user: Mai2UserDetail, userPlayDate: String): List } interface Mai2UserPrintDetailRepo : JpaRepository @@ -126,6 +125,10 @@ interface Mai2GameEventRepo : JpaRepository { interface Mai2GameSellingCardRepo : JpaRepository +interface Mai2UserRegionsRepo: Mai2UserLinked { + 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, ) diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt index 076b7e53..f2187650 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/userdata/UserEntities.kt @@ -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 { @@ -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() { + override fun serialize(v: LocalDateTime, j: JsonGenerator, s: SerializerProvider) { + j.writeString(v.format(MAIMAI_DATETIME)) + } +} diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiRepos.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiRepos.kt index 28a41842..e54cfc18 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiRepos.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiRepos.kt @@ -147,6 +147,10 @@ interface OgkUserTrainingRoomRepo : OngekiUserLinked { fun findByUserAndRoomId(user: UserData, roomId: Int): Optional } +interface OgkUserRegionsRepo: OngekiUserLinked { + fun findByUserAndRegionId(user: UserData, regionId: Int): UserRegions? +} + // Re:Fresh interface OgkUserEventMapRepo : OngekiUserLinked interface OgkUserSkinRepo : OngekiUserLinked @@ -190,6 +194,7 @@ class OngekiUserRepos( val trainingRoom: OgkUserTrainingRoomRepo, val eventMap: OgkUserEventMapRepo, val skin: OgkUserSkinRepo, + val regions: OgkUserRegionsRepo, ) @Component diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUpsertAllApi.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUpsertAllApi.kt index ed38ad93..5fc4c602 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUpsertAllApi.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUpsertAllApi.kt @@ -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( diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUserApis.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUserApis.kt index e3027851..c2b14bd2 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUserApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUserApis.kt @@ -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) } -} \ No newline at end of file +} diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt index 59fadaef..1e971d8d 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/model/OngekiUserEntities.kt @@ -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 { @@ -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() } \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt b/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt index df4fdc1e..fc02d532 100644 --- a/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt +++ b/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt @@ -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( diff --git a/src/main/resources/db/80/V1000_41__aqua_net_user_fedy.sql b/src/main/resources/db/80/V1000_41__aqua_net_user_fedy.sql new file mode 100644 index 00000000..502b3813 --- /dev/null +++ b/src/main/resources/db/80/V1000_41__aqua_net_user_fedy.sql @@ -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) +); \ No newline at end of file diff --git a/src/main/resources/db/80/v1000_41__chusan_verse.sql b/src/main/resources/db/80/V1000_51__chusan_verse_event_122_to_151.sql similarity index 98% rename from src/main/resources/db/80/v1000_41__chusan_verse.sql rename to src/main/resources/db/80/V1000_51__chusan_verse_event_122_to_151.sql index 86e7ef5c..52bf92aa 100644 --- a/src/main/resources/db/80/v1000_41__chusan_verse.sql +++ b/src/main/resources/db/80/V1000_51__chusan_verse_event_122_to_151.sql @@ -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); \ No newline at end of file diff --git a/src/main/resources/db/80/V1000_52__cardmaker_event.sql b/src/main/resources/db/80/V1000_52__cardmaker_event.sql new file mode 100644 index 00000000..4b3c87e6 --- /dev/null +++ b/src/main/resources/db/80/V1000_52__cardmaker_event.sql @@ -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'); diff --git a/src/main/resources/db/80/V1000_53__chusan_verse_event_181_to_191.sql b/src/main/resources/db/80/V1000_53__chusan_verse_event_181_to_191.sql new file mode 100644 index 00000000..6d5ebf3d --- /dev/null +++ b/src/main/resources/db/80/V1000_53__chusan_verse_event_181_to_191.sql @@ -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); \ No newline at end of file diff --git a/src/main/resources/db/80/V1000_54__chusan_verse_event_172.sql b/src/main/resources/db/80/V1000_54__chusan_verse_event_172.sql new file mode 100644 index 00000000..d1aabb72 --- /dev/null +++ b/src/main/resources/db/80/V1000_54__chusan_verse_event_172.sql @@ -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); diff --git a/src/main/resources/db/80/V1000_55__net_session.sql b/src/main/resources/db/80/V1000_55__net_session.sql new file mode 100644 index 00000000..47fdf780 --- /dev/null +++ b/src/main/resources/db/80/V1000_55__net_session.sql @@ -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); \ No newline at end of file diff --git a/src/main/resources/db/80/V1000_56__prefectures.sql b/src/main/resources/db/80/V1000_56__prefectures.sql new file mode 100644 index 00000000..c3e0b9e8 --- /dev/null +++ b/src/main/resources/db/80/V1000_56__prefectures.sql @@ -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'; \ No newline at end of file diff --git a/src/main/resources/email/confirm.html b/src/main/resources/email/confirm.html index d3be2da1..72908b92 100644 --- a/src/main/resources/email/confirm.html +++ b/src/main/resources/email/confirm.html @@ -212,7 +212,7 @@

Dear {{name}},

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.

-

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.

+

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.

@@ -225,7 +225,7 @@
- Confirm email + Verify email diff --git a/src/main/resources/email/reset.html b/src/main/resources/email/reset.html new file mode 100644 index 00000000..0d3ff41b --- /dev/null +++ b/src/main/resources/email/reset.html @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 2544600f..f908dbba 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -22,7 +22,7 @@ ${LOG_FILE} ${LOG_FILE}.%d{yyyy-MM-dd}.gz - 7 + 90