mirror of
https://github.com/MewoLab/AquaDX.git
synced 2025-10-25 12:02:40 +00:00
[+] Data support APIs (#151)
This commit is contained in:
parent
068b6179e5
commit
d79a4e5499
@ -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 }
|
||||
|
||||
@ -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
|
||||
|
||||
203
src/main/java/icu/samnyan/aqua/net/Fedy.kt
Normal file
203
src/main/java/icu/samnyan/aqua/net/Fedy.kt
Normal file
@ -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<UserData : IUserData, UserRepo : GenericUserDataRepo<UserData>> removeOldData(repo: UserRepo) {
|
||||
val oldData = repo.findByCard_ExtId(extId)
|
||||
if (oldData.isPresent) {
|
||||
log.info("Fedy: Deleting old data for $extId (${req.game})")
|
||||
repo.delete(oldData.get());
|
||||
repo.flush()
|
||||
}
|
||||
}
|
||||
transaction.execute { when (req.game) {
|
||||
"mai2" -> {
|
||||
if (req.removeOldData) { removeOldData(mai2UserDataRepo) }
|
||||
val playlogs = req.data["userPlaylogList"] as List<JDict>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -98,6 +98,7 @@ interface AquaNetUserRepo : JpaRepository<AquaNetUser, Long> {
|
||||
fun findByEmailIgnoreCase(email: String): AquaNetUser?
|
||||
fun findByUsernameIgnoreCase(username: String): AquaNetUser?
|
||||
fun findByKeychip(keychip: String): AquaNetUser?
|
||||
fun findByGhostCardExtId(extId: Long): AquaNetUser?
|
||||
}
|
||||
|
||||
data class SettingField(
|
||||
|
||||
29
src/main/java/icu/samnyan/aqua/net/db/AquaNetUserFedy.kt
Normal file
29
src/main/java/icu/samnyan/aqua/net/db/AquaNetUserFedy.kt
Normal file
@ -0,0 +1,29 @@
|
||||
package icu.samnyan.aqua.net.db
|
||||
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
@Table(name = "aqua_net_user_fedy")
|
||||
class AquaNetUserFedy(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long = 0,
|
||||
|
||||
@Column(nullable = false)
|
||||
var createdAt: Instant = Instant.now(),
|
||||
|
||||
// Linking to the AquaNetUser
|
||||
@OneToOne
|
||||
@JoinColumn(name = "auId", referencedColumnName = "auId")
|
||||
var aquaNetUser: AquaNetUser,
|
||||
) : Serializable
|
||||
|
||||
@Repository
|
||||
interface AquaNetUserFedyRepo : JpaRepository<AquaNetUserFedy, Long> {
|
||||
fun findByAquaNetUserAuId(auId: Long): AquaNetUserFedy?
|
||||
fun deleteByAquaNetUserAuId(auId: Long): Unit
|
||||
}
|
||||
@ -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<ExportModel: IExportClass<UserModel>, 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<ExportModel: IExportClass<UserModel>, UserModel:
|
||||
}
|
||||
}
|
||||
|
||||
Fedy.getGameName(game)?.let { fedy?.onImported(it, u.ghostCard.extId) }
|
||||
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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<Any>()
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
);
|
||||
Loading…
x
Reference in New Issue
Block a user