From 2563a31d15557525bf742dfbf28be70adf017949 Mon Sep 17 00:00:00 2001 From: Raymond <101374892+raymonable@users.noreply.github.com> Date: Sat, 31 May 2025 16:32:42 -0400 Subject: [PATCH 01/44] fix: :art: migrate from / to /confirm for email confirmation --- AquaNet/src/App.svelte | 1 + AquaNet/src/pages/Welcome.svelte | 31 ++++++++++--------- .../icu/samnyan/aqua/net/components/Email.kt | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/AquaNet/src/App.svelte b/AquaNet/src/App.svelte index cc9d363e..8e5a8944 100644 --- a/AquaNet/src/App.svelte +++ b/AquaNet/src/App.svelte @@ -79,6 +79,7 @@ + diff --git a/AquaNet/src/pages/Welcome.svelte b/AquaNet/src/pages/Welcome.svelte index 669c56eb..d655c93b 100644 --- a/AquaNet/src/pages/Welcome.svelte +++ b/AquaNet/src/pages/Welcome.svelte @@ -24,23 +24,26 @@ if (USER.isLoggedIn()) { window.location.href = "/home" } +if (location.pathname !== '/') { + location.href = `/${params.get('confirm-email') ? `?confirm-email=${params.get('confirm-email')}` : ""}` + } else + if (params.get('confirm-email')) { - if (params.get('confirm-email')) { - state = 'verify' - verifyMsg = t("welcome.verifying") - submitting = true + 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(params.get('confirm-email')!) + .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 })) - } + // Clear the query param + window.history.replaceState({}, document.title, window.location.pathname) + }) + .catch(e => verifyMsg = t('welcome.verification-failed', { message: e.message })) + } async function submit(): Promise { submitting = true 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..2acded03 100644 --- a/src/main/java/icu/samnyan/aqua/net/components/Email.kt +++ b/src/main/java/icu/samnyan/aqua/net/components/Email.kt @@ -76,7 +76,7 @@ class EmailService( .withSubject("Confirm Your Email Address for AquaNet") .withHTMLText(confirmTemplate .replace("{{name}}", user.computedName) - .replace("{{url}}", "https://${props.webHost}?confirm-email=$token")) + .replace("{{url}}", "https://${props.webHost}/confirm?confirm-email=$token")) .buildEmail()).thenRun { log.info("Confirmation email sent to ${user.email}") } } From 88d4a3d2989b40fa267b2b54ce67f399f0f05f64 Mon Sep 17 00:00:00 2001 From: Raymond <101374892+raymonable@users.noreply.github.com> Date: Sat, 14 Jun 2025 16:43:29 -0400 Subject: [PATCH 02/44] style: move from "confirm" to "verify" by May's request --- AquaNet/src/App.svelte | 2 +- AquaNet/src/pages/Welcome.svelte | 6 +++--- src/main/java/icu/samnyan/aqua/net/components/Email.kt | 8 ++++---- src/main/resources/email/confirm.html | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/AquaNet/src/App.svelte b/AquaNet/src/App.svelte index 8e5a8944..8483a0b4 100644 --- a/AquaNet/src/App.svelte +++ b/AquaNet/src/App.svelte @@ -79,7 +79,7 @@ - + diff --git a/AquaNet/src/pages/Welcome.svelte b/AquaNet/src/pages/Welcome.svelte index d655c93b..2e2d1d04 100644 --- a/AquaNet/src/pages/Welcome.svelte +++ b/AquaNet/src/pages/Welcome.svelte @@ -25,16 +25,16 @@ window.location.href = "/home" } if (location.pathname !== '/') { - location.href = `/${params.get('confirm-email') ? `?confirm-email=${params.get('confirm-email')}` : ""}` + location.href = `/${params.get('code') ? `?code=${params.get('code')}` : ""}` } else - if (params.get('confirm-email')) { + if (params.get('code')) { state = 'verify' verifyMsg = t("welcome.verifying") submitting = true // Send request to server - USER.confirmEmail(params.get('confirm-email')!) + USER.confirmEmail(params.get('code')!) .then(() => { verifyMsg = t('welcome.verified') submitting = false 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 2acded03..75269b6c 100644 --- a/src/main/java/icu/samnyan/aqua/net/components/Email.kt +++ b/src/main/java/icu/samnyan/aqua/net/components/Email.kt @@ -69,15 +69,15 @@ 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("Verification Your Email Address for AquaNet") .withHTMLText(confirmTemplate .replace("{{name}}", user.computedName) - .replace("{{url}}", "https://${props.webHost}/confirm?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}") } } fun testEmail(addr: Str, name: Str) { 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 From 71512bdad4c0ee02d6d34a1ac3ea2a34490eb32f Mon Sep 17 00:00:00 2001 From: Raymond <101374892+raymonable@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:43:53 -0400 Subject: [PATCH 03/44] fix: typo --- src/main/java/icu/samnyan/aqua/net/components/Email.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 75269b6c..21f62cfa 100644 --- a/src/main/java/icu/samnyan/aqua/net/components/Email.kt +++ b/src/main/java/icu/samnyan/aqua/net/components/Email.kt @@ -73,7 +73,7 @@ class EmailService( mailer.sendMail(EmailBuilder.startingBlank() .from(props.senderName, props.senderAddr) .to(user.computedName, user.email) - .withSubject("Verification Your Email Address for AquaNet") + .withSubject("Verify Your Email Address for AquaNet") .withHTMLText(confirmTemplate .replace("{{name}}", user.computedName) .replace("{{url}}", "https://${props.webHost}/verify?code=$token")) @@ -93,4 +93,4 @@ class EmailService( } } -} \ No newline at end of file +} From 155202dab9d64db9ce25440e00e035eb9661ce19 Mon Sep 17 00:00:00 2001 From: Clansty Date: Mon, 23 Jun 2025 09:41:49 +0800 Subject: [PATCH 04/44] chore: hide migrated cards in ranking --- src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5c1f659437bce7ae44515e26a742a527ad3fdd83 Mon Sep 17 00:00:00 2001 From: Menci Date: Wed, 25 Jun 2025 01:03:19 +0800 Subject: [PATCH 05/44] export options --- config/application.properties | 2 +- .../samnyan/aqua/net/games/ImportController.kt | 18 +++++++++++++++--- .../samnyan/aqua/net/games/mai2/Mai2Import.kt | 16 ++++++++++++++-- .../samnyan/aqua/sega/maimai2/model/Repos.kt | 1 + 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/config/application.properties b/config/application.properties index b191d170..3fdc3fed 100644 --- a/config/application.properties +++ b/config/application.properties @@ -30,7 +30,7 @@ allnet.server.redirect=https://aquadx.net ## Http Server Port ## Only change this if you have a reverse proxy running. ## The game rely on 80 port for boot up command -server.port=80 +server.port=8080 ## Static file server ## This is used to server static files in /web/ directory, which is Aquaviewer 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..e17ebb0a 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt @@ -16,6 +16,10 @@ import kotlin.io.path.Path import kotlin.io.path.writeText import kotlin.reflect.KClass +data class ExportOptions( + val playlogSince: String? = null +) + // Import class with renaming data class ImportClass( val type: KClass, @@ -54,6 +58,7 @@ abstract class ImportController, UserModel: val exportFields: Map>, val exportRepos: Map, IUserRepo>, val artemisRenames: Map>, + val customExporters: Map, (UserModel, ExportOptions) -> Any?> = emptyMap() ) { abstract fun createEmpty(): ExportModel abstract val userDataRepo: GenericUserDataRepo @@ -72,12 +77,19 @@ 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) } + val customExporter = customExporters[f] + if (customExporter != null) { + customExporter(userData, options)?.let { f.set(this, it) } + } else { + if (f returns List::class) f.set(this, u.findByUser(userData)) + else u.findSingleByUser(userData)()?.let { f.set(this, it) } + } } } 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..90f83f1c 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,6 +3,7 @@ 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 @@ -44,7 +45,18 @@ 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 = run { + mapOf( + Maimai2DataExport::userPlaylogList to { user: Mai2UserDetail, options: ExportOptions -> + if (options.playlogSince != null) { + repos.userPlaylog.findByUserAndUserPlayDateAfter(user, options.playlogSince) + } else { + repos.userPlaylog.findByUser(user) + } + } + ) as Map, (Mai2UserDetail, ExportOptions) -> Any?> + } ) { override fun createEmpty() = Maimai2DataExport() override val userDataRepo = repos.userData @@ -74,4 +86,4 @@ data class Maimai2DataExport( mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf()) -} \ No newline at end of file +} 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..3718971b 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 @@ -104,6 +104,7 @@ interface Mai2UserPlaylogRepo : GenericPlaylogRepo, Mai2UserLin musicId: Int, userPlayDate: String ): MutableList + fun findByUserAndUserPlayDateAfter(user: Mai2UserDetail, userPlayDate: String): List } interface Mai2UserPrintDetailRepo : JpaRepository From ac6cbb9dd3afaed1907b1726cf5f7439669f9755 Mon Sep 17 00:00:00 2001 From: Menci Date: Wed, 25 Jun 2025 02:12:03 +0800 Subject: [PATCH 06/44] add mai2 fields --- .../aqua/net/games/ImportController.kt | 11 +++- .../samnyan/aqua/net/games/mai2/Mai2Import.kt | 58 ++++++++++++++----- 2 files changed, 51 insertions(+), 18 deletions(-) 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 e17ebb0a..27a452be 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt @@ -58,7 +58,8 @@ abstract class ImportController, UserModel: val exportFields: Map>, val exportRepos: Map, IUserRepo>, val artemisRenames: Map>, - val customExporters: Map, (UserModel, ExportOptions) -> Any?> = emptyMap() + val customExporters: Map, (UserModel, ExportOptions) -> Any?> = emptyMap(), + val customImporters: Map, (ExportModel, UserModel) -> Unit> = emptyMap() ) { abstract fun createEmpty(): ExportModel abstract val userDataRepo: GenericUserDataRepo @@ -105,8 +106,8 @@ abstract class ImportController, UserModel: val export = json.parseJackson(exportClass.java) if (!export.gameId.equals(game, true)) 400 - "Invalid game ID" - 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() + val lists = listRepos.toList().filter { (f, _) -> f !in customImporters }.associate { (f, r) -> r to f.get(export) as List> }.vNotNull() + val singles = singleRepos.toList().filter { (f, _) -> f !in customImporters }.associate { (f, r) -> r to f.get(export) as IUserEntity }.vNotNull() // Validate new user data // Check that all ids are 0 (this should be true since all ids are @JsonIgnore) @@ -138,6 +139,10 @@ 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) + } } 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 90f83f1c..03da745c 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 @@ -9,6 +9,7 @@ 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 @@ -24,10 +25,15 @@ class Mai2Import( }, 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}") + .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), @@ -46,17 +52,36 @@ class Mai2Import( "mai2_score_best" to ImportClass(Mai2UserMusicDetail::class), "mai2_score_course" to ImportClass(Mai2UserCourse::class), ), - customExporters = run { - mapOf( - Maimai2DataExport::userPlaylogList to { user: Mai2UserDetail, options: ExportOptions -> - if (options.playlogSince != null) { - repos.userPlaylog.findByUserAndUserPlayDateAfter(user, options.playlogSince) - } else { - repos.userPlaylog.findByUser(user) - } + customExporters = mapOf( + Maimai2DataExport::userPlaylogList to { user: Mai2UserDetail, options: ExportOptions -> + if (options.playlogSince != null) { + repos.userPlaylog.findByUserAndUserPlayDateAfter(user, options.playlogSince) + } else { + repos.userPlaylog.findByUser(user) } - ) as Map, (Mai2UserDetail, ExportOptions) -> Any?> - } + }, + 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::userFavoriteMusicList to { export: Maimai2DataExport, user: Mai2UserDetail -> + val favoriteMusicList = export.userFavoriteMusicList + if (favoriteMusicList.isNotEmpty()) { + val key = "favorite_music" + val data = repos.userGeneralData.findByUserAndPropertyKey(user, key).orElse(null) + ?: Mai2UserGeneralData().apply { this.user = user; propertyKey = key } + repos.userGeneralData.save(data.apply { + propertyValue = favoriteMusicList.map { it.id }.joinToString(",") + }) + } + } + ) as Map, (Maimai2DataExport, Mai2UserDetail) -> Unit> ) { override fun createEmpty() = Maimai2DataExport() override val userDataRepo = repos.userData @@ -79,11 +104,14 @@ data class Maimai2DataExport( var userLoginBonusList: List, var userMapList: List, var userMusicDetailList: List, + var userIntimateList: List, + var userFavoriteMusicList: List, + var userKaleidxScopeList: List, var userPlaylogList: List, 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()) + mutableListOf(), mutableListOf(), mutableListOf(), mutableListOf()) } From 11dbe849cf1d9c1b2da1f472de7fa1a5c1c71805 Mon Sep 17 00:00:00 2001 From: Menci Date: Wed, 25 Jun 2025 17:59:17 +0800 Subject: [PATCH 07/44] add mai2 fields --- .../aqua/net/games/ImportController.kt | 17 +++--- .../samnyan/aqua/net/games/mai2/Mai2Import.kt | 55 +++++++++---------- 2 files changed, 35 insertions(+), 37 deletions(-) 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 27a452be..69d9b8e2 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt @@ -84,13 +84,11 @@ abstract class ImportController, UserModel: gameId = game userData = userDataRepo.findByCard(u.ghostCard) ?: (404 - "User not found") exportRepos.forEach { (f, u) -> - val customExporter = customExporters[f] - if (customExporter != null) { - customExporter(userData, options)?.let { f.set(this, it) } - } else { - if (f returns List::class) f.set(this, u.findByUser(userData)) - else u.findSingleByUser(userData)()?.let { f.set(this, it) } - } + 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) } } } @@ -106,8 +104,9 @@ abstract class ImportController, UserModel: val export = json.parseJackson(exportClass.java) if (!export.gameId.equals(game, true)) 400 - "Invalid game ID" - val lists = listRepos.toList().filter { (f, _) -> f !in customImporters }.associate { (f, r) -> r to f.get(export) as List> }.vNotNull() - val singles = singleRepos.toList().filter { (f, _) -> f !in customImporters }.associate { (f, r) -> r to f.get(export) as IUserEntity }.vNotNull() + 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) 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 03da745c..06f310d8 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 @@ -24,7 +24,7 @@ class Mai2Import( it.name.replace("List", "").lowercase() }, exportRepos = Maimai2DataExport::class.vars() - .filter { f -> f.name !in setOf("gameId", "userData") } + .filter { f -> f.name !in setOf("gameId", "userData", "userPlaylogList", "userFavoriteMusicList") } .associateWith { field -> val repoName = when (field.name) { "userKaleidxScopeList" -> "userKaleidx" @@ -70,14 +70,18 @@ class Mai2Import( } ) 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.map { it.id }.joinToString(",") + propertyValue = favoriteMusicList.sortedBy { it.orderId }.map { it.id }.joinToString(",") }) } } @@ -88,30 +92,25 @@ class Mai2Import( } 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 userIntimateList: List, - var userFavoriteMusicList: List, - var userKaleidxScopeList: 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(), 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(), mutableListOf(), mutableListOf(), mutableListOf()) -} +): IExportClass From 42b8eabb3ab25e5576460c00618ee2371e38ba82 Mon Sep 17 00:00:00 2001 From: Menci Date: Wed, 25 Jun 2025 18:00:14 +0800 Subject: [PATCH 08/44] revert --- config/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.properties b/config/application.properties index 3fdc3fed..b191d170 100644 --- a/config/application.properties +++ b/config/application.properties @@ -30,7 +30,7 @@ allnet.server.redirect=https://aquadx.net ## Http Server Port ## Only change this if you have a reverse proxy running. ## The game rely on 80 port for boot up command -server.port=8080 +server.port=80 ## Static file server ## This is used to server static files in /web/ directory, which is Aquaviewer From 3b90ac3c77f2d78f33be3919b4d22d3d0f641cd5 Mon Sep 17 00:00:00 2001 From: Menci Date: Wed, 25 Jun 2025 18:05:31 +0800 Subject: [PATCH 09/44] add stubs --- src/main/java/icu/samnyan/aqua/net/games/mai2/Mai2Import.kt | 5 +++++ 1 file changed, 5 insertions(+) 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 06f310d8..fd7a642a 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 @@ -112,5 +112,10 @@ data class Maimai2DataExport( 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 From 068b6179e52d0baef04615d48a4f3b2d7ae55411 Mon Sep 17 00:00:00 2001 From: Menci Date: Fri, 27 Jun 2025 14:03:47 +0800 Subject: [PATCH 10/44] fixup (#152) --- .../sega/maimai2/model/userdata/UserEntities.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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..211a1011 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,11 @@ 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 @MappedSuperclass open class Mai2UserEntity : BaseEntity(), IUserEntity { @@ -526,10 +531,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 +550,10 @@ class Mai2UserIntimate : Mai2UserEntity() { var intimateLevel = 0; var intimateCountRewarded = 0; } + +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)) + } +} From d79a4e5499c5c4fe64954c608d47b4e981cfb7a7 Mon Sep 17 00:00:00 2001 From: Menci Date: Fri, 4 Jul 2025 12:01:32 +0800 Subject: [PATCH 11/44] [+] Data support APIs (#151) --- AquaNet/src/libs/sdk.ts | 11 +- config/application.properties | 5 + src/main/java/icu/samnyan/aqua/net/Fedy.kt | 203 ++++++++++++++++++ .../icu/samnyan/aqua/net/db/AquaNetUser.kt | 1 + .../samnyan/aqua/net/db/AquaNetUserFedy.kt | 29 +++ .../aqua/net/games/ImportController.kt | 7 +- .../samnyan/aqua/net/games/mai2/Mai2Import.kt | 4 +- .../sega/maimai2/Maimai2ServletController.kt | 8 + .../db/40/V1000_41__add_aquanet_user_fedy.sql | 9 + 9 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 src/main/java/icu/samnyan/aqua/net/Fedy.kt create mode 100644 src/main/java/icu/samnyan/aqua/net/db/AquaNetUserFedy.kt create mode 100644 src/main/resources/db/40/V1000_41__add_aquanet_user_fedy.sql diff --git a/AquaNet/src/libs/sdk.ts b/AquaNet/src/libs/sdk.ts index 9cf7d173..3904313e 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -254,5 +254,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/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/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..c09039b7 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/Fedy.kt @@ -0,0 +1,203 @@ +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 + +@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 +@ConditionalOnProperty("aqua-net.fedy.enabled", havingValue = "true") +@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 (!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) + + notifyRemote(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) + + notifyRemote(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 playlogs = req.data["userPlaylogList"] as List + playlogs.forEach { mai2UploadUserPlaylog.handle(mapOf("userId" to extId, "userPlaylog" to it)) } + val userAll = req.data["upsertUserAll"] as JDict + mai2UpsertUserAll.handle(mapOf("userId" to extId, "upsertUserAll" to userAll)) + } + else -> 406 - "Unsupported game" + } } + + return SUCCESS + } + + fun onUpserted(game: Str, maybeExtId: Any?) = notifyRemote(FedyEvent.Upserted, game, maybeExtId) + fun onImported(game: Str, maybeExtId: Any?) = notifyRemote(FedyEvent.Imported, game, maybeExtId) + + private fun notifyRemote(event: FedyEvent, game: Str, maybeExtId: Any?) { try { + val extId = maybeExtId?.long ?: return + val user = userRepo.findByGhostCardExtId(extId) ?: return + val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) ?: return + notifyRemote(event, mapOf("auId" to user.auId, "game" to game)) + } catch (e: Exception) { + log.error("Error handling Fedy on notifyRemote($event, $game, $maybeExtId)", e) + } } + + private fun notifyRemote(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/db/AquaNetUser.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt index 70ff544c..17b90ed6 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt @@ -98,6 +98,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/ImportController.kt b/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt index 69d9b8e2..a7d6e697 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,9 +16,10 @@ 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 playlogSince: String? = null + val playlogAfter: String? = null ) // Import class with renaming @@ -68,6 +70,7 @@ abstract class ImportController, UserModel: @Autowired lateinit var netProps: AquaNetProps @Autowired lateinit var transManager: PlatformTransactionManager val trans by lazy { TransactionTemplate(transManager) } + @Autowired(required = false) @Lazy var fedy: Fedy? = null init { artemisRenames.values.forEach { @@ -144,6 +147,8 @@ abstract class ImportController, UserModel: } } + 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 fd7a642a..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 @@ -54,8 +54,8 @@ class Mai2Import( ), customExporters = mapOf( Maimai2DataExport::userPlaylogList to { user: Mai2UserDetail, options: ExportOptions -> - if (options.playlogSince != null) { - repos.userPlaylog.findByUserAndUserPlayDateAfter(user, options.playlogSince) + if (options.playlogAfter != null) { + repos.userPlaylog.findByUserAndUserPlayDateAfter(user, options.playlogAfter) } else { repos.userPlaylog.findByUser(user) } 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..e88f7371 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,9 @@ 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 /** * @author samnyan (privateamusement@protonmail.com) @@ -37,6 +40,8 @@ class Maimai2ServletController( val net: Maimai2, ): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) { + @Autowired(required = false) @Lazy var fedy: Fedy? = null + companion object { private val log = logger() private val empty = listOf() @@ -89,6 +94,9 @@ 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/resources/db/40/V1000_41__add_aquanet_user_fedy.sql b/src/main/resources/db/40/V1000_41__add_aquanet_user_fedy.sql new file mode 100644 index 00000000..14b4578f --- /dev/null +++ b/src/main/resources/db/40/V1000_41__add_aquanet_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) +); From e3486042a5d3357e2bfc362a3d0096a8fbf3c0df Mon Sep 17 00:00:00 2001 From: Menci Date: Sat, 5 Jul 2025 00:45:09 +0800 Subject: [PATCH 12/44] [F] Data import fix (#153) --- src/main/java/ext/Json.kt | 12 ++++++-- src/main/java/icu/samnyan/aqua/net/Fedy.kt | 29 ++++++++++--------- .../aqua/net/games/ImportController.kt | 4 +-- .../sega/maimai2/Maimai2ServletController.kt | 7 ++--- 4 files changed, 30 insertions(+), 22 deletions(-) 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 index c09039b7..b6a89470 100644 --- a/src/main/java/icu/samnyan/aqua/net/Fedy.kt +++ b/src/main/java/icu/samnyan/aqua/net/Fedy.kt @@ -26,6 +26,7 @@ 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") @@ -43,7 +44,6 @@ enum class FedyEvent { } @RestController -@ConditionalOnProperty("aqua-net.fedy.enabled", havingValue = "true") @API("/api/v2/fedy") class Fedy( val jwt: JWT, @@ -59,6 +59,7 @@ class Fedy( 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" } @@ -77,7 +78,7 @@ class Fedy( val userFedy = AquaNetUserFedy(aquaNetUser = user) userFedyRepo.save(userFedy) - notifyRemote(FedyEvent.Linked, mapOf("auId" to user.auId, "nonce" to nonce)) + notify(FedyEvent.Linked, mapOf("auId" to user.auId, "nonce" to nonce)) return mapOf("linkedAt" to userFedy.createdAt.toEpochMilli()) } @@ -88,7 +89,7 @@ class Fedy( val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) ?: 412 - "User not linked" userFedyRepo.delete(userFedy) - notifyRemote(FedyEvent.Unlinked, mapOf("auId" to user.auId)) + notify(FedyEvent.Unlinked, mapOf("auId" to user.auId)) return SUCCESS } @@ -140,10 +141,10 @@ class Fedy( 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)) } - val userAll = req.data["upsertUserAll"] as JDict - mai2UpsertUserAll.handle(mapOf("userId" to extId, "upsertUserAll" to userAll)) } else -> 406 - "Unsupported game" } } @@ -151,19 +152,19 @@ class Fedy( return SUCCESS } - fun onUpserted(game: Str, maybeExtId: Any?) = notifyRemote(FedyEvent.Upserted, game, maybeExtId) - fun onImported(game: Str, maybeExtId: Any?) = notifyRemote(FedyEvent.Imported, game, maybeExtId) + fun onUpserted(game: Str, maybeExtId: Any?) = maybeNotifyAsync(FedyEvent.Upserted, game, maybeExtId) + fun onImported(game: Str, maybeExtId: Any?) = maybeNotifyAsync(FedyEvent.Imported, game, maybeExtId) - private fun notifyRemote(event: FedyEvent, game: Str, maybeExtId: Any?) { try { - val extId = maybeExtId?.long ?: return - val user = userRepo.findByGhostCardExtId(extId) ?: return - val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) ?: return - notifyRemote(event, mapOf("auId" to user.auId, "game" to game)) + 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 notifyRemote($event, $game, $maybeExtId)", e) + log.error("Error handling Fedy on maybeNotifyAsync($event, $game, $maybeExtId)", e) } } - private fun notifyRemote(event: FedyEvent, body: Any?) { + private fun notify(event: FedyEvent, body: Any?) { val MAX_RETRY = 3 val body = body?.toJson() ?: "{}" var retry = 0 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 a7d6e697..f9e822b3 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/ImportController.kt @@ -70,7 +70,7 @@ abstract class ImportController, UserModel: @Autowired lateinit var netProps: AquaNetProps @Autowired lateinit var transManager: PlatformTransactionManager val trans by lazy { TransactionTemplate(transManager) } - @Autowired(required = false) @Lazy var fedy: Fedy? = null + @Autowired @Lazy lateinit var fedy: Fedy init { artemisRenames.values.forEach { @@ -147,7 +147,7 @@ abstract class ImportController, UserModel: } } - Fedy.getGameName(game)?.let { fedy?.onImported(it, u.ghostCard.extId) } + Fedy.getGameName(game)?.let { fedy.onImported(it, u.ghostCard.extId) } SUCCESS } 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 e88f7371..f1dfe3d4 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt @@ -17,6 +17,7 @@ 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) @@ -40,7 +41,7 @@ class Maimai2ServletController( val net: Maimai2, ): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) { - @Autowired(required = false) @Lazy var fedy: Fedy? = null + @Autowired @Lazy lateinit var fedy: Fedy companion object { private val log = logger() @@ -94,9 +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"]) - } + if (api == "UpsertUserAllApi") { fedy.onUpserted("mai2", data["userId"]) } } } } catch (e: Exception) { From 2430b8c4486c29ad8b46e5763ac6c069b08ce45b Mon Sep 17 00:00:00 2001 From: Clansty Date: Sun, 6 Jul 2025 16:29:36 +0800 Subject: [PATCH 13/44] [+] Auto redirect when migrated --- AquaNet/src/pages/Welcome.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AquaNet/src/pages/Welcome.svelte b/AquaNet/src/pages/Welcome.svelte index 2e2d1d04..6a1b0118 100644 --- a/AquaNet/src/pages/Welcome.svelte +++ b/AquaNet/src/pages/Welcome.svelte @@ -97,6 +97,9 @@ if (location.pathname !== '/') { 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 From a98db63bec1a422d27c31342d55e2b8fecc4118e Mon Sep 17 00:00:00 2001 From: Adelyn Flowers <111309743+adelynflowers@users.noreply.github.com> Date: Wed, 9 Jul 2025 02:56:39 -0400 Subject: [PATCH 14/44] Add missing chusan opts from bad migration (#155) --- ...1000_51__chusan_verse_event_122_to_151.sql | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/main/resources/db/80/V1000_51__chusan_verse_event_122_to_151.sql diff --git a/src/main/resources/db/80/V1000_51__chusan_verse_event_122_to_151.sql b/src/main/resources/db/80/V1000_51__chusan_verse_event_122_to_151.sql new file mode 100644 index 00000000..52bf92aa --- /dev/null +++ b/src/main/resources/db/80/V1000_51__chusan_verse_event_122_to_151.sql @@ -0,0 +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), + (99001,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true); \ No newline at end of file From bd32677e9e393c0d5f6c5a0397f4b501a8457ba0 Mon Sep 17 00:00:00 2001 From: Raymond <101374892+raymonable@users.noreply.github.com> Date: Sun, 20 Jul 2025 12:03:21 -0400 Subject: [PATCH 15/44] fix: subtrophies on userbox not showing up correctly --- AquaNet/src/components/settings/ChuniSettings.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AquaNet/src/components/settings/ChuniSettings.svelte b/AquaNet/src/components/settings/ChuniSettings.svelte index ba472700..a17ffeac 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)) }] } From 5b699a2c3ccf8d2d415737d7cc34243572fe6b4d Mon Sep 17 00:00:00 2001 From: crxmsxn <59166650+asterisk727@users.noreply.github.com> Date: Thu, 24 Jul 2025 22:04:16 -0500 Subject: [PATCH 16/44] feature: Batch-Manual export for CHUNITHM (#161) --- .../components/settings/ChuniSettings.svelte | 123 ++++++++++++++++++ AquaNet/src/libs/i18n/en_ref.ts | 1 + AquaNet/src/libs/i18n/zh.ts | 1 + 3 files changed, 125 insertions(+) diff --git a/AquaNet/src/components/settings/ChuniSettings.svelte b/AquaNet/src/components/settings/ChuniSettings.svelte index a17ffeac..5e1b0fd1 100644 --- a/AquaNet/src/components/settings/ChuniSettings.svelte +++ b/AquaNet/src/components/settings/ChuniSettings.svelte @@ -106,6 +106,125 @@ .finally(() => submitting = "") } + async function exportBatchManual() { + submitting = "batchExport" + + const DIFFICULTY_MAP: Record = { + 0: "BASIC", + 1: "ADVANCED", + 2: "EXPERT", + 3: "MASTER", + 4: "ULTIMA" + } // 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" + } + const CATASTROPHY_SKILL_IDS: number[] = [100009, 102009, 103007] + const ABSOLUTE_SKILL_IDS: number[] = [100008, 101008, 102008, 103006] + const BRAVE_SKILL_IDS: number[] = [100007, 101007, 102007, 103005] // Needs to be updated every major version :( + const HARD_SKILL_IDS: number[] = [100005, 100006, 101004, 101005, 101006, 102004, 102005, 102006, 103002, 103003, 103004] // Shamelessly stolen from https://github.com/beer-psi/saekawa/commit/b3bee13e126df2f4e2a449bdf971debb8c95ba40 + + 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 level = score.level + let clearLamp = null; + let noteLamp = null; + + if (level in DIFFICULTY_MAP) { + if (score.isClear) { + if (CATASTROPHY_SKILL_IDS.includes(score.skillId)) { + clearLamp = "CATASTROPHY"; + } + else if (ABSOLUTE_SKILL_IDS.includes(score.skillId)) { + clearLamp = "ABSOLUTE"; + } + else if (BRAVE_SKILL_IDS.includes(score.skillId)) { + clearLamp = "BRAVE"; + } + else if (HARD_SKILL_IDS.includes(score.skillId)) { + clearLamp = "HARD"; + } + else { + clearLamp = "CLEAR"; + } + } + else { + clearLamp = "FAILED"; + } + + + if (score.isAllPerfect) { + 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[level], + "timeAchieved": new Date(score.userPlayDate).getTime(), + "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 +420,10 @@ {t('settings.export')} + {/if} diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts index 1b8cab27..0f523b21 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -183,6 +183,7 @@ 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." } diff --git a/AquaNet/src/libs/i18n/zh.ts b/AquaNet/src/libs/i18n/zh.ts index fa37cf36..4712364e 100644 --- a/AquaNet/src/libs/i18n/zh.ts +++ b/AquaNet/src/libs/i18n/zh.ts @@ -195,6 +195,7 @@ const zhSettings: typeof EN_REF_SETTINGS = { 'settings.profile.logout': '登出', 'settings.profile.unchanged': '未更改', 'settings.export': '导出玩家数据', + 'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi)", 'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置', 'settings.gameNotice': "这些设置仅对舞萌和华卡生效。", } From 13ffe45dc64ace011af52fc5710ac21b399ab239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=91?= Date: Fri, 25 Jul 2025 15:29:54 +0800 Subject: [PATCH 17/44] [+] CardMaker maimai event (#159) --- .../db/80/V1000_54_cardmaker_event.sql | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/resources/db/80/V1000_54_cardmaker_event.sql diff --git a/src/main/resources/db/80/V1000_54_cardmaker_event.sql b/src/main/resources/db/80/V1000_54_cardmaker_event.sql new file mode 100644 index 00000000..4b3c87e6 --- /dev/null +++ b/src/main/resources/db/80/V1000_54_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'); From 4fb815a1840848e1246b703274e66d3222c99122 Mon Sep 17 00:00:00 2001 From: Raymond <101374892+raymonable@users.noreply.github.com> Date: Sat, 26 Jul 2025 23:51:00 -0400 Subject: [PATCH 18/44] fix: correct filename --- ...V1000_54_cardmaker_event.sql => V1000_52__cardmaker_event.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/80/{V1000_54_cardmaker_event.sql => V1000_52__cardmaker_event.sql} (100%) diff --git a/src/main/resources/db/80/V1000_54_cardmaker_event.sql b/src/main/resources/db/80/V1000_52__cardmaker_event.sql similarity index 100% rename from src/main/resources/db/80/V1000_54_cardmaker_event.sql rename to src/main/resources/db/80/V1000_52__cardmaker_event.sql From 955743aecd29b2b9ea6016d59ea20bf413bffe3b Mon Sep 17 00:00:00 2001 From: Paiton Bertschy <78337764+thewiilover@users.noreply.github.com> Date: Thu, 31 Jul 2025 03:54:44 -0500 Subject: [PATCH 19/44] chore: add a172 event ids (#156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 凌莞~(=^▽^=) --- ...usan_verse.sql => V1000_41__chusan_verse.sql} | 2 +- .../db/80/V1000_52_chusan_verse_event_172.sql | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) rename src/main/resources/db/80/{v1000_41__chusan_verse.sql => V1000_41__chusan_verse.sql} (99%) create mode 100644 src/main/resources/db/80/V1000_52_chusan_verse_event_172.sql diff --git a/src/main/resources/db/80/v1000_41__chusan_verse.sql b/src/main/resources/db/80/V1000_41__chusan_verse.sql similarity index 99% rename from src/main/resources/db/80/v1000_41__chusan_verse.sql rename to src/main/resources/db/80/V1000_41__chusan_verse.sql index 86e7ef5c..c527778f 100644 --- a/src/main/resources/db/80/v1000_41__chusan_verse.sql +++ b/src/main/resources/db/80/V1000_41__chusan_verse.sql @@ -154,4 +154,4 @@ VALUES (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 + (99001,3,'2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true); diff --git a/src/main/resources/db/80/V1000_52_chusan_verse_event_172.sql b/src/main/resources/db/80/V1000_52_chusan_verse_event_172.sql new file mode 100644 index 00000000..e76d1bf6 --- /dev/null +++ b/src/main/resources/db/80/V1000_52_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); From e0d12acf61d79233a59a3e515e26bd26bd634a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=87=8C=E8=8E=9E=7E=28=3D=5E=E2=96=BD=5E=3D=29?= Date: Thu, 31 Jul 2025 16:55:15 +0800 Subject: [PATCH 20/44] [+] Chusan event A181 to A191 (#157) --- ...1000_53__chusan_verse_event_181_to_191.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/resources/db/80/V1000_53__chusan_verse_event_181_to_191.sql 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 From 82adf5c1385d60bba3c5bed83f3fd736de4c1d99 Mon Sep 17 00:00:00 2001 From: asterisk727 <59166650+asterisk727@users.noreply.github.com> Date: Sat, 26 Jul 2025 01:20:07 -0500 Subject: [PATCH 21/44] feature: password reset --- AquaNet/src/App.svelte | 1 + AquaNet/src/libs/i18n/en_ref.ts | 8 + AquaNet/src/libs/sdk.ts | 10 + AquaNet/src/pages/Welcome.svelte | 110 ++++++- .../icu/samnyan/aqua/net/UserRegistrar.kt | 48 ++++ .../icu/samnyan/aqua/net/components/Email.kt | 26 +- .../aqua/net/db/AquaEmailResetPassword.kt | 33 +++ src/main/resources/email/reset.html | 272 ++++++++++++++++++ 8 files changed, 496 insertions(+), 12 deletions(-) create mode 100644 src/main/java/icu/samnyan/aqua/net/db/AquaEmailResetPassword.kt create mode 100644 src/main/resources/email/reset.html diff --git a/AquaNet/src/App.svelte b/AquaNet/src/App.svelte index 8483a0b4..64268330 100644 --- a/AquaNet/src/App.svelte +++ b/AquaNet/src/App.svelte @@ -80,6 +80,7 @@ + diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts index 0f523b21..7fb75e88 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -34,21 +34,29 @@ 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.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.email-password-missing': 'Email and password are required', 'welcome.username-missing': 'Username/email is required', 'welcome.waiting-turnstile': 'Waiting for Turnstile to verify your network environment...', 'welcome.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.reset-password-sent': 'A password reset email has been sent to ${email}. Please check your inbox!', 'welcome.verify-state-0': 'You haven\'t verified your email. A verification email had been sent to your inbox less than a minute ago. Please check your inbox!', 'welcome.verify-state-1': 'You haven\'t verified your email. We\'ve already sent 3 emails over the last 24 hours so we\'ll not send another one. Please check your inbox!', 'welcome.verify-state-2': 'You haven\'t verified your email. We just sent you another verification email. Please check your inbox!', '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 changed! You can log in now.', } export const EN_REF_LEADERBOARD = { diff --git a/AquaNet/src/libs/sdk.ts b/AquaNet/src/libs/sdk.ts index 3904313e..bb0b65d8 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: { code: 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 => { diff --git a/AquaNet/src/pages/Welcome.svelte b/AquaNet/src/pages/Welcome.svelte index 6a1b0118..4fa30b50 100644 --- a/AquaNet/src/pages/Welcome.svelte +++ b/AquaNet/src/pages/Welcome.svelte @@ -20,31 +20,33 @@ let error = "" let verifyMsg = "" + let code = "" if (USER.isLoggedIn()) { window.location.href = "/home" } -if (location.pathname !== '/') { - location.href = `/${params.get('code') ? `?code=${params.get('code')}` : ""}` - } else - if (params.get('code')) { - + if (params.get('code')) { + code = params.get('code')! + if (location.pathname === '/verify') { state = 'verify' verifyMsg = t("welcome.verifying") submitting = true // Send request to server - USER.confirmEmail(params.get('code')!) + USER.confirmEmail(code) .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 })) + // 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 @@ -111,6 +113,52 @@ if (location.pathname !== '/') { submitting = false } + async function resetPassword(): Promise { + submitting = true; + + if (email === "") { + error = t("welcome.email-missing") + return submitting = false + } + + // 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 => { + 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({ code, password }) + .then(() => { + verifyMsg = t("welcome.password-reset-done") + }) + .catch(e => { + error = e.message + submitting = false + turnstileReset() + }) + + submitting = false + } +
@@ -143,6 +191,9 @@ if (location.pathname !== '/') { {isSignup ? t('welcome.btn-signup') : t('welcome.btn-login')} {/if} + {#if !submitting} + + {/if} {#if TURNSTILE_SITE_KEY} console.log(turnstile = e.detail.token)} @@ -151,6 +202,32 @@ if (location.pathname !== '/') { on:turnstile-timeout={_ => console.log(error = t('welcome.turnstile-timeout'))} /> {/if} + {:else if state === "submitreset"} + {:else if state === "verify"} + {:else if state === "reset"} + {/if} diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index a1e6c519..3e887c61 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -29,6 +29,7 @@ class UserRegistrar( val geoIP: GeoIP, val jwt: JWT, val confirmationRepo: EmailConfirmationRepo, + val resetPasswordRepo: ResetPasswordRepo, val cardRepo: CardRepository, val cardService: CardService, val validator: AquaUserServices, @@ -144,6 +145,53 @@ 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") // wtf is the second param in this annotation? + 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" // maybe similar logic to login here + + // 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") // again have no idea what it is + 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) }) } // how... + + 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 { 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 21f62cfa..73db2d97 100644 --- a/src/main/java/icu/samnyan/aqua/net/components/Email.kt +++ b/src/main/java/icu/samnyan/aqua/net/components/Email.kt @@ -5,6 +5,7 @@ import ext.Str import ext.logger import icu.samnyan.aqua.net.db.AquaNetUser import icu.samnyan.aqua.net.db.EmailConfirmation +import icu.samnyan.aqua.net.db.PasswordReset import icu.samnyan.aqua.net.db.EmailConfirmationRepo import org.simplejavamail.api.mailer.Mailer import org.simplejavamail.email.EmailBuilder @@ -38,10 +39,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) @@ -80,6 +84,26 @@ class EmailService( .buildEmail()).thenRun { log.info("Verification email sent to ${user.email}") } } + 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) { if (!props.enable) return 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/resources/email/reset.html b/src/main/resources/email/reset.html new file mode 100644 index 00000000..caf7fc44 --- /dev/null +++ b/src/main/resources/email/reset.html @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 39ed8af8409bb9d1d364c6272fef6b509023f073 Mon Sep 17 00:00:00 2001 From: Raymond <101374892+raymonable@users.noreply.github.com> Date: Sun, 27 Jul 2025 03:06:33 -0400 Subject: [PATCH 22/44] feat: :sparkles: swap auId in JWT for individual token note: has not been tested to ensure there are no collisions, todo --- .../icu/samnyan/aqua/net/UserRegistrar.kt | 7 + .../icu/samnyan/aqua/net/components/JWT.kt | 186 +++++++++++------- .../icu/samnyan/aqua/net/db/AquaNetSession.kt | 31 +++ .../resources/db/80/V1000_53__net_session.sql | 7 + 4 files changed, 155 insertions(+), 76 deletions(-) create mode 100644 src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt create mode 100644 src/main/resources/db/80/V1000_53__net_session.sql diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index 3e887c61..a8619727 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -34,6 +34,7 @@ class UserRegistrar( val cardService: CardService, val validator: AquaUserServices, val emailProps: EmailProperties, + val sessionRepo: SessionTokenRepo, final val paths: PathProps ) { val portraitPath = paths.aquaNetPortrait.path() @@ -233,6 +234,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 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..41d9a275 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,111 @@ -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 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(4) // the cap is 5, 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 + } + } + + 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/AquaNetSession.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt new file mode 100644 index 00000000..86416f13 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt @@ -0,0 +1,31 @@ +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 + +@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 = Instant.now().plusSeconds(3 * 86400), + + // 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 +} \ No newline at end of file diff --git a/src/main/resources/db/80/V1000_53__net_session.sql b/src/main/resources/db/80/V1000_53__net_session.sql new file mode 100644 index 00000000..bdc4c5f2 --- /dev/null +++ b/src/main/resources/db/80/V1000_53__net_session.sql @@ -0,0 +1,7 @@ +CREATE TABLE aqua_net_session +( + token VARCHAR(36) NOT NULL, + expiry datetime NOT NULL, + au_id BIGINT NULL, + CONSTRAINT pk_session PRIMARY KEY (token) +); \ No newline at end of file From c01c40fe45a9198a94477c27070f9875c100f85a Mon Sep 17 00:00:00 2001 From: asterisk727 <59166650+asterisk727@users.noreply.github.com> Date: Mon, 28 Jul 2025 22:24:53 -0700 Subject: [PATCH 23/44] fix: bug fixes to password reset (INCOMPLETE) --- AquaNet/src/libs/i18n/en_ref.ts | 2 + AquaNet/src/libs/sdk.ts | 2 +- AquaNet/src/pages/Welcome.svelte | 68 +++++++++++++------ docs/api-v2.md | 14 +++- .../icu/samnyan/aqua/net/UserRegistrar.kt | 22 ++++-- .../icu/samnyan/aqua/net/components/Email.kt | 3 + .../db/40/V1000_42__add_reset_password.sql | 8 +++ src/main/resources/email/reset.html | 4 +- 8 files changed, 92 insertions(+), 31 deletions(-) create mode 100644 src/main/resources/db/40/V1000_42__add_reset_password.sql diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts index 7fb75e88..2ce7368b 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -53,6 +53,8 @@ export const EN_REF_Welcome = { '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.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 had been sent to your inbox less than a minute ago. Please check your inbox!', + 'welcome.reset-state-1': 'We\'ve already sent 3 emails over the last 24 hours so we\'ll not send another one. Please check your inbox!', '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.', diff --git a/AquaNet/src/libs/sdk.ts b/AquaNet/src/libs/sdk.ts index bb0b65d8..8cfe382c 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -167,7 +167,7 @@ async function resetPassword(user: { email: string, turnstile: string }) { return await post('api/v2/user/reset-password', user) } -async function changePassword(user: { code: string, password: string }) { +async function changePassword(user: { token: string, password: string }) { return await post('/api/v2/user/change-password', user) } diff --git a/AquaNet/src/pages/Welcome.svelte b/AquaNet/src/pages/Welcome.svelte index 4fa30b50..14ffc31a 100644 --- a/AquaNet/src/pages/Welcome.svelte +++ b/AquaNet/src/pages/Welcome.svelte @@ -20,20 +20,20 @@ let error = "" let verifyMsg = "" - let code = "" + let token = "" if (USER.isLoggedIn()) { window.location.href = "/home" } if (params.get('code')) { - code = params.get('code')! + token = params.get('code')! if (location.pathname === '/verify') { state = 'verify' verifyMsg = t("welcome.verifying") submitting = true // Send request to server - USER.confirmEmail(code) + USER.confirmEmail(token) .then(() => { verifyMsg = t('welcome.verified') submitting = false @@ -104,7 +104,7 @@ } else { error = e.message - submitting = false + submitting = false // unnecessary? see line 113, same for both reset functions turnstileReset() } }) @@ -121,6 +121,12 @@ 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(() => { @@ -129,12 +135,22 @@ verifyMsg = t("welcome.reset-password-sent", { email }) }) .catch(e => { - error = e.message - submitting = false - turnstileReset() + 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; + submitting = false } async function changePassword(): Promise { @@ -145,9 +161,10 @@ return submitting = false } - // Send request to server - await USER.changePassword({ code, password }) + // Send request to server + await USER.changePassword({ token, password }) .then(() => { + state = 'verify' verifyMsg = t("welcome.password-reset-done") }) .catch(e => { @@ -174,11 +191,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} @@ -191,7 +210,7 @@ {isSignup ? t('welcome.btn-signup') : t('welcome.btn-login')} {/if} - {#if !submitting} + {#if state === "login" && !submitting} {/if} {#if TURNSTILE_SITE_KEY} @@ -207,11 +226,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 = 'login'} on:keypress={() => state = 'login'} + role="button" tabindex="0" class="clickable"> + + {t('back')} +
+ {/if} + + + + +{/if} + + From f6aa7d1fe358eb0d5730c4e80ed0c2cb2e2dcf7e Mon Sep 17 00:00:00 2001 From: Clansty Date: Wed, 23 Jul 2025 18:54:52 +0800 Subject: [PATCH 36/44] [+] register notice --- .../src/components/MunetRegisterBanner.svelte | 32 +++++++++++++++++++ AquaNet/src/pages/Home/MigrateAction.svelte | 3 -- AquaNet/src/pages/Welcome.svelte | 4 +++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 AquaNet/src/components/MunetRegisterBanner.svelte 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/pages/Home/MigrateAction.svelte b/AquaNet/src/pages/Home/MigrateAction.svelte index b5692ecc..530d2e28 100644 --- a/AquaNet/src/pages/Home/MigrateAction.svelte +++ b/AquaNet/src/pages/Home/MigrateAction.svelte @@ -2,9 +2,7 @@ import { fade } from "svelte/transition" import { t } from "../../libs/i18n"; import ActionCard from "../../components/ActionCard.svelte"; - import StatusOverlays from "../../components/StatusOverlays.svelte"; import { CARD, GAME, USER } from "../../libs/sdk"; - import Icon from "@iconify/svelte"; export let username: string; let shouldShow = navigator.language.startsWith('zh'); @@ -60,7 +58,6 @@ {/if} diff --git a/AquaNet/src/components/settings/RegionSelector.svelte b/AquaNet/src/components/settings/RegionSelector.svelte new file mode 100644 index 00000000..e2f74711 --- /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 6213df51..eb9b9ab5 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -195,7 +195,11 @@ export const EN_REF_SETTINGS = { '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 de3c0236..a56eefda 100644 --- a/AquaNet/src/libs/i18n/zh.ts +++ b/AquaNet/src/libs/i18n/zh.ts @@ -208,6 +208,14 @@ const zhSettings: typeof EN_REF_SETTINGS = { '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 38ae98e6..c6e36a72 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -196,6 +196,8 @@ export const USER = { }, isLoggedIn, ensureLoggedIn, + changeRegion: (regionId: number) => + post('/api/v2/user/change-region', { regionId }), } export const USERBOX = { diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index d2f39e78..bf9aed47 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -161,7 +161,7 @@ class UserRegistrar( // 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" @@ -179,7 +179,7 @@ class UserRegistrar( // Send a password reset email emailService.sendPasswordReset(user) - + return SUCCESS } @@ -189,7 +189,7 @@ class UserRegistrar( @RP token: Str, @RP password: Str, request: HttpServletRequest ) : Any { - + // Find the reset token val reset = async { resetPasswordRepo.findByToken(token) } @@ -302,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/db/AquaNetUser.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt index 17b90ed6..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, 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..af9ef82d 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 @@ -288,6 +288,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..fd07ac29 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,25 @@ 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 + if (req.userPlaylogList?.isNotEmpty() == true) { + val region = req.userPlaylogList!![0].regionId + + val userRegion = db.userRegions.findByUserIdAndRegionId(u.id, region) + if (userRegion.isPresent) { + userRegion.get().apply { + playCount += 1 + db.userRegions.save(this) + } + } else { + db.userRegions.save(UserRegions().apply { + user = u + regionId = region + playCount = 1 + }) + } + } + 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..adf1a16c 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 findByUserIdAndRegionId(userId: Long, regionId: Int): Optional +} + @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..84fdf608 --- /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 = 0 + var created: String = LocalDate.now().toString() +} 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..014bda83 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt @@ -7,6 +7,7 @@ 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() { @@ -134,6 +135,31 @@ 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 userRegion = db.userRegions.findByUserIdAndRegionId(uid, region) + if (userRegion.isPresent) { + userRegion.get().apply { + playCount += 1 + db.userRegions.save(this) + } + } else { + logger().info("user: $d") + logger().info("region: $region") + +// Create a new user region row + // Crea una nueva fila de región de usuario + db.userRegions.save(UserRegions().apply { + user = d + regionId = region + playCount = 1 + }) + } + } + res } @@ -178,13 +204,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, "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) } 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 3718971b..0263bdc9 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 @@ -127,6 +127,10 @@ interface Mai2GameEventRepo : JpaRepository { interface Mai2GameSellingCardRepo : JpaRepository +interface Mai2UserRegionsRepo: Mai2UserLinked { + fun findByUserIdAndRegionId(userId: Long, regionId: Int): Optional +} + @Component class Mai2Repos( val mapEncountNpc: Mai2MapEncountNpcRepo, @@ -152,5 +156,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 211a1011..47548f88 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 @@ -21,6 +21,7 @@ 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 { @@ -451,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 } @@ -551,6 +552,17 @@ class Mai2UserIntimate : Mai2UserEntity() { 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 = 0 + 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) { diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt index 41bf4a8d..947c9083 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt @@ -69,4 +69,10 @@ fun OngekiController.ongekiInit() { "GetClientTestmode" { empty.staticLst("clientTestmodeList") + mapOf("placeId" to data["placeId"]) } + + "GetUserRegion" { + db.regions.findByUser_Card_ExtId(uid) + .map { mapOf("regionId" to it.regionId, "playCount" to it.playCount) } + .staticLst("userRegionList") + mapOf("userId" to uid) + } } \ No newline at end of file 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..a9af626a 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 findByUserIdAndRegionId(userId: Long, regionId: Int): Optional +} + // 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..ed25e9e1 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,26 @@ 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 userRegion = db.regions.findByUserIdAndRegionId(u.id, region) + if (userRegion.isPresent) { + userRegion.get().apply { + playCount += 1 + db.regions.save(this) + } + } else { + db.regions.save(UserRegions().apply { + user = u + regionId = region + playCount = 1 + }) + } + } + all.run { // Set users listOfNotNull( 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..5b4f8752 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 = 0 + var created: String = LocalDate.now().toString() } \ 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 From fc3f2171ee37213f31fb04de456e0ff0a19df517 Mon Sep 17 00:00:00 2001 From: Raymond <101374892+raymonable@users.noreply.github.com> Date: Fri, 22 Aug 2025 06:02:22 -0400 Subject: [PATCH 41/44] revert: prefectures (temporary) --- .../settings/GeneralGameSettings.svelte | 27 +++++---- .../components/settings/RegionSelector.svelte | 59 ------------------- AquaNet/src/libs/generalTypes.ts | 1 - AquaNet/src/libs/i18n/en_ref.ts | 6 +- AquaNet/src/libs/i18n/zh.ts | 8 --- AquaNet/src/libs/sdk.ts | 2 - .../icu/samnyan/aqua/net/UserRegistrar.kt | 19 +----- .../icu/samnyan/aqua/net/db/AquaNetUser.kt | 4 -- .../icu/samnyan/aqua/sega/allnet/AllNet.kt | 6 -- .../aqua/sega/chusan/handler/ChusanApis.kt | 6 -- .../sega/chusan/handler/ChusanUpsertApis.kt | 19 ------ .../aqua/sega/chusan/model/Chu3Repos.kt | 5 -- .../sega/chusan/model/userdata/UserRegions.kt | 14 ----- .../samnyan/aqua/sega/maimai2/Maimai2Apis.kt | 34 +---------- .../samnyan/aqua/sega/maimai2/model/Repos.kt | 7 +-- .../maimai2/model/userdata/UserEntities.kt | 18 +----- .../samnyan/aqua/sega/ongeki/OngekiApis.kt | 6 -- .../samnyan/aqua/sega/ongeki/OngekiRepos.kt | 5 -- .../aqua/sega/ongeki/OngekiUpsertAllApi.kt | 22 ------- .../sega/ongeki/model/OngekiUserEntities.kt | 12 ---- .../resources/db/80/V1000_56__prefectures.sql | 38 ------------ 21 files changed, 24 insertions(+), 294 deletions(-) delete mode 100644 AquaNet/src/components/settings/RegionSelector.svelte delete mode 100644 src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserRegions.kt delete mode 100644 src/main/resources/db/80/V1000_56__prefectures.sql diff --git a/AquaNet/src/components/settings/GeneralGameSettings.svelte b/AquaNet/src/components/settings/GeneralGameSettings.svelte index d400792e..07146fd1 100644 --- a/AquaNet/src/components/settings/GeneralGameSettings.svelte +++ b/AquaNet/src/components/settings/GeneralGameSettings.svelte @@ -4,7 +4,6 @@ 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); @@ -23,11 +22,6 @@ -
-
- {ts("settings.regionNotice")} -
- diff --git a/AquaNet/src/components/settings/RegionSelector.svelte b/AquaNet/src/components/settings/RegionSelector.svelte deleted file mode 100644 index e2f74711..00000000 --- a/AquaNet/src/components/settings/RegionSelector.svelte +++ /dev/null @@ -1,59 +0,0 @@ - - -
- - -
- - - - diff --git a/AquaNet/src/libs/generalTypes.ts b/AquaNet/src/libs/generalTypes.ts index 715722b0..5dd4cd12 100644 --- a/AquaNet/src/libs/generalTypes.ts +++ b/AquaNet/src/libs/generalTypes.ts @@ -19,7 +19,6 @@ 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 eb9b9ab5..6213df51 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -195,11 +195,7 @@ export const EN_REF_SETTINGS = { '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.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", + 'settings.gameNotice': "These only apply to Mai and Wacca." } export const EN_REF_USERBOX = { diff --git a/AquaNet/src/libs/i18n/zh.ts b/AquaNet/src/libs/i18n/zh.ts index a56eefda..de3c0236 100644 --- a/AquaNet/src/libs/i18n/zh.ts +++ b/AquaNet/src/libs/i18n/zh.ts @@ -208,14 +208,6 @@ const zhSettings: typeof EN_REF_SETTINGS = { '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 c6e36a72..38ae98e6 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -196,8 +196,6 @@ export const USER = { }, isLoggedIn, ensureLoggedIn, - changeRegion: (regionId: number) => - post('/api/v2/user/change-region', { regionId }), } export const USERBOX = { diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index bf9aed47..d2f39e78 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -161,7 +161,7 @@ class UserRegistrar( // 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" @@ -179,7 +179,7 @@ class UserRegistrar( // Send a password reset email emailService.sendPasswordReset(user) - + return SUCCESS } @@ -189,7 +189,7 @@ class UserRegistrar( @RP token: Str, @RP password: Str, request: HttpServletRequest ) : Any { - + // Find the reset token val reset = async { resetPasswordRepo.findByToken(token) } @@ -302,17 +302,4 @@ 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/db/AquaNetUser.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt index 8651f41a..17b90ed6 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt @@ -43,10 +43,6 @@ 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, 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 c0c6a6b4..00a88c86 100644 --- a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt +++ b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt @@ -103,7 +103,6 @@ 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 @@ -115,10 +114,6 @@ 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 } @@ -145,7 +140,6 @@ 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 af9ef82d..b8a55e4a 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 @@ -288,12 +288,6 @@ 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 fd07ac29..ccbe9f45 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,25 +29,6 @@ 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 - if (req.userPlaylogList?.isNotEmpty() == true) { - val region = req.userPlaylogList!![0].regionId - - val userRegion = db.userRegions.findByUserIdAndRegionId(u.id, region) - if (userRegion.isPresent) { - userRegion.get().apply { - playCount += 1 - db.userRegions.save(this) - } - } else { - db.userRegions.save(UserRegions().apply { - user = u - regionId = region - playCount = 1 - }) - } - } - 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 adf1a16c..936c316f 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,10 +174,6 @@ interface Chu3GameLoginBonusRepo : JpaRepository { fun findByRequiredDays(version: Int, presetId: Int, requiredDays: Int): Optional } -interface Chu3UserRegionsRepo: Chu3UserLinked { - fun findByUserIdAndRegionId(userId: Long, regionId: Int): Optional -} - @Component class Chu3Repos( val userLoginBonus: Chu3UserLoginBonusRepo, @@ -195,7 +191,6 @@ 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 deleted file mode 100644 index 84fdf608..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserRegions.kt +++ /dev/null @@ -1,14 +0,0 @@ -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 = 0 - var created: String = LocalDate.now().toString() -} 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 014bda83..b5e4609b 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt @@ -7,7 +7,6 @@ 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() { @@ -135,31 +134,6 @@ 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 userRegion = db.userRegions.findByUserIdAndRegionId(uid, region) - if (userRegion.isPresent) { - userRegion.get().apply { - playCount += 1 - db.userRegions.save(this) - } - } else { - logger().info("user: $d") - logger().info("region: $region") - -// Create a new user region row - // Crea una nueva fila de región de usuario - db.userRegions.save(UserRegions().apply { - user = d - regionId = region - playCount = 1 - }) - } - } - res } @@ -204,19 +178,13 @@ 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, "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) } 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 0263bdc9..3718971b 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 @@ -127,10 +127,6 @@ interface Mai2GameEventRepo : JpaRepository { interface Mai2GameSellingCardRepo : JpaRepository -interface Mai2UserRegionsRepo: Mai2UserLinked { - fun findByUserIdAndRegionId(userId: Long, regionId: Int): Optional -} - @Component class Mai2Repos( val mapEncountNpc: Mai2MapEncountNpcRepo, @@ -156,6 +152,5 @@ class Mai2Repos( val userIntimate: MAi2UserIntimateRepo, val gameCharge: Mai2GameChargeRepo, val gameEvent: Mai2GameEventRepo, - val gameSellingCard: Mai2GameSellingCardRepo, - val userRegions: Mai2UserRegionsRepo, + val gameSellingCard: Mai2GameSellingCardRepo ) 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 47548f88..211a1011 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 @@ -21,7 +21,6 @@ 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 { @@ -452,9 +451,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 } @@ -552,17 +551,6 @@ class Mai2UserIntimate : Mai2UserEntity() { 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 = 0 - 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) { diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt index 947c9083..41bf4a8d 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt @@ -69,10 +69,4 @@ fun OngekiController.ongekiInit() { "GetClientTestmode" { empty.staticLst("clientTestmodeList") + mapOf("placeId" to data["placeId"]) } - - "GetUserRegion" { - db.regions.findByUser_Card_ExtId(uid) - .map { mapOf("regionId" to it.regionId, "playCount" to it.playCount) } - .staticLst("userRegionList") + mapOf("userId" to uid) - } } \ No newline at end of file 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 a9af626a..28a41842 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiRepos.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiRepos.kt @@ -147,10 +147,6 @@ interface OgkUserTrainingRoomRepo : OngekiUserLinked { fun findByUserAndRoomId(user: UserData, roomId: Int): Optional } -interface OgkUserRegionsRepo: OngekiUserLinked { - fun findByUserIdAndRegionId(userId: Long, regionId: Int): Optional -} - // Re:Fresh interface OgkUserEventMapRepo : OngekiUserLinked interface OgkUserSkinRepo : OngekiUserLinked @@ -194,7 +190,6 @@ 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 ed25e9e1..ed38ad93 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUpsertAllApi.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUpsertAllApi.kt @@ -1,13 +1,11 @@ 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() { @@ -35,26 +33,6 @@ 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 userRegion = db.regions.findByUserIdAndRegionId(u.id, region) - if (userRegion.isPresent) { - userRegion.get().apply { - playCount += 1 - db.regions.save(this) - } - } else { - db.regions.save(UserRegions().apply { - user = u - regionId = region - playCount = 1 - }) - } - } - all.run { // Set users listOfNotNull( 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 5b4f8752..59fadaef 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,7 +7,6 @@ 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 { @@ -512,15 +511,4 @@ 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 = 0 - var created: String = LocalDate.now().toString() } \ 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 deleted file mode 100644 index c3e0b9e8..00000000 --- a/src/main/resources/db/80/V1000_56__prefectures.sql +++ /dev/null @@ -1,38 +0,0 @@ -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 From 6ca419dd5bfc50af776e530e1b6d51297c6ba485 Mon Sep 17 00:00:00 2001 From: Raymond <101374892+raymonable@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:24:48 -0400 Subject: [PATCH 42/44] fix: un-revert prefectures :BocchiSobSmile: (#173) Co-authored-by: alexay7 <43906716+alexay7@users.noreply.github.com> --- .../settings/GeneralGameSettings.svelte | 27 ++++----- .../components/settings/RegionSelector.svelte | 59 +++++++++++++++++++ AquaNet/src/libs/generalTypes.ts | 1 + AquaNet/src/libs/i18n/en_ref.ts | 6 +- AquaNet/src/libs/i18n/zh.ts | 8 +++ AquaNet/src/libs/sdk.ts | 2 + .../icu/samnyan/aqua/net/UserRegistrar.kt | 19 +++++- .../icu/samnyan/aqua/net/db/AquaNetUser.kt | 4 ++ .../icu/samnyan/aqua/sega/allnet/AllNet.kt | 6 ++ .../aqua/sega/chusan/handler/ChusanApis.kt | 7 ++- .../sega/chusan/handler/ChusanUpsertApis.kt | 11 ++++ .../aqua/sega/chusan/model/Chu3Repos.kt | 5 ++ .../sega/chusan/model/userdata/UserRegions.kt | 14 +++++ .../samnyan/aqua/sega/general/BaseHandler.kt | 5 +- .../samnyan/aqua/sega/maimai2/Maimai2Apis.kt | 27 ++++++++- .../samnyan/aqua/sega/maimai2/model/Repos.kt | 9 ++- .../maimai2/model/userdata/UserEntities.kt | 18 +++++- .../samnyan/aqua/sega/ongeki/OngekiRepos.kt | 5 ++ .../aqua/sega/ongeki/OngekiUpsertAllApi.kt | 16 +++++ .../aqua/sega/ongeki/OngekiUserApis.kt | 5 +- .../sega/ongeki/model/OngekiUserEntities.kt | 12 ++++ .../resources/db/80/V1000_56__prefectures.sql | 38 ++++++++++++ 22 files changed, 274 insertions(+), 30 deletions(-) create mode 100644 AquaNet/src/components/settings/RegionSelector.svelte create mode 100644 src/main/java/icu/samnyan/aqua/sega/chusan/model/userdata/UserRegions.kt create mode 100644 src/main/resources/db/80/V1000_56__prefectures.sql 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 6213df51..eb9b9ab5 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -195,7 +195,11 @@ export const EN_REF_SETTINGS = { '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 de3c0236..a56eefda 100644 --- a/AquaNet/src/libs/i18n/zh.ts +++ b/AquaNet/src/libs/i18n/zh.ts @@ -208,6 +208,14 @@ const zhSettings: typeof EN_REF_SETTINGS = { '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 38ae98e6..c6e36a72 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -196,6 +196,8 @@ export const USER = { }, isLoggedIn, ensureLoggedIn, + changeRegion: (regionId: number) => + post('/api/v2/user/change-region', { regionId }), } export const USERBOX = { diff --git a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt index d2f39e78..bf9aed47 100644 --- a/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt +++ b/src/main/java/icu/samnyan/aqua/net/UserRegistrar.kt @@ -161,7 +161,7 @@ class UserRegistrar( // 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" @@ -179,7 +179,7 @@ class UserRegistrar( // Send a password reset email emailService.sendPasswordReset(user) - + return SUCCESS } @@ -189,7 +189,7 @@ class UserRegistrar( @RP token: Str, @RP password: Str, request: HttpServletRequest ) : Any { - + // Find the reset token val reset = async { resetPasswordRepo.findByToken(token) } @@ -302,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/db/AquaNetUser.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt index 17b90ed6..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, 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..721e4970 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 @@ -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..0bd89acc 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")) @@ -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/model/Repos.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt index 3718971b..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 } @@ -127,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, @@ -152,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 211a1011..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 @@ -21,6 +21,7 @@ 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 { @@ -451,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 } @@ -551,6 +552,17 @@ class Mai2UserIntimate : Mai2UserEntity() { 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) { 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 4c15a896..c5881fa9 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 } 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/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 From e4734924f3efa452173fa36a3cc1e129e6b1002e Mon Sep 17 00:00:00 2001 From: Clansty Date: Tue, 26 Aug 2025 20:08:13 +0800 Subject: [PATCH 43/44] [O] Change Migrated display --- .../java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt | 2 +- src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt | 2 +- src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUserApis.kt | 2 +- src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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 721e4970..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 @@ -236,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 } 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 0bd89acc..0e068447 100644 --- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt @@ -114,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 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 c5881fa9..c2b14bd2 100644 --- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUserApis.kt +++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiUserApis.kt @@ -162,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 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( From b3d0670e1d4ef397656db2a2c5b5da8bf0346b36 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:51:14 +0900 Subject: [PATCH 44/44] [F] FUCK JS --- AquaNet/src/pages/Transfer/TransferServer.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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