-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 初步完成后端API框架 1. 完成 Bomb 脚手架(模块化设计,内容序列化、反序列化,登录验证) 2. 添加 saveConfig() 方法 3. 添加查询用户自己(/user/me) 4. 添加欢迎接口(/):会返回一些随机的内容,内容来源于预言地城(命运2)的对话内容 5. 更新 .gitignore * 添加用户相关API 1. 添加用户查询(/user/{id}) 2. 添加用户注册(/user/register) 3. 添加用户查询成绩(/user/{id}/best 和 /user/{id}/last) 4. 添加用户修改名称(/user/{id}/username) 5. 添加用户修改密码(/user/{id}/password) * 将 WelcomeBO 设为 internal * 修复 BombPrincipal 假用户缺少字段编译错误 * 修复修改用户名称的逻辑错误 * 修改 RecordBO 和 UserBO 的可见性 * 扩充 BombApi 1. 添加曲目查询(/set) 2. 添加谱面查询(/chart) 3. 添加数据上传接口(/upload) * 修改 BombApi 版本为 2
- Loading branch information
Showing
26 changed files
with
745 additions
and
152 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,4 @@ build | |
logs | ||
out | ||
|
||
*.config.toml | ||
*.cfg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
plugins { | ||
kotlin("jvm") | ||
} | ||
|
||
group = "explode" | ||
version = "1.0-SNAPSHOT" | ||
|
||
repositories { | ||
mavenCentral() | ||
} | ||
|
||
dependencies { | ||
implementation(project(":booster")) | ||
implementation(project(":labyrinth")) | ||
|
||
// ktor shits | ||
implementation("io.ktor:ktor-server-cors:2.1.2") | ||
implementation("io.ktor:ktor-server-auth:2.1.2") | ||
implementation("io.ktor:ktor-server-status-pages:2.1.2") | ||
implementation("io.ktor:ktor-server-content-negotiation:2.1.2") | ||
implementation("io.ktor:ktor-serialization-gson:2.1.2") | ||
|
||
// gson (the most lenient json serializer, fuck off kotlinx.serialization!) | ||
implementation("com.google.code.gson:gson:2.9.0") | ||
|
||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.0") | ||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.0") | ||
} | ||
|
||
tasks.getByName<Test>("test") { | ||
useJUnitPlatform() | ||
} |
172 changes: 172 additions & 0 deletions
172
booster-bomb/src/main/kotlin/explode2/booster/bomb/BombPlugin.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
package explode2.booster.bomb | ||
|
||
import com.google.gson.Gson | ||
import explode2.booster.* | ||
import explode2.booster.bomb.submods.* | ||
import explode2.booster.bomb.submods.basic.WelcomeBO | ||
import explode2.booster.bomb.submods.chart.chartModule | ||
import explode2.booster.bomb.submods.chart.setModule | ||
import explode2.booster.bomb.submods.extra.newSongModule | ||
import explode2.booster.bomb.submods.user.userModule | ||
import explode2.booster.event.KtorModuleEvent | ||
import explode2.booster.event.RouteConfigure | ||
import explode2.gateau.GameUser | ||
import explode2.labyrinth.LabyrinthPlugin.Companion.labyrinth | ||
import io.ktor.http.* | ||
import io.ktor.serialization.gson.* | ||
import io.ktor.server.application.* | ||
import io.ktor.server.auth.* | ||
import io.ktor.server.plugins.contentnegotiation.* | ||
import io.ktor.server.plugins.statuspages.* | ||
import io.ktor.server.request.* | ||
import io.ktor.server.response.* | ||
import io.ktor.server.routing.* | ||
import io.ktor.util.pipeline.* | ||
import org.greenrobot.eventbus.Subscribe | ||
import org.slf4j.LoggerFactory | ||
import org.slf4j.MarkerFactory | ||
import java.lang.reflect.Type | ||
import java.util.UUID | ||
|
||
const val BombApiVersion = 2 // 0 和 1 在 Explode-Kotlin 里,反正都是异坨屎 | ||
const val BombApiVersionPatch = 0 | ||
|
||
internal val logger = LoggerFactory.getLogger("Bomb") | ||
internal val superstarMarker = MarkerFactory.getMarker("Superstar") | ||
internal val configureMarker = MarkerFactory.getMarker("Configure") | ||
|
||
class BombPlugin : BoosterPlugin { | ||
|
||
override val id: String = "bomb-api" | ||
override val version: String = "1.0.0" | ||
|
||
private val pathRaw = config.getString("route-path", "general", "bomb/v{version}", "后端根地址") | ||
private val useSuperstar = config.getBoolean("superstar-admin", "general", true, "启用 Superstar 账号") | ||
|
||
// 后门账号,用于创建初始账号 | ||
private var superstar = UUID.randomUUID().toString() | ||
|
||
init { | ||
subscribeEvents() | ||
saveConfig() | ||
|
||
if(useSuperstar) { | ||
logger.info(superstarMarker, "Superstar Enabled") | ||
logger.info(superstarMarker, "Superstar: $superstar") | ||
} | ||
} | ||
|
||
@Subscribe | ||
fun ktor(event: KtorModuleEvent) { | ||
val path = pathRaw | ||
.replace("{version}", "$BombApiVersion") // {version} -> API大版本号 | ||
.replace("{version_patch}", "$BombApiVersionPatch") // {version_patch} -> API补丁版本号 | ||
|
||
logger.info(configureMarker, "Configuring Ktor Bomb Module: $path") | ||
|
||
event.configure { | ||
install(ContentNegotiation) { | ||
gson { | ||
setPrettyPrinting() | ||
} | ||
} | ||
|
||
install(Authentication) { | ||
basic { | ||
realm = "Access to User Certification requested paths" | ||
validate { c -> | ||
if(useSuperstar && c.name == superstar) { | ||
SuperstarPrincipal | ||
} else { | ||
val username = c.name | ||
val password = c.password | ||
val user = labyrinth.gameUserFactory.getGameUserByName(username) | ||
logger.debug("Login requested with username \"$username\" and password \"$password\", found ${user != null}") | ||
if(user != null && (user.validatePassword(password) || password == superstar)) { // 密码正确或者使用 Superstar | ||
object : BombPrincipal { | ||
override val user: GameUser = user | ||
} | ||
} else { | ||
null | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
install(StatusPages) { | ||
exception<Throwable> { call, cause -> | ||
call.respondError(cause.toError("Uncaught Exception!")) | ||
} | ||
} | ||
|
||
routing { | ||
route(path, bombModule) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// 返回值序列化 | ||
private val gson = Gson() | ||
|
||
// 随机欢迎语 | ||
private val welcomeMessages = listOf( | ||
"Light or Dark — nothing changes. Everything dies.", | ||
"“What is the Dark?” they ask. “What is its nature?” Why don’t they ask the same of the Light?", | ||
"You asked a question. Welcome to the answer.", | ||
"You ask a question of pure potential. Death is intrinsic to the answer.", | ||
"Light and Dark. Heaven or hell. What’s the difference?" | ||
) | ||
|
||
internal inline val PipelineContext<*, ApplicationCall>.bombCall: BombApplicationCall get() = BombApplicationCall(context) | ||
internal class BombApplicationCall(private val delegate: ApplicationCall) : ApplicationCall by delegate { | ||
override fun toString(): String = | ||
"BombApplicationCall(requestUri=${request.uri}, requestHeaders=${request.headers}, requestCookies=${request.cookies})" | ||
} | ||
|
||
private val bombModule: RouteConfigure = { | ||
|
||
logger.debug(configureMarker, "[Module]: <default>") | ||
|
||
// <GET>[/] 用来测试的接口 | ||
get { | ||
bombCall.respondData(WelcomeBO(welcomeMessages.random()).toData()) | ||
} | ||
|
||
// 用户接口模块 | ||
logger.debug(configureMarker, "[Module]: User") | ||
route("user", userModule) | ||
// 曲目接口模块 | ||
route("set", setModule) | ||
// 谱面接口模块 | ||
route("chart", chartModule) | ||
// 上传接口模块 | ||
route("upload", newSongModule) | ||
|
||
} | ||
|
||
internal suspend fun ApplicationCall.respondJson(content: Any?, typeOfSrc: Type? = null, contentType: ContentType? = null, status: HttpStatusCode? = null) { | ||
runCatching { | ||
if(typeOfSrc != null) { // 如果提供了 Type 就用 | ||
gson.toJson(content, typeOfSrc) | ||
} else { // 否则就默认 | ||
gson.toJson(content) | ||
} | ||
}.onSuccess { | ||
// 返回数据 | ||
respondText(it, contentType, status) | ||
}.onFailure { | ||
logger.warn("Exception occurred when parsing content into json: $content", it) | ||
// 把异常返回 | ||
respondError(it.toError()) | ||
} | ||
} | ||
|
||
internal suspend fun <T> ApplicationCall.respondData(content: Data<T>) { | ||
respondJson(content, status = HttpStatusCode.OK) | ||
} | ||
|
||
internal suspend fun ApplicationCall.respondError(content: Error) { | ||
respondJson(content, status = HttpStatusCode.InternalServerError) | ||
} |
17 changes: 17 additions & 0 deletions
17
booster-bomb/src/main/kotlin/explode2/booster/bomb/Localization.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package explode2.booster.bomb | ||
|
||
object Localization { | ||
|
||
const val NotAuthenticated = "NOT_AUTHENTICATED" | ||
|
||
const val NotAuthenticatedOrUserNotFound = "NOT_AUTHENTICATED_OR_USER_NOT_FOUND" | ||
|
||
const val UserNotFound = "USER_NOT_FOUND" | ||
|
||
const val UserAlreadyExists = "USER_ALREADY_EXISTS" | ||
|
||
const val UserLoginFailed = "USER_LOGIN_FAILED" | ||
|
||
const val ResourceNotFound = "RESOURCE_NOT_FOUND" | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package explode2.booster.bomb | ||
|
||
infix fun Int.clamp(closure: IntRange): Int = when { | ||
this < closure.first -> closure.first | ||
this > closure.last -> closure.last | ||
else -> this | ||
} |
49 changes: 49 additions & 0 deletions
49
booster-bomb/src/main/kotlin/explode2/booster/bomb/submods/BasicEntities.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package explode2.booster.bomb.submods | ||
|
||
data class Data<T>( | ||
val success: Boolean, | ||
val message: String?, | ||
val data: T | ||
) | ||
|
||
fun <T> T.toData(message: String? = null, success: Boolean = true) = | ||
Data(success, message, this) | ||
|
||
data class Error( | ||
val success: Boolean, | ||
val message: String?, | ||
val stackTrace: String? | ||
) | ||
|
||
data class ExceptionContext(val message: String, val exception: Throwable) | ||
|
||
/** | ||
* 根据 [Throwable] 生成 [Error] 实例 | ||
*/ | ||
fun Throwable.toError(message: String? = null, success: Boolean = false) = | ||
Error(success, message ?: this.message, stackTraceToString()) | ||
|
||
fun toError(message: String, success: Boolean = false, needStackTrace: Boolean = false) = | ||
Error(success, message, if(needStackTrace) Exception().stackTraceToString() else null) | ||
|
||
fun toError(operationName: String, success: Boolean = false, contexts: List<ExceptionContext>) = | ||
Error(success, buildString { | ||
// single => Exception occurred when executing {OperationName}: | ||
// multiple => Exception occurred multiple times when executing {OperationName}: | ||
append("Exception occurred ") | ||
if(contexts.size != 1) append("multiple times ") | ||
append("when executing ") | ||
append(operationName) | ||
append(":") | ||
appendLine() | ||
// for each exception => [{SimpleClassName}] {Message of Context} - {Message of Exception} | ||
contexts.forEach { | ||
append("[") | ||
append(it.exception.javaClass.simpleName) | ||
append("] ") | ||
append(it.message) | ||
append(" - ") | ||
append(it.exception.message) | ||
appendLine() | ||
} | ||
}, null) |
40 changes: 40 additions & 0 deletions
40
booster-bomb/src/main/kotlin/explode2/booster/bomb/submods/BombPrincipal.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package explode2.booster.bomb.submods | ||
|
||
import explode2.booster.bomb.logger | ||
import explode2.booster.bomb.superstarMarker | ||
import explode2.gateau.* | ||
import io.ktor.server.auth.* | ||
import java.time.OffsetDateTime | ||
|
||
interface BombPrincipal : Principal { | ||
|
||
val user: GameUser? | ||
} | ||
|
||
object SuperstarPrincipal : BombPrincipal { | ||
// 假玩家数据 | ||
override val user: GameUser = object : GameUser { | ||
override val id: String = "00000000-0000-0000-0000-000000000000" | ||
override var username: String = "SUPERSTAR" | ||
override var coin: Int = Int.MAX_VALUE | ||
override var diamond: Int = Int.MAX_VALUE | ||
override var ppTime: OffsetDateTime = OffsetDateTime.MIN | ||
override var isReviewer: Boolean = true | ||
override val SongSet.isOwned: Boolean get() = true | ||
override val ownedSets: List<SongSet> = listOf() | ||
override fun calculateR(): Int = Int.MIN_VALUE | ||
override fun calculateHighestGoldenMedal(): Int = 0 | ||
override fun changePassword(password: String) { | ||
logger.warn(superstarMarker, "Unexpected \"changePassword($password)\" has been invoked!") | ||
} | ||
override fun validatePassword(password: String): Boolean { | ||
logger.warn(superstarMarker, "Unexpected \"validatePassword($password)\" has been invoked!") | ||
return false | ||
} | ||
override fun giveSet(setId: String) { | ||
logger.warn(superstarMarker, "Unexpected \"giveSet($setId)\" has been invoked!") | ||
} | ||
override fun calculateLastRecords(limit: Int): List<GameRecord> = emptyList() | ||
override fun calculateBestRecords(limit: Int, sortedBy: ScoreOrRanking): List<GameRecord> = emptyList() | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
booster-bomb/src/main/kotlin/explode2/booster/bomb/submods/basic/WelcomeBO.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package explode2.booster.bomb.submods.basic | ||
|
||
internal data class WelcomeBO(val welcome: String) |
35 changes: 35 additions & 0 deletions
35
booster-bomb/src/main/kotlin/explode2/booster/bomb/submods/chart/ChartAndSetModule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package explode2.booster.bomb.submods.chart | ||
|
||
import explode2.booster.bomb.* | ||
import explode2.booster.bomb.submods.toData | ||
import explode2.booster.bomb.submods.toError | ||
import explode2.booster.event.RouteConfigure | ||
import explode2.labyrinth.LabyrinthPlugin.Companion.labyrinth | ||
import io.ktor.server.routing.* | ||
import io.ktor.server.util.* | ||
|
||
// [/chart] | ||
internal val chartModule: RouteConfigure = { | ||
|
||
// [/chart/{id}] | ||
get("{id}") { | ||
val cid = bombCall.parameters.getOrFail("id") | ||
val chart = labyrinth.songChartFactory.getSongChartById(cid) | ||
?: return@get bombCall.respondError(toError(Localization.ResourceNotFound)) | ||
bombCall.respondData(chart.toBO().toData()) | ||
} | ||
|
||
} | ||
|
||
// [/set] | ||
internal val setModule: RouteConfigure = { | ||
|
||
// [/set/{id}] | ||
get("{id}") { | ||
val sid = bombCall.parameters.getOrFail("id") | ||
val set = labyrinth.songSetFactory.getSongSetById(sid) | ||
?: return@get bombCall.respondError(toError(Localization.ResourceNotFound)) | ||
bombCall.respondData(set.toBO().toData()) | ||
} | ||
|
||
} |
13 changes: 13 additions & 0 deletions
13
booster-bomb/src/main/kotlin/explode2/booster/bomb/submods/chart/ChartBO.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package explode2.booster.bomb.submods.chart | ||
|
||
import explode2.gateau.SongChart | ||
|
||
internal data class ChartBO( | ||
val id: String, | ||
val difficulty_class: Int, | ||
val difficulty_value: Int, | ||
val d: Double? | ||
) | ||
|
||
internal fun SongChart.toBO() = | ||
ChartBO(id, difficultyClass, difficultyValue, d) |
Oops, something went wrong.