refactor: 代码调整
This commit is contained in:
parent
4da8263b45
commit
37eeaf143c
|
|
@ -1,11 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
<data-source source="LOCAL" name="@47.116.125.76" uuid="5b1b9d12-d8be-43ba-a647-9d6e467bf201">
|
<data-source source="LOCAL" name="@110.42.70.155" uuid="5b1b9d12-d8be-43ba-a647-9d6e467bf201">
|
||||||
<driver-ref>mysql.8</driver-ref>
|
<driver-ref>mysql.8</driver-ref>
|
||||||
<synchronize>true</synchronize>
|
<synchronize>true</synchronize>
|
||||||
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
||||||
<jdbc-url>jdbc:mysql://47.116.125.76:3308</jdbc-url>
|
<jdbc-url>jdbc:mysql://110.42.70.155:3306</jdbc-url>
|
||||||
<jdbc-additional-properties>
|
<jdbc-additional-properties>
|
||||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ repositories {
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://repo.glaremasters.me/repository/public/")
|
url = uri("https://repo.glaremasters.me/repository/public/")
|
||||||
}
|
}
|
||||||
|
maven {
|
||||||
|
name = "LTD Maven"
|
||||||
|
url = uri("https://nexus.bot.leisuretimedock.top/repository/maven-public/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//TODO: 0872d1c0-829c-e1d7-6782-89e45c8a6b76
|
//TODO: 0872d1c0-829c-e1d7-6782-89e45c8a6b76
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -77,7 +81,7 @@ repositories {
|
||||||
//DG_Lab 依赖库导入
|
//DG_Lab 依赖库导入
|
||||||
implementation("io.netty:netty-all:4.1.109.Final")
|
implementation("io.netty:netty-all:4.1.109.Final")
|
||||||
implementation("com.google.code.gson:gson:2.10.1")
|
implementation("com.google.code.gson:gson:2.10.1")
|
||||||
implementation(files("libs/DgLab-common-${k("dg_lab_version")}.jar"))
|
implementation("top.r3944realms.dg_lab:Common:${k("dg_lab_version")}")
|
||||||
|
|
||||||
//生成 二维码
|
//生成 二维码
|
||||||
implementation("com.google.zxing:core:[3.5.3,)")
|
implementation("com.google.zxing:core:[3.5.3,)")
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,4 @@ org.gradle.parallel=true
|
||||||
org.gradle.degree_of_parallelism=16
|
org.gradle.degree_of_parallelism=16
|
||||||
project_group=top.r3944realms.ltdmanager
|
project_group=top.r3944realms.ltdmanager
|
||||||
project_version=1.14-SNAPSHOT
|
project_version=1.14-SNAPSHOT
|
||||||
dg_lab_version=4.3.13.18
|
dg_lab_version=4.4.14.18
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import kotlinx.coroutines.*
|
||||||
import top.r3944realms.ltdmanager.blessingskin.BlessingSkinClient
|
import top.r3944realms.ltdmanager.blessingskin.BlessingSkinClient
|
||||||
import top.r3944realms.ltdmanager.chevereto.CheveretoClient
|
import top.r3944realms.ltdmanager.chevereto.CheveretoClient
|
||||||
import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool
|
import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool
|
||||||
|
import top.r3944realms.ltdmanager.mcms.MCSMClient
|
||||||
import top.r3944realms.ltdmanager.mcserver.McSrvStatusClient
|
import top.r3944realms.ltdmanager.mcserver.McSrvStatusClient
|
||||||
import top.r3944realms.ltdmanager.module.ModuleManager
|
import top.r3944realms.ltdmanager.module.ModuleManager
|
||||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||||
|
|
@ -20,6 +21,10 @@ object GlobalManager {
|
||||||
MysqlHikariConnectPool()
|
MysqlHikariConnectPool()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun initApplication() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// NapCat 客户端
|
// NapCat 客户端
|
||||||
val napCatClient: NapCatClient by lazy {
|
val napCatClient: NapCatClient by lazy {
|
||||||
NapCatClient.create()
|
NapCatClient.create()
|
||||||
|
|
@ -33,6 +38,9 @@ object GlobalManager {
|
||||||
val cheveretoClient: CheveretoClient by lazy {
|
val cheveretoClient: CheveretoClient by lazy {
|
||||||
CheveretoClient.create()
|
CheveretoClient.create()
|
||||||
}
|
}
|
||||||
|
val mcsmClient: MCSMClient by lazy {
|
||||||
|
MCSMClient.create()
|
||||||
|
}
|
||||||
|
|
||||||
val moduleManager: ModuleManager by lazy { ModuleManager() }
|
val moduleManager: ModuleManager by lazy { ModuleManager() }
|
||||||
|
|
||||||
|
|
@ -72,7 +80,8 @@ object GlobalManager {
|
||||||
"McSrvStatusClient" to { mcSrvStatusClient.close() },
|
"McSrvStatusClient" to { mcSrvStatusClient.close() },
|
||||||
"BlessingSkinClient" to { blessingSkinClient.close() },
|
"BlessingSkinClient" to { blessingSkinClient.close() },
|
||||||
"Hikari 数据源" to { dataSource.close() },
|
"Hikari 数据源" to { dataSource.close() },
|
||||||
"CheveretoClient" to { cheveretoClient.close() }
|
"CheveretoClient" to { cheveretoClient.close() },
|
||||||
|
"McsmClient" to { mcsmClient.close() },
|
||||||
)
|
)
|
||||||
|
|
||||||
resources.forEach { (name, closer) ->
|
resources.forEach { (name, closer) ->
|
||||||
|
|
@ -99,4 +108,5 @@ object GlobalManager {
|
||||||
isRunning.set(false)
|
isRunning.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,27 +1,24 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin
|
package top.r3944realms.ltdmanager.blessingskin
|
||||||
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.request.*
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import io.ktor.http.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.sync.withPermit
|
|
||||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
import top.r3944realms.ltdmanager.core.client.IClient
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
import top.r3944realms.ltdmanager.utils.Environment
|
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class BlessingSkinClient private constructor() : AutoCloseable {
|
class BlessingSkinClient private constructor() : IClient<BlessingSkinRequest, BlessingSkinQueueItem, BlessingSkinResponse, FailedBlessingSkinResponse> {
|
||||||
private val client = HttpClient(CIO) {
|
private val client = HttpClient(CIO) {
|
||||||
expectSuccess = false
|
expectSuccess = false
|
||||||
|
|
||||||
|
|
@ -40,170 +37,40 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
||||||
// 限流控制
|
// 限流控制
|
||||||
private val semaphore = Semaphore(5)
|
private val semaphore = Semaphore(5)
|
||||||
private val requestMutex = Mutex()
|
private val requestMutex = Mutex()
|
||||||
private val requestQueue = PriorityQueue<BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>>(compareBy { it.priority })
|
private val requestQueue = PriorityQueue<BlessingSkinQueueItem>(compareBy { it.priority })
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
startQueueProcessor()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun getBaseUrl(): String = blessingSkinServerConfig.url!!
|
||||||
* 提交请求
|
|
||||||
*/
|
override fun getType(): String = "BlessingSkinClient"
|
||||||
suspend fun <T : BlessingSkinResponse, F : FailedBlessingSkinResponse> submitRequest(
|
|
||||||
request: BlessingSkinRequest<T, F>,
|
override fun getClient(): HttpClient = client
|
||||||
priority: Int = 5,
|
|
||||||
maxRetries: Int = 3
|
override fun getSemaphore(): Semaphore = semaphore
|
||||||
): ResponseResult<T, F> {
|
|
||||||
val deferred = CompletableDeferred<ResponseResult<T, F>>()
|
override fun getRequestMutex(): Mutex = requestMutex
|
||||||
requestMutex.withLock {
|
|
||||||
requestQueue.add(BlessingSkinQueueItem(request, deferred, priority, maxRetries, true))
|
override fun getResponseQueue(): PriorityQueue<BlessingSkinQueueItem> = requestQueue
|
||||||
}
|
|
||||||
return deferred.await()
|
override fun getScope(): CoroutineScope = scope
|
||||||
|
|
||||||
|
override fun createFailureResponse(exception: Exception?): IFailedResponse {
|
||||||
|
return FailedBlessingSkinResponse.Default(exception?.stackTraceToString()?:"ERROR")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun addToQueue(
|
||||||
* 启动队列处理器
|
request: BlessingSkinRequest,
|
||||||
*/
|
deferredC: CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>,
|
||||||
private fun startQueueProcessor() {
|
priority: Int,
|
||||||
scope.launch {
|
maxRetries: Int
|
||||||
while (isActive) {
|
): BlessingSkinQueueItem {
|
||||||
val item = requestMutex.withLock {
|
val element = BlessingSkinQueueItem(request, deferredC, priority, maxRetries, false)
|
||||||
requestQueue.poll()
|
requestQueue.add(element)
|
||||||
}
|
return element
|
||||||
if (item == null) {
|
|
||||||
delay(50)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
processQueueItem(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理队列项
|
|
||||||
*/
|
|
||||||
private suspend fun processQueueItem(item: BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>) {
|
|
||||||
semaphore.withPermit {
|
|
||||||
val (request, deferred, _, maxRetries, _) = item
|
|
||||||
var attempt = 0
|
|
||||||
var lastError: Exception? = null
|
|
||||||
|
|
||||||
while (attempt < maxRetries) {
|
|
||||||
try {
|
|
||||||
// 构建完整的URL,包括查询参数
|
|
||||||
val fullUrl = buildFullUrlWithQueryParams(request)
|
|
||||||
|
|
||||||
if (!Environment.isProduction()) {
|
|
||||||
LoggerUtil.logger.debug("发送请求到: $fullUrl")
|
|
||||||
LoggerUtil.logger.debug("请求方法: {}", request.method())
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = client.request(fullUrl) {
|
|
||||||
method = request.method()
|
|
||||||
|
|
||||||
|
|
||||||
// 设置请求头
|
|
||||||
headers {
|
|
||||||
request.headers().invoke(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对于非GET请求,设置请求体
|
|
||||||
if (request.method() != HttpMethod.Get) {
|
|
||||||
setBody(request.toJSON())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val responseText: String = response.body()
|
|
||||||
|
|
||||||
if (!Environment.isProduction()) {
|
|
||||||
LoggerUtil.logger.debug("响应状态: {}", response.status)
|
|
||||||
LoggerUtil.logger.debug("响应内容: $responseText")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否是HTML响应(重定向)
|
|
||||||
if (isHtmlResponse(responseText)) {
|
|
||||||
throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析响应
|
|
||||||
val result = request.getResponse(responseText, response.status)
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(deferred as CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).complete(result)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
lastError = e
|
|
||||||
attempt++
|
|
||||||
|
|
||||||
if (!request.shouldRetryOnFailure() || attempt >= maxRetries) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
LoggerUtil.logger.warn("BlessingSkin请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
|
||||||
delay((attempt * 1000L)) // 指数退避
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 所有重试都失败或不应重试
|
|
||||||
val errorResponse = createFailureResponse(lastError, request)
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(deferred as CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).complete(
|
|
||||||
ResponseResult.Failure(errorResponse)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建完整的URL,包含查询参数
|
|
||||||
*/
|
|
||||||
private fun buildFullUrlWithQueryParams(request: BlessingSkinRequest<*, *>): String {
|
|
||||||
val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/")
|
|
||||||
val path = request.path().removePrefix("/")
|
|
||||||
|
|
||||||
// 构建基础URL
|
|
||||||
val urlBuilder = StringBuilder("$baseUrl/$path")
|
|
||||||
|
|
||||||
// 添加查询参数
|
|
||||||
val queryParams = request.queryParameters().entries.joinToString("&") { (key, value) ->
|
|
||||||
"${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryParams.isNotEmpty()) {
|
|
||||||
urlBuilder.append("?").append(queryParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
return urlBuilder.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否是HTML响应
|
|
||||||
*/
|
|
||||||
private fun isHtmlResponse(text: String): Boolean {
|
|
||||||
return text.contains("<!DOCTYPE html>", ignoreCase = true) ||
|
|
||||||
text.contains("<html>", ignoreCase = true) ||
|
|
||||||
text.contains("Redirecting", ignoreCase = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建失败响应
|
|
||||||
*/
|
|
||||||
private fun createFailureResponse(
|
|
||||||
exception: Exception?,
|
|
||||||
request: BlessingSkinRequest<*, *>
|
|
||||||
): FailedBlessingSkinResponse {
|
|
||||||
return FailedBlessingSkinResponse.Default(
|
|
||||||
failedResult = exception?.message ?: "未知错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
scope.cancel()
|
|
||||||
runBlocking {
|
|
||||||
client.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ import kotlinx.coroutines.CompletableDeferred
|
||||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.QueueItem
|
||||||
|
|
||||||
data class BlessingSkinQueueItem<out T:BlessingSkinResponse,out F:FailedBlessingSkinResponse>(
|
data class BlessingSkinQueueItem (
|
||||||
val request: BlessingSkinRequest<T,F>,
|
val request0: BlessingSkinRequest,
|
||||||
val deferred: CompletableDeferred<*>,
|
val deferred0: CompletableDeferred<*>,
|
||||||
var retries: Int,
|
val priority0: Int,
|
||||||
val priority: Int,
|
var retries0: Int,
|
||||||
val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit
|
val expectsResponse0: Boolean
|
||||||
) : Comparable<BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
|
) : QueueItem<BlessingSkinRequest, BlessingSkinResponse, FailedBlessingSkinResponse> (
|
||||||
override fun compareTo(other: BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
request0, deferred0, retries0, priority0, expectsResponse0
|
||||||
}
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,13 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin.data
|
package top.r3944realms.ltdmanager.blessingskin.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InvitationCode(
|
||||||
|
val code: String,
|
||||||
|
@SerialName("generated_at")
|
||||||
|
val generatedAt: String,
|
||||||
|
@SerialName("expires_at")
|
||||||
|
val expiresAt: String
|
||||||
|
)
|
||||||
|
|
@ -1,79 +1,13 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin.request
|
package top.r3944realms.ltdmanager.blessingskin.request
|
||||||
|
|
||||||
import io.ktor.http.*
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class BlessingSkinRequest<out T : BlessingSkinResponse, out F : FailedBlessingSkinResponse>(
|
abstract class BlessingSkinRequest(
|
||||||
@Transient
|
@Transient
|
||||||
open val createTime: Long = System.currentTimeMillis()
|
override val createTime: Long = System.currentTimeMillis()
|
||||||
) {
|
): IRequest<BlessingSkinResponse, FailedBlessingSkinResponse>
|
||||||
/**
|
|
||||||
* 转换为JSON字符串
|
|
||||||
*/
|
|
||||||
abstract fun toJSON(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取API路径(不包含基础URL)
|
|
||||||
* 例如: "invitation-codes/generate"
|
|
||||||
*/
|
|
||||||
abstract fun path(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取HTTP方法,默认为GET(因为大多数API使用GET+查询参数)
|
|
||||||
*/
|
|
||||||
open fun method(): HttpMethod = HttpMethod.Get
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义请求头
|
|
||||||
*/
|
|
||||||
open fun headers(): HeadersBuilder.() -> Unit = {
|
|
||||||
// 默认添加Content-Type
|
|
||||||
append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
|
||||||
// 添加Accept头
|
|
||||||
append(HttpHeaders.Accept, "application/json")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取查询参数(用于URL参数)
|
|
||||||
* 例如: mapOf("token" to "abc123", "amount" to "1")
|
|
||||||
*/
|
|
||||||
open fun queryParameters(): Map<String, String> = emptyMap()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取请求体参数(用于POST请求的JSON body)
|
|
||||||
* 例如: mapOf("token" to "abc123", "amount" to 1)
|
|
||||||
*/
|
|
||||||
open fun bodyParameters(): Map<String, Any> = emptyMap()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取请求体内容类型,默认为Application.Json
|
|
||||||
*/
|
|
||||||
open fun contentType(): ContentType = ContentType.Application.Json
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析响应JSON字符串
|
|
||||||
* @param responseJson 响应JSON字符串
|
|
||||||
* @param httpStatusCode HTTP状态码
|
|
||||||
*/
|
|
||||||
abstract fun getResponse(responseJson: String, httpStatusCode: HttpStatusCode): ResponseResult<T, F>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取预期的成功响应类型名称(用于日志和调试)
|
|
||||||
*/
|
|
||||||
abstract fun expectedResponseType(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取预期的失败响应类型名称(用于日志和调试)
|
|
||||||
*/
|
|
||||||
abstract fun expectedFailureType(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否需要在失败时重试(默认重试)
|
|
||||||
*/
|
|
||||||
open fun shouldRetryOnFailure(): Boolean = true
|
|
||||||
}
|
|
||||||
|
|
@ -6,8 +6,8 @@ import kotlinx.serialization.Transient
|
||||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
@ -17,9 +17,7 @@ class GenerateInvitationCodeRequest(
|
||||||
val token: String? = null,
|
val token: String? = null,
|
||||||
@Transient
|
@Transient
|
||||||
val amount: Int? = 1,
|
val amount: Int? = 1,
|
||||||
@Transient
|
) : BlessingSkinRequest() {
|
||||||
override val createTime: Long = System.currentTimeMillis()
|
|
||||||
) : BlessingSkinRequest<InvitationCodeGenerationResponse, FailedBlessingSkinResponse.Default>() {
|
|
||||||
|
|
||||||
override fun toJSON(): String {
|
override fun toJSON(): String {
|
||||||
// 对于GET请求,参数在URL中,body可以为空
|
// 对于GET请求,参数在URL中,body可以为空
|
||||||
|
|
@ -66,7 +64,7 @@ class GenerateInvitationCodeRequest(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ResponseResult.Failure(
|
ResponseResult.Failure(
|
||||||
FailedBlessingSkinResponse.Default(
|
FailedBlessingSkinResponse.Default(
|
||||||
failedResult = "解析响应失败: ${e.message}"
|
failedMessage = "解析响应失败: ${e.message}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,15 @@ import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import kotlinx.serialization.modules.polymorphic
|
import kotlinx.serialization.modules.polymorphic
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class BlessingSkinResponse (
|
abstract class BlessingSkinResponse (
|
||||||
@Transient
|
@Transient
|
||||||
open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||||
@Transient
|
@Transient
|
||||||
open val createTime: Long = System.currentTimeMillis()
|
override val createTime: Long = System.currentTimeMillis()
|
||||||
) {
|
) : IResponse {
|
||||||
companion object {
|
companion object {
|
||||||
// 通用的反序列化方法
|
// 通用的反序列化方法
|
||||||
inline fun <reified T : BlessingSkinResponse> decode(jsonString: String): T {
|
inline fun <reified T : BlessingSkinResponse> decode(jsonString: String): T {
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@ package top.r3944realms.ltdmanager.blessingskin.response
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class FailedBlessingSkinResponse: BlessingSkinResponse() {
|
abstract class FailedBlessingSkinResponse: BlessingSkinResponse(), IFailedResponse {
|
||||||
abstract fun failedMessage(): String
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Default(@Transient val failedResult: String? = "未知错误") : FailedBlessingSkinResponse() {
|
class Default(@Transient override val failedMessage: String = "未知错误") : FailedBlessingSkinResponse() {
|
||||||
override fun failedMessage(): String = failedResult!!
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin.response.invitecode
|
package top.r3944realms.ltdmanager.blessingskin.response.invitecode
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.r3944realms.ltdmanager.blessingskin.data.InvitationCode
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||||
@Serializable
|
@Serializable
|
||||||
data class InvitationCodeGenerationResponse(
|
data class InvitationCodeGenerationResponse(
|
||||||
|
|
@ -10,12 +10,4 @@ data class InvitationCodeGenerationResponse(
|
||||||
val data: List<InvitationCode>? = null
|
val data: List<InvitationCode>? = null
|
||||||
) : BlessingSkinResponse() {
|
) : BlessingSkinResponse() {
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class InvitationCode(
|
|
||||||
val code: String,
|
|
||||||
@SerialName("generated_at")
|
|
||||||
val generatedAt: String,
|
|
||||||
@SerialName("expires_at")
|
|
||||||
val expiresAt: String
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,65 +3,157 @@ package top.r3944realms.ltdmanager.chevereto
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.request.forms.*
|
|
||||||
import io.ktor.client.statement.*
|
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import kotlinx.serialization.json.Json
|
import top.r3944realms.ltdmanager.chevereto.data.CheveretoSource
|
||||||
import top.r3944realms.ltdmanager.chevereto.data.CheveretoResponse
|
import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.request.v1.CheveretoUploadRequest
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.IClient
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
|
import top.r3944realms.ltdmanager.utils.Environment
|
||||||
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayDeque
|
|
||||||
|
|
||||||
|
class CheveretoClient private constructor() :
|
||||||
class CheveretoClient private constructor() : Closeable {
|
IClient<CheveretoRequest, CheveretoQueueItem, CheveretoResponse, FailedCheveretoResponse> {
|
||||||
|
|
||||||
private val client = HttpClient(CIO) {
|
private val client = HttpClient(CIO) {
|
||||||
install(ContentNegotiation) {
|
expectSuccess = false
|
||||||
json(Json { ignoreUnknownKeys = true })
|
// 安装 HttpTimeout 插件
|
||||||
|
install(HttpTimeout) {
|
||||||
|
// 默认超时配置,会被具体请求的配置覆盖
|
||||||
|
requestTimeoutMillis = 30000
|
||||||
|
connectTimeoutMillis = 10000
|
||||||
|
socketTimeoutMillis = 15000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val imgTuConfig = YamlConfigLoader.loadTuImgConfig()
|
private val imgTuConfig = YamlConfigLoader.loadTuImgConfig()
|
||||||
private val apiUrl = imgTuConfig.url!!
|
private val baseUrl = imgTuConfig.url!!.removeSuffix("/")
|
||||||
private val apiKey = imgTuConfig.decryptedPassword!!
|
private val apiKey = imgTuConfig.decryptedPassword!!
|
||||||
// 限流,同时最多 3 个上传
|
|
||||||
private val semaphore = Semaphore(3)
|
private val semaphore = Semaphore(3)
|
||||||
|
private val queue = PriorityQueue<CheveretoQueueItem>()
|
||||||
// 普通队列 (按 priority 排序)
|
|
||||||
private val queue = PriorityQueue<CheveretoQueueItem<CheveretoResponse>>(compareBy { it.priority })
|
|
||||||
private val queueMutex = Mutex()
|
private val queueMutex = Mutex()
|
||||||
|
|
||||||
// 紧急队列 (FIFO,最多 10 个)
|
|
||||||
private val urgentQueue = ArrayDeque<CheveretoQueueItem<CheveretoResponse>>(10)
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
init()
|
||||||
while (isActive) {
|
}
|
||||||
val item = queueMutex.withLock {
|
|
||||||
when {
|
override fun getType(): String = "CheveretoClient"
|
||||||
urgentQueue.isNotEmpty() -> urgentQueue.removeFirst()
|
|
||||||
queue.isNotEmpty() -> queue.poll()
|
override fun getClient(): HttpClient = client
|
||||||
else -> null
|
|
||||||
|
override fun getSemaphore(): Semaphore = semaphore
|
||||||
|
|
||||||
|
override fun getRequestMutex(): Mutex = queueMutex
|
||||||
|
|
||||||
|
override fun getResponseQueue(): PriorityQueue<CheveretoQueueItem> = queue
|
||||||
|
|
||||||
|
override fun getScope(): CoroutineScope = scope
|
||||||
|
|
||||||
|
override fun getBaseUrl(): String = baseUrl
|
||||||
|
|
||||||
|
override fun createFailureResponse(exception: Exception?): FailedCheveretoResponse =
|
||||||
|
FailedCheveretoResponse.Default(
|
||||||
|
httpStatusCode = HttpStatusCode.InternalServerError,
|
||||||
|
failedMessage = exception?.message ?: "Unknown error"
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun addToQueue(
|
||||||
|
request: CheveretoRequest,
|
||||||
|
deferredC: CompletableDeferred<ResponseResult<CheveretoResponse, FailedCheveretoResponse>>,
|
||||||
|
priority: Int,
|
||||||
|
maxRetries: Int
|
||||||
|
): CheveretoQueueItem {
|
||||||
|
val item = CheveretoQueueItem(request, deferredC, maxRetries, priority, true)
|
||||||
|
queue.add(item)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun processQueueItem(item: CheveretoQueueItem) {
|
||||||
|
getSemaphore().withPermit {
|
||||||
|
val request = item.request
|
||||||
|
val deferred = item.deferred
|
||||||
|
val maxRetries = item.retries
|
||||||
|
var attempt = 0
|
||||||
|
var lastError: Exception?
|
||||||
|
|
||||||
|
while (attempt < maxRetries) {
|
||||||
|
try {
|
||||||
|
val fullUrl = buildFullUrlWithQueryParams(request)
|
||||||
|
if (!Environment.isProduction()) {
|
||||||
|
LoggerUtil.logger.debug("发送请求到: $fullUrl")
|
||||||
|
LoggerUtil.logger.debug("请求方法: {}", request.method())
|
||||||
}
|
}
|
||||||
|
val response = getClient().request(fullUrl) {
|
||||||
|
method = request.method()
|
||||||
|
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
headers {
|
||||||
|
request.headers().invoke(this)
|
||||||
|
header("X-API-Key", apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于非GET请求,设置请求体
|
||||||
|
if (request.method() != HttpMethod.Get) {
|
||||||
|
setBody(request.toJSON())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val responseText: String = response.body()
|
||||||
|
|
||||||
|
if (!Environment.isProduction()) {
|
||||||
|
LoggerUtil.logger.debug("响应状态: {}", response.status)
|
||||||
|
LoggerUtil.logger.debug("响应内容: $responseText")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是HTML响应(重定向)
|
||||||
|
if (isHtmlResponse(responseText)) {
|
||||||
|
throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
val result = request.getResponse(responseText, response.status)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(deferred as CompletableDeferred<ResponseResult<IResponse, IFailedResponse>>).complete(result)
|
||||||
|
|
||||||
|
return
|
||||||
|
} catch (e: Exception) {
|
||||||
|
lastError = e
|
||||||
|
attempt++
|
||||||
|
|
||||||
|
if (!request.shouldRetryOnFailure() || attempt >= maxRetries) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerUtil.logger.warn("${getType()} 请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
||||||
|
delay((attempt * 1000L)) // 指数退避
|
||||||
}
|
}
|
||||||
if (item != null) processItem(item)
|
// 所有重试都失败或不应重试
|
||||||
else delay(20)
|
val errorResponse = createFailureResponse(lastError)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(deferred as CompletableDeferred<ResponseResult<IResponse, IFailedResponse>>).complete(
|
||||||
|
ResponseResult.Failure(errorResponse)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传 File
|
* 上传 File
|
||||||
*/
|
*/
|
||||||
|
|
@ -77,35 +169,24 @@ class CheveretoClient private constructor() : Closeable {
|
||||||
nsfw: Int? = null,
|
nsfw: Int? = null,
|
||||||
format: String = "json",
|
format: String = "json",
|
||||||
useFileDate: Int? = null,
|
useFileDate: Int? = null,
|
||||||
priority: Int = 5
|
priority: Int = 5,
|
||||||
|
maxRetries: Int = 3
|
||||||
|
|
||||||
): CheveretoResponse {
|
): CheveretoResponse {
|
||||||
val deferred = CompletableDeferred<CheveretoResponse>()
|
upload(CheveretoUploadRequest(
|
||||||
val source = suspend {
|
source = CheveretoSource.ByteArraySource(file.readBytes(), file.name),
|
||||||
safeUpload {
|
format = format,
|
||||||
submitFormWithBinaryData(
|
title = title,
|
||||||
url = apiUrl,
|
description = description,
|
||||||
formData = formData {
|
tags = tags,
|
||||||
append("source", file.readBytes(), Headers.build {
|
albumId = albumId,
|
||||||
append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"${file.name}\"")
|
categoryId = categoryId,
|
||||||
})
|
width = width,
|
||||||
append("format", format)
|
expiration = expiration,
|
||||||
title?.let { append("title", it) }
|
nsfw = nsfw,
|
||||||
description?.let { append("description", it) }
|
useFileDate = useFileDate
|
||||||
tags?.let { append("tags", it) }
|
), priority, maxRetries).getRetResponse()
|
||||||
albumId?.let { append("album_id", it) }
|
throw Exception("Never Reach")
|
||||||
categoryId?.let { append("category_id", it) }
|
|
||||||
width?.let { append("width", it.toString()) }
|
|
||||||
expiration?.let { append("expiration", it) }
|
|
||||||
nsfw?.let { append("nsfw", it.toString()) }
|
|
||||||
useFileDate?.let { append("use_file_date", it.toString()) }
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
header("X-API-Key", apiKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) }
|
|
||||||
return deferred.await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -125,36 +206,23 @@ class CheveretoClient private constructor() : Closeable {
|
||||||
nsfw: Int? = null,
|
nsfw: Int? = null,
|
||||||
format: String = "json",
|
format: String = "json",
|
||||||
useFileDate: Int? = null,
|
useFileDate: Int? = null,
|
||||||
priority: Int = 5
|
priority: Int = 5,
|
||||||
|
maxRetries: Int = 3
|
||||||
): CheveretoResponse {
|
): CheveretoResponse {
|
||||||
val deferred = CompletableDeferred<CheveretoResponse>()
|
upload(CheveretoUploadRequest(
|
||||||
val source = suspend {
|
source = CheveretoSource.ByteArraySource(inputStream.readBytes(), fileName),
|
||||||
val bytes = inputStream.readBytes()
|
format = format,
|
||||||
safeUpload {
|
title = title,
|
||||||
submitFormWithBinaryData(
|
description = description,
|
||||||
url = apiUrl,
|
tags = tags,
|
||||||
formData = formData {
|
albumId = albumId,
|
||||||
append("source", bytes, Headers.build {
|
categoryId = categoryId,
|
||||||
append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"$fileName\"")
|
width = width,
|
||||||
})
|
expiration = expiration,
|
||||||
append("format", format)
|
nsfw = nsfw,
|
||||||
title?.let { append("title", it) }
|
useFileDate = useFileDate
|
||||||
description?.let { append("description", it) }
|
), priority, maxRetries).getRetResponse()
|
||||||
tags?.let { append("tags", it) }
|
throw Exception("Never Reach")
|
||||||
albumId?.let { append("album_id", it) }
|
|
||||||
categoryId?.let { append("category_id", it) }
|
|
||||||
width?.let { append("width", it.toString()) }
|
|
||||||
expiration?.let { append("expiration", it) }
|
|
||||||
nsfw?.let { append("nsfw", it.toString()) }
|
|
||||||
useFileDate?.let { append("use_file_date", it.toString()) }
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
header("X-API-Key", apiKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) }
|
|
||||||
return deferred.await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -172,64 +240,41 @@ class CheveretoClient private constructor() : Closeable {
|
||||||
nsfw: Int? = null,
|
nsfw: Int? = null,
|
||||||
format: String = "json",
|
format: String = "json",
|
||||||
useFileDate: Int? = null,
|
useFileDate: Int? = null,
|
||||||
priority: Int = 5
|
priority: Int = 5,
|
||||||
|
maxRetries: Int = 3
|
||||||
): CheveretoResponse {
|
): CheveretoResponse {
|
||||||
val deferred = CompletableDeferred<CheveretoResponse>()
|
upload(CheveretoUploadRequest(
|
||||||
val source = suspend {
|
source = CheveretoSource.UrlSource(url),
|
||||||
safeUpload {
|
format = format,
|
||||||
submitForm(
|
title = title,
|
||||||
url = apiUrl,
|
description = description,
|
||||||
formParameters = Parameters.build {
|
tags = tags,
|
||||||
append("source", url)
|
albumId = albumId,
|
||||||
append("format", format)
|
categoryId = categoryId,
|
||||||
title?.let { append("title", it) }
|
width = width,
|
||||||
description?.let { append("description", it) }
|
expiration = expiration,
|
||||||
tags?.let { append("tags", it) }
|
nsfw = nsfw,
|
||||||
albumId?.let { append("album_id", it) }
|
useFileDate = useFileDate
|
||||||
categoryId?.let { append("category_id", it) }
|
), priority, maxRetries).getRetResponse()
|
||||||
width?.let { append("width", it.toString()) }
|
throw Exception("Never Reach")
|
||||||
expiration?.let { append("expiration", it) }
|
|
||||||
nsfw?.let { append("nsfw", it.toString()) }
|
|
||||||
useFileDate?.let { append("use_file_date", it.toString()) }
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
header("X-API-Key", apiKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) }
|
|
||||||
return deferred.await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun processItem(item: CheveretoQueueItem<CheveretoResponse>) {
|
suspend fun upload(
|
||||||
semaphore.withPermit {
|
request: CheveretoUploadRequest, priority: Int, maxRetries: Int
|
||||||
try {
|
): ResponseResult<CheveretoUploadResponse, FailedCheveretoResponse> {
|
||||||
val result = item.source()
|
|
||||||
item.deferred.complete(result)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
item.deferred.completeExceptionally(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 包装上传,失败时打印原始响应
|
|
||||||
*/
|
|
||||||
private suspend fun safeUpload(block: suspend HttpClient.() -> HttpResponse): CheveretoResponse {
|
|
||||||
val response = client.block()
|
|
||||||
return try {
|
return try {
|
||||||
response.body()
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
submitRequest(request, priority, maxRetries) as ResponseResult<CheveretoUploadResponse, FailedCheveretoResponse>
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val raw = response.bodyAsText()
|
ResponseResult.Failure(
|
||||||
throw RuntimeException("Upload failed (status=${response.status}): $raw", e)
|
FailedCheveretoResponse.Default(
|
||||||
|
httpStatusCode = HttpStatusCode.InternalServerError,
|
||||||
|
failedMessage = "Byte array upload failed: ${e.message}"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
scope.cancel()
|
|
||||||
runBlocking { client.close() }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun create(): CheveretoClient = CheveretoClient()
|
fun create(): CheveretoClient = CheveretoClient()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
package top.r3944realms.ltdmanager.chevereto
|
package top.r3944realms.ltdmanager.chevereto
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.QueueItem
|
||||||
|
|
||||||
data class CheveretoQueueItem<T>(
|
data class CheveretoQueueItem(
|
||||||
val source: suspend () -> T,
|
val request0: CheveretoRequest,
|
||||||
val deferred: CompletableDeferred<T>,
|
val deferred0: CompletableDeferred<*>,
|
||||||
val priority: Int = 5
|
val priority0: Int,
|
||||||
|
var retries0: Int,
|
||||||
|
val expectsResponse0: Boolean
|
||||||
|
) : QueueItem<CheveretoRequest, CheveretoResponse, FailedCheveretoResponse>(
|
||||||
|
request0, deferred0, retries0, priority0, expectsResponse0
|
||||||
)
|
)
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
package top.r3944realms.ltdmanager.chevereto.data
|
package top.r3944realms.ltdmanager.chevereto.data
|
||||||
|
|
||||||
class CheveretoSource {
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class CheveretoSource {
|
||||||
|
@Serializable
|
||||||
|
data class ByteArraySource(val bytes: ByteArray, val fileName: String) : CheveretoSource()
|
||||||
|
@Serializable
|
||||||
|
data class UrlSource(val url: String) : CheveretoSource()
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ package top.r3944realms.ltdmanager.chevereto.data
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Success(
|
data class SuccessInfo(
|
||||||
val message : String? = null,
|
val message : String? = null,
|
||||||
val code: Int? = 200,
|
val code: Int? = 200,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,13 @@
|
||||||
package top.r3944realms.ltdmanager.chevereto.request
|
package top.r3944realms.ltdmanager.chevereto.request
|
||||||
|
|
||||||
class CheveretoRequest {
|
import kotlinx.serialization.Serializable
|
||||||
}
|
import kotlinx.serialization.Transient
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class CheveretoRequest(
|
||||||
|
@Transient
|
||||||
|
override val createTime: Long = System.currentTimeMillis()
|
||||||
|
) : IRequest<CheveretoResponse, FailedCheveretoResponse>
|
||||||
|
|
@ -1,4 +1,89 @@
|
||||||
package top.r3944realms.ltdmanager.chevereto.request.v1
|
package top.r3944realms.ltdmanager.chevereto.request.v1
|
||||||
|
|
||||||
class CheveretoUploadRequest {
|
import io.ktor.http.*
|
||||||
}
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.data.CheveretoSource
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CheveretoUploadRequest(
|
||||||
|
private val source: CheveretoSource,
|
||||||
|
private val title: String? = null,
|
||||||
|
private val description: String? = null,
|
||||||
|
private val tags: String? = null,
|
||||||
|
@SerialName("album_id")
|
||||||
|
private val albumId: String? = null,
|
||||||
|
@SerialName("category_id")
|
||||||
|
private val categoryId: String? = null,
|
||||||
|
private val width: Int? = null,
|
||||||
|
private val expiration: String? = null,
|
||||||
|
private val nsfw: Int? = null,
|
||||||
|
private val format: String = "json",
|
||||||
|
@SerialName("use_file_date")
|
||||||
|
private val useFileDate: Int? = null
|
||||||
|
) : CheveretoRequest() {
|
||||||
|
override fun path(): String = "api/1/upload"
|
||||||
|
|
||||||
|
override fun method(): HttpMethod = HttpMethod.Post
|
||||||
|
|
||||||
|
override fun headers(): HeadersBuilder.() -> Unit = {
|
||||||
|
append(HttpHeaders.Accept, "application/json")
|
||||||
|
// 对于文件上传,Content-Type 由 Ktor 自动设置
|
||||||
|
}
|
||||||
|
override fun bodyParameters(): Map<String, Any> {
|
||||||
|
val params = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
title?.let { params["title"] = it }
|
||||||
|
description?.let { params["description"] = it }
|
||||||
|
tags?.let { params["tags"] = it }
|
||||||
|
albumId?.let { params["album_id"] = it }
|
||||||
|
categoryId?.let { params["category_id"] = it }
|
||||||
|
width?.let { params["width"] = it }
|
||||||
|
expiration?.let { params["expiration"] = it }
|
||||||
|
nsfw?.let { params["nsfw"] = it }
|
||||||
|
params["format"] = format
|
||||||
|
useFileDate?.let { params["use_file_date"] = it }
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJSON(): String = Json.encodeToString(this)
|
||||||
|
|
||||||
|
override fun getResponse(
|
||||||
|
responseJson: String,
|
||||||
|
httpStatusCode: HttpStatusCode
|
||||||
|
): ResponseResult<CheveretoUploadResponse, FailedCheveretoResponse> {
|
||||||
|
return try {
|
||||||
|
if (httpStatusCode.isSuccess()) {
|
||||||
|
val successResponse = Json.decodeFromString<CheveretoUploadResponse>(responseJson)
|
||||||
|
ResponseResult.Success(successResponse)
|
||||||
|
} else {
|
||||||
|
ResponseResult.Failure(
|
||||||
|
FailedCheveretoResponse.Default(
|
||||||
|
httpStatusCode = HttpStatusCode.InternalServerError,
|
||||||
|
failedMessage = responseJson.takeIf { it.isNotBlank() }?:"ERROR"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ResponseResult.Failure(
|
||||||
|
FailedCheveretoResponse.Default(
|
||||||
|
httpStatusCode = HttpStatusCode.InternalServerError,
|
||||||
|
failedMessage = "Failed to parse response: ${e.message}. Raw response: $responseJson"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun expectedResponseType(): String = "CheveretoUploadResponse"
|
||||||
|
|
||||||
|
override fun expectedFailureType(): String = "FailedCheveretoResponse"
|
||||||
|
|
||||||
|
override fun shouldRetryOnFailure(): Boolean = true
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,37 @@
|
||||||
package top.r3944realms.ltdmanager.chevereto.response
|
package top.r3944realms.ltdmanager.chevereto.response
|
||||||
|
|
||||||
class CheveretoSuccessResponse {
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.polymorphic
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class CheveretoResponse (
|
||||||
|
@Transient
|
||||||
|
override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||||
|
@Transient
|
||||||
|
override val createTime: Long = System.currentTimeMillis()
|
||||||
|
) : IResponse {
|
||||||
|
companion object {
|
||||||
|
// 通用的反序列化方法
|
||||||
|
inline fun <reified T : CheveretoResponse> decode(jsonString: String): T {
|
||||||
|
return json.decodeFromString(jsonString)
|
||||||
|
}
|
||||||
|
val json: Json by lazy {
|
||||||
|
Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
serializersModule = SerializersModule {
|
||||||
|
polymorphic(CheveretoResponse::class) {
|
||||||
|
subclass(FailedCheveretoResponse.Default::class, FailedCheveretoResponse.Default.serializer())
|
||||||
|
subclass(CheveretoUploadResponse::class, CheveretoUploadResponse.serializer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
package top.r3944realms.ltdmanager.chevereto.response
|
package top.r3944realms.ltdmanager.chevereto.response
|
||||||
|
|
||||||
class FailedCheveretoResponse {
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class FailedCheveretoResponse: CheveretoResponse(), IFailedResponse {
|
||||||
|
@Serializable
|
||||||
|
class Default(@Transient override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, @Transient override val failedMessage: String = "未知错误") : FailedCheveretoResponse()
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,17 @@
|
||||||
package top.r3944realms.ltdmanager.chevereto.response.v1
|
package top.r3944realms.ltdmanager.chevereto.response.v1
|
||||||
|
|
||||||
class CheveretoUploadResponse {
|
import kotlinx.serialization.SerialName
|
||||||
}
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.data.CheveretoImage
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.data.SuccessInfo
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CheveretoUploadResponse(
|
||||||
|
@SerialName("status_code")
|
||||||
|
val statusCode: Int,
|
||||||
|
val success: SuccessInfo,
|
||||||
|
val image: CheveretoImage,
|
||||||
|
@SerialName("status_txt")
|
||||||
|
val statusTxt: String
|
||||||
|
) : CheveretoResponse()
|
||||||
|
|
@ -1,4 +1,170 @@
|
||||||
package top.r3944realms.ltdmanager.basic
|
package top.r3944realms.ltdmanager.core.client
|
||||||
|
|
||||||
interface IClient {
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
|
import top.r3944realms.ltdmanager.utils.Environment
|
||||||
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface IClient<R: IRequest<T, F>, Q: QueueItem<R, T, F>, T: IResponse, F: IFailedResponse> : AutoCloseable {
|
||||||
|
fun getType(): String
|
||||||
|
fun getClient(): HttpClient
|
||||||
|
fun getSemaphore(): Semaphore
|
||||||
|
fun getRequestMutex(): Mutex
|
||||||
|
fun getResponseQueue(): PriorityQueue<Q>
|
||||||
|
fun getScope(): CoroutineScope
|
||||||
|
fun getBaseUrl(): String = "http://localhost:1234"
|
||||||
|
fun createFailureResponse(exception: Exception? ): IFailedResponse
|
||||||
|
fun init() {
|
||||||
|
startQueueProcessor()
|
||||||
|
}
|
||||||
|
fun startQueueProcessor() {
|
||||||
|
getScope().launch {
|
||||||
|
while (isActive) {
|
||||||
|
val item = getRequestMutex().withLock {
|
||||||
|
getResponseQueue().poll()
|
||||||
|
}
|
||||||
|
if (item == null) {
|
||||||
|
delay(50)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processQueueItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun addToQueue(request: R,
|
||||||
|
deferredC: CompletableDeferred<ResponseResult<T, F>>,
|
||||||
|
priority: Int = 5,
|
||||||
|
maxRetries: Int = 3): Q
|
||||||
|
/**
|
||||||
|
* 提交请求
|
||||||
|
*/
|
||||||
|
suspend fun submitRequest(
|
||||||
|
request: R,
|
||||||
|
priority: Int = 5,
|
||||||
|
maxRetries: Int = 3
|
||||||
|
): ResponseResult<T, F> {
|
||||||
|
val deferred = CompletableDeferred<ResponseResult<T, F>>()
|
||||||
|
getRequestMutex().withLock {
|
||||||
|
addToQueue(request, deferred, priority, maxRetries)
|
||||||
|
}
|
||||||
|
return deferred.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun processQueueItem(item: Q) {
|
||||||
|
getSemaphore().withPermit {
|
||||||
|
val request = item.request
|
||||||
|
val deferred = item.deferred
|
||||||
|
val maxRetries = item.retries
|
||||||
|
var attempt = 0
|
||||||
|
var lastError: Exception?
|
||||||
|
|
||||||
|
while (attempt < maxRetries) {
|
||||||
|
try {
|
||||||
|
val fullUrl = buildFullUrlWithQueryParams(request)
|
||||||
|
if (!Environment.isProduction()) {
|
||||||
|
LoggerUtil.logger.debug("发送请求到: $fullUrl")
|
||||||
|
LoggerUtil.logger.debug("请求方法: {}", request.method())
|
||||||
|
}
|
||||||
|
val response = getClient().request(fullUrl) {
|
||||||
|
method = request.method()
|
||||||
|
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
headers {
|
||||||
|
request.headers().invoke(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于非GET请求,设置请求体
|
||||||
|
if (request.method() != HttpMethod.Get) {
|
||||||
|
setBody(request.toJSON())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val responseText: String = response.body()
|
||||||
|
|
||||||
|
if (!Environment.isProduction()) {
|
||||||
|
LoggerUtil.logger.debug("响应状态: {}", response.status)
|
||||||
|
LoggerUtil.logger.debug("响应内容: $responseText")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是HTML响应(重定向)
|
||||||
|
if (isHtmlResponse(responseText)) {
|
||||||
|
throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
val result = request.getResponse(responseText, response.status)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(deferred as CompletableDeferred<ResponseResult<IResponse, IFailedResponse>>).complete(result)
|
||||||
|
|
||||||
|
return
|
||||||
|
} catch (e: Exception) {
|
||||||
|
lastError = e
|
||||||
|
attempt++
|
||||||
|
|
||||||
|
if (!request.shouldRetryOnFailure() || attempt >= maxRetries) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerUtil.logger.warn("${getType()} 请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
||||||
|
delay((attempt * 1000L)) // 指数退避
|
||||||
|
}
|
||||||
|
// 所有重试都失败或不应重试
|
||||||
|
val errorResponse = createFailureResponse(lastError)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(deferred as CompletableDeferred<ResponseResult<IResponse, IFailedResponse>>).complete(
|
||||||
|
ResponseResult.Failure(errorResponse)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 构建完整的URL,包含查询参数
|
||||||
|
*/
|
||||||
|
fun buildFullUrlWithQueryParams(request: R): String {
|
||||||
|
val baseUrl = getBaseUrl().removeSuffix("/")
|
||||||
|
val path = request.path().removePrefix("/")
|
||||||
|
|
||||||
|
// 构建基础URL
|
||||||
|
val urlBuilder = StringBuilder("$baseUrl/$path")
|
||||||
|
|
||||||
|
// 添加查询参数
|
||||||
|
val queryParams = request.queryParameters().entries.joinToString("&") { (key, value) ->
|
||||||
|
"${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryParams.isNotEmpty()) {
|
||||||
|
urlBuilder.append("?").append(queryParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlBuilder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否是HTML响应
|
||||||
|
*/
|
||||||
|
fun isHtmlResponse(text: String): Boolean {
|
||||||
|
return text.contains("<!DOCTYPE html>", ignoreCase = true) ||
|
||||||
|
text.contains("<html>", ignoreCase = true) ||
|
||||||
|
text.contains("Redirecting", ignoreCase = true)
|
||||||
|
}
|
||||||
|
override fun close() {
|
||||||
|
getScope().cancel()
|
||||||
|
runBlocking {
|
||||||
|
getClient().close()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,26 @@
|
||||||
package top.r3944realms.ltdmanager.core.client
|
package top.r3944realms.ltdmanager.core.client
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
||||||
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
|
|
||||||
interface IQueueItem<out T:IResponse, out F:IFailedResponse> : Comparable<IQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
|
open class QueueItem<R: IRequest<T, F>, T:IResponse, F:IFailedResponse>(
|
||||||
fun getRequest(): IRequest<T,F>
|
val request: R,
|
||||||
fun getDeferred(): CompletableFuture<*>
|
val deferred: CompletableDeferred<*>,
|
||||||
fun getRetries(): Int
|
val retries: Int,
|
||||||
fun getPriority(): Int
|
val priority: Int,
|
||||||
|
val expectsResponse: Boolean
|
||||||
|
|
||||||
|
) : Comparable<QueueItem<R, T, F>> {
|
||||||
|
// fun getRequest(): R = request
|
||||||
|
// fun getDeferred(): CompletableDeferred<*> = deferred
|
||||||
|
// fun getRetries(): Int = retries
|
||||||
|
// fun getPriority(): Int = priority
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return true 表示返回 BlessingSkinResponse, false 表示 Unit
|
* @return true 表示返回 BlessingSkinResponse, false 表示 Unit
|
||||||
*/
|
*/
|
||||||
fun expectsResponse(): Boolean
|
fun expectsResponse(): Boolean = expectsResponse
|
||||||
override fun compareTo(other: IQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = getPriority().compareTo(other.getPriority())
|
override fun compareTo(other: QueueItem<R, @UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,76 @@
|
||||||
package top.r3944realms.ltdmanager.core.client.request
|
package top.r3944realms.ltdmanager.core.client.request
|
||||||
|
|
||||||
interface IRequest {
|
import io.ktor.http.*
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
|
|
||||||
|
interface IRequest<out T: IResponse, out F: IFailedResponse> {
|
||||||
|
// 只使用属性
|
||||||
|
val createTime: Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为JSON字符串
|
||||||
|
*/
|
||||||
|
fun toJSON(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取API路径(不包含基础URL)
|
||||||
|
* 例如: "invitation-codes/generate"
|
||||||
|
*/
|
||||||
|
fun path(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取HTTP方法,默认为GET(因为大多数API使用GET+查询参数)
|
||||||
|
*/
|
||||||
|
fun method(): HttpMethod = HttpMethod.Get
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义请求头
|
||||||
|
*/
|
||||||
|
fun headers(): HeadersBuilder.() -> Unit = {
|
||||||
|
// 默认添加Content-Type
|
||||||
|
append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
// 添加Accept头
|
||||||
|
append(HttpHeaders.Accept, "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取查询参数(用于URL参数)
|
||||||
|
* 例如: mapOf("token" to "abc123", "amount" to "1")
|
||||||
|
*/
|
||||||
|
fun queryParameters(): Map<String, String> = emptyMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求体参数(用于POST请求的JSON body)
|
||||||
|
* 例如: mapOf("token" to "abc123", "amount" to 1)
|
||||||
|
*/
|
||||||
|
fun bodyParameters(): Map<String, Any> = emptyMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求体内容类型,默认为Application.Json
|
||||||
|
*/
|
||||||
|
fun contentType(): ContentType = ContentType.Application.Json
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析响应JSON字符串
|
||||||
|
* @param responseJson 响应JSON字符串
|
||||||
|
* @param httpStatusCode HTTP状态码
|
||||||
|
*/
|
||||||
|
fun getResponse(responseJson: String, httpStatusCode: HttpStatusCode): ResponseResult<T, F>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预期的成功响应类型名称(用于日志和调试)
|
||||||
|
*/
|
||||||
|
fun expectedResponseType(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预期的失败响应类型名称(用于日志和调试)
|
||||||
|
*/
|
||||||
|
fun expectedFailureType(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要在失败时重试(默认重试)
|
||||||
|
*/
|
||||||
|
fun shouldRetryOnFailure(): Boolean = true
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
package top.r3944realms.ltdmanager.core.client.response
|
package top.r3944realms.ltdmanager.core.client.response
|
||||||
|
|
||||||
class IFailedResponse {
|
interface IFailedResponse : IResponse {
|
||||||
|
val failedMessage: String
|
||||||
|
val thrownException: Exception
|
||||||
|
get() = Exception(failedMessage)
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
package top.r3944realms.ltdmanager.core.client.response
|
package top.r3944realms.ltdmanager.core.client.response
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
interface IResponse {
|
interface IResponse {
|
||||||
|
val httpStatusCode: HttpStatusCode
|
||||||
|
val createTime: Long
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,47 @@
|
||||||
package top.r3944realms.ltdmanager.core.client.response
|
package top.r3944realms.ltdmanager.core.client.response
|
||||||
|
|
||||||
class ResponseResult {
|
sealed class ResponseResult<out T: IResponse, out F: IFailedResponse> {
|
||||||
|
data class Success<T : IResponse>(val response: T) : ResponseResult<T, Nothing>()
|
||||||
|
data class Failure<F : IFailedResponse>(val failure: F) : ResponseResult<Nothing, F>()
|
||||||
|
/**
|
||||||
|
* 检查是否成功
|
||||||
|
*/
|
||||||
|
fun isSuccess(): Boolean = this is Success
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取成功响应(如果存在)
|
||||||
|
*/
|
||||||
|
fun getSuccessResponse(): T? = (this as? Success)?.response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取失败响应(如果存在)
|
||||||
|
*/
|
||||||
|
fun getFailureResponse(): F? = (this as? Failure)?.failure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功时执行操作
|
||||||
|
*/
|
||||||
|
inline fun onSuccess(action: (T) -> Unit): ResponseResult<T, F> {
|
||||||
|
if (this is Success) action(response)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败时执行操作
|
||||||
|
*/
|
||||||
|
inline fun onFailure(action: (F) -> Unit): ResponseResult<T, F> {
|
||||||
|
if (this is Failure) action(failure)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRetResponse(): T {
|
||||||
|
if (this is Success) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
else if (this is Failure) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return failure as T
|
||||||
|
}
|
||||||
|
throw Exception("Never Reach")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,54 @@
|
||||||
package top.r3944realms.ltdmanager.core.config
|
package top.r3944realms.ltdmanager.core.config
|
||||||
|
|
||||||
class McsmConfig {
|
import top.r3944realms.ltdmanager.utils.CryptoUtil
|
||||||
|
import top.r3944realms.ltdmanager.utils.YamlUpdater
|
||||||
|
|
||||||
|
data class McsmConfig(
|
||||||
|
var url: String ?= null,
|
||||||
|
var encryptedApiKey: String ?= null,
|
||||||
|
var instanceID: String ?= null,
|
||||||
|
) {
|
||||||
|
val decryptedApiKey: String?
|
||||||
|
get() {
|
||||||
|
if (encryptedApiKey == null) return null
|
||||||
|
if (!isEncrypted()) return encryptedApiKey
|
||||||
|
try {
|
||||||
|
val cipherText = encryptedApiKey!!.substring(4, encryptedApiKey!!.length - 1)
|
||||||
|
return CryptoUtil.decrypt(cipherText)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("API解密失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密密码(如果未加密),并写回配置文件
|
||||||
|
*/
|
||||||
|
fun encryptApi() {
|
||||||
|
if (encryptedApiKey == null || isEncrypted()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
encryptedApiKey = "ENC(${CryptoUtil.encrypt(encryptedApiKey!!)})"
|
||||||
|
YamlUpdater.updateYaml(
|
||||||
|
YamlConfigLoader.configFilePath.toString(),
|
||||||
|
"mcsm.encrypted-api-key",
|
||||||
|
this.encryptedApiKey!!
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("API加密失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查密码是否已加密
|
||||||
|
*/
|
||||||
|
private fun isEncrypted(): Boolean {
|
||||||
|
return encryptedApiKey != null &&
|
||||||
|
encryptedApiKey!!.startsWith("ENC(") &&
|
||||||
|
encryptedApiKey!!.endsWith(")")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "McsmConfig(url=$url, api-key=***)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ object YamlConfigLoader {
|
||||||
config?.database?.encryptPassword()
|
config?.database?.encryptPassword()
|
||||||
config?.websocket?.encryptToken()
|
config?.websocket?.encryptToken()
|
||||||
config?.http?.encryptToken()
|
config?.http?.encryptToken()
|
||||||
|
config?.mcsm?.encryptApi()
|
||||||
config?.mail?.encryptPassword()
|
config?.mail?.encryptPassword()
|
||||||
config?.tools?.rcon?.encryptPassword()
|
config?.tools?.rcon?.encryptPassword()
|
||||||
config?.blessingSkinServer?.invitationApi?.encryptToken()
|
config?.blessingSkinServer?.invitationApi?.encryptToken()
|
||||||
|
|
@ -72,6 +73,7 @@ object YamlConfigLoader {
|
||||||
|
|
||||||
fun loadDatabaseConfig(): DatabaseConfig = config.database
|
fun loadDatabaseConfig(): DatabaseConfig = config.database
|
||||||
fun loadCryptoConfig(): CryptoConfig = config.crypto
|
fun loadCryptoConfig(): CryptoConfig = config.crypto
|
||||||
|
fun loadMcsmConfig(): McsmConfig = config.mcsm
|
||||||
fun loadWebsocketConfig(): WebsocketConfig = config.websocket
|
fun loadWebsocketConfig(): WebsocketConfig = config.websocket
|
||||||
fun loadHttpConfig(): HttpConfig = config.http
|
fun loadHttpConfig(): HttpConfig = config.http
|
||||||
fun loadModeConfig(): ModeConfig = config.mode
|
fun loadModeConfig(): ModeConfig = config.mode
|
||||||
|
|
@ -88,6 +90,7 @@ object YamlConfigLoader {
|
||||||
var http: HttpConfig = HttpConfig(),
|
var http: HttpConfig = HttpConfig(),
|
||||||
var tools: ToolConfig = ToolConfig(),
|
var tools: ToolConfig = ToolConfig(),
|
||||||
var mail: MailConfig = MailConfig(),
|
var mail: MailConfig = MailConfig(),
|
||||||
|
var mcsm: McsmConfig = McsmConfig(),
|
||||||
var blessingSkinServer: BlessingSkinServerConfig = BlessingSkinServerConfig(),
|
var blessingSkinServer: BlessingSkinServerConfig = BlessingSkinServerConfig(),
|
||||||
var dgLab: DgLabConfig = DgLabConfig(),
|
var dgLab: DgLabConfig = DgLabConfig(),
|
||||||
var imgTu: ImgTuConfig = ImgTuConfig(),
|
var imgTu: ImgTuConfig = ImgTuConfig(),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package top.r3944realms.ltdmanager.core.init
|
package top.r3944realms.ltdmanager.core.init
|
||||||
|
|
||||||
class DependencyResolver {
|
class DependencyResolver() {
|
||||||
}
|
}
|
||||||
|
|
@ -1,26 +1,149 @@
|
||||||
package top.r3944realms.ltdmanager.core.config
|
package top.r3944realms.ltdmanager.core.init
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.module.Modules
|
||||||
|
import top.r3944realms.ltdmanager.module.exception.ConfigError
|
||||||
|
|
||||||
data class ModuleConfig(
|
data class ModuleConfig(
|
||||||
val name: String,
|
val name: String,
|
||||||
val type: ModuleType,
|
val type: ModuleType,
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
val
|
val dependencies: List<Dependency> = emptyList(),
|
||||||
|
val config: Map<String, Any> = emptyMap()
|
||||||
) {
|
) {
|
||||||
data class Dependency(
|
data class Dependency(
|
||||||
val moduleName: String, // 依赖的模块名称
|
private val name: String,
|
||||||
val type: DependencyType, // 依赖类型
|
val type: ModuleType,
|
||||||
val required: Boolean = true // 是否必需
|
val required: Boolean = true
|
||||||
)
|
) {
|
||||||
enum class ModuleType {
|
private val dependencyName: String = "${type.modName}-$name"
|
||||||
GROUP_MESSAGE_POLLING_MODULE,
|
|
||||||
GROUP_REQUEST_HANDLER_MODULE,
|
fun getDepName() :String = dependencyName
|
||||||
MAIL_MODULE,
|
|
||||||
BAN_MODULE,
|
}
|
||||||
DG_LAB_MODULE,
|
enum class ModuleType(val modName: String) {
|
||||||
INVITE_MODULE,
|
GROUP_MESSAGE_POLLING_MODULE(Modules.GROUP_MESSAGE_POLLING),
|
||||||
MC_SERVER_STATUS_MODULE,
|
GROUP_REQUEST_HANDLER_MODULE(Modules.GROUP_REQUEST_HANDLER),
|
||||||
RCON_PLAYER_LIST_MODULE,
|
MAIL_MODULE(Modules.MAIL),
|
||||||
STATE_MODULE
|
BAN_MODULE(Modules.BAN),
|
||||||
|
DG_LAB_MODULE(Modules.DG_LAB),
|
||||||
|
INVITE_MODULE(Modules.INVITATION_CODE),
|
||||||
|
MC_SERVER_STATUS_MODULE(Modules.MC_SERVER_STATUS),
|
||||||
|
MOD_GROUP_HANDLER_MODULE(Modules.MOD_GROUP_HANDLER),
|
||||||
|
RCON_PLAYER_LIST_MODULE(Modules.RCON_PLAYER_LIST),
|
||||||
|
STATE_MODULE(Modules.STATE),
|
||||||
|
HELP_MODULE(Modules.HELP),;
|
||||||
|
}
|
||||||
|
// 基础获取方法
|
||||||
|
fun value(paramName: String): Any =
|
||||||
|
config[paramName] ?: throw ConfigError(
|
||||||
|
ConfigError.Type.MISSING_PARAMETER,
|
||||||
|
name,
|
||||||
|
paramName
|
||||||
|
)
|
||||||
|
|
||||||
|
// 泛型获取方法
|
||||||
|
private inline fun <reified T> get(paramName: String): T {
|
||||||
|
val value = value(paramName)
|
||||||
|
return when (T::class) {
|
||||||
|
Long::class -> convertToLong(value, paramName) as T
|
||||||
|
Int::class -> convertToInt(value, paramName) as T
|
||||||
|
String::class -> value.toString() as T
|
||||||
|
Boolean::class -> convertToBoolean(value, paramName) as T
|
||||||
|
Double::class -> convertToDouble(value, paramName) as T
|
||||||
|
Float::class -> convertToFloat(value, paramName) as T
|
||||||
|
else -> {
|
||||||
|
if (value is T) value
|
||||||
|
else throw typeMismatchError<T>(value, paramName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特定类型方法(向后兼容)
|
||||||
|
fun long(paramName: String): Long = get<Long>(paramName)
|
||||||
|
fun int(paramName: String): Int = get<Int>(paramName)
|
||||||
|
fun string(paramName: String): String = get<String>(paramName)
|
||||||
|
fun boolean(paramName: String): Boolean = get<Boolean>(paramName)
|
||||||
|
fun double(paramName: String): Double = get<Double>(paramName)
|
||||||
|
fun float(paramName: String): Float = get<Float>(paramName)
|
||||||
|
|
||||||
|
// 可选值方法
|
||||||
|
inline fun <reified T> getOrNull(paramName: String): T? =
|
||||||
|
config[paramName] as? T ?: run {
|
||||||
|
val value = config[paramName]
|
||||||
|
if (value == null) null
|
||||||
|
else if (value is T) value
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> getOrDefault(paramName: String, defaultValue: T): T =
|
||||||
|
getOrNull<T>(paramName) ?: defaultValue
|
||||||
|
|
||||||
|
// 类型转换辅助方法
|
||||||
|
private fun convertToLong(value: Any, paramName: String): Long = when (value) {
|
||||||
|
is Long -> value
|
||||||
|
is Number -> value.toLong()
|
||||||
|
is String -> try {
|
||||||
|
value.toLong()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw typeMismatchError<Long>(value, paramName)
|
||||||
|
}
|
||||||
|
else -> throw typeMismatchError<Long>(value, paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToInt(value: Any, paramName: String): Int = when (value) {
|
||||||
|
is Int -> value
|
||||||
|
is Number -> value.toInt()
|
||||||
|
is String -> try {
|
||||||
|
value.toInt()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw typeMismatchError<Int>(value, paramName)
|
||||||
|
}
|
||||||
|
else -> throw typeMismatchError<Int>(value, paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToBoolean(value: Any, paramName: String): Boolean = when (value) {
|
||||||
|
is Boolean -> value
|
||||||
|
is String -> when (value.lowercase()) {
|
||||||
|
"true", "yes", "1" -> true
|
||||||
|
"false", "no", "0" -> false
|
||||||
|
else -> throw typeMismatchError<Boolean>(value, paramName)
|
||||||
|
}
|
||||||
|
is Number -> value.toInt() != 0
|
||||||
|
else -> throw typeMismatchError<Boolean>(value, paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToDouble(value: Any, paramName: String): Double = when (value) {
|
||||||
|
is Double -> value
|
||||||
|
is Number -> value.toDouble()
|
||||||
|
is String -> try {
|
||||||
|
value.toDouble()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw typeMismatchError<Double>(value, paramName)
|
||||||
|
}
|
||||||
|
else -> throw typeMismatchError<Double>(value, paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToFloat(value: Any, paramName: String): Float = when (value) {
|
||||||
|
is Float -> value
|
||||||
|
is Number -> value.toFloat()
|
||||||
|
is String -> try {
|
||||||
|
value.toFloat()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw typeMismatchError<Float>(value, paramName)
|
||||||
|
}
|
||||||
|
else -> throw typeMismatchError<Float>(value, paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理辅助方法
|
||||||
|
private inline fun <reified T> typeMismatchError(
|
||||||
|
actualValue: Any,
|
||||||
|
paramName: String
|
||||||
|
): Nothing {
|
||||||
|
throw ConfigError(
|
||||||
|
ConfigError.Type.NOT_EXPECTED_VALUE,
|
||||||
|
name,
|
||||||
|
T::class.simpleName ?: T::class.java.simpleName,
|
||||||
|
actualValue::class.simpleName ?: actualValue::class.java.simpleName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,32 @@
|
||||||
package top.r3944realms.ltdmanager.core.init
|
package top.r3944realms.ltdmanager.core.init
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.GlobalManager
|
||||||
|
import top.r3944realms.ltdmanager.core.init.ModuleConfig.ModuleType.*
|
||||||
|
import top.r3944realms.ltdmanager.module.BaseModule
|
||||||
|
import top.r3944realms.ltdmanager.module.GroupRequestHandlerModule
|
||||||
|
|
||||||
object ModuleFactory {
|
object ModuleFactory {
|
||||||
|
fun createModule(config: ModuleConfig): BaseModule {
|
||||||
|
return when(config.type) {
|
||||||
|
GROUP_MESSAGE_POLLING_MODULE -> TODO()
|
||||||
|
GROUP_REQUEST_HANDLER_MODULE -> createGroupRequestHandler(config)
|
||||||
|
MAIL_MODULE -> TODO()
|
||||||
|
BAN_MODULE -> TODO()
|
||||||
|
DG_LAB_MODULE -> TODO()
|
||||||
|
INVITE_MODULE -> TODO()
|
||||||
|
MC_SERVER_STATUS_MODULE -> TODO()
|
||||||
|
RCON_PLAYER_LIST_MODULE -> TODO()
|
||||||
|
STATE_MODULE -> TODO()
|
||||||
|
MOD_GROUP_HANDLER_MODULE -> TODO()
|
||||||
|
HELP_MODULE -> TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun createGroupRequestHandler(config: ModuleConfig): GroupRequestHandlerModule {
|
||||||
|
val targetGroupId = config.long("targetGroupId")
|
||||||
|
val pollIntervalMillis = config.getOrDefault("pollIntervalMillis", 30_000L)
|
||||||
|
return GroupRequestHandlerModule(
|
||||||
|
config.name, GlobalManager.napCatClient,
|
||||||
|
targetGroupId, pollIntervalMillis
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
package top.r3944realms.ltdmanager.core.init
|
package top.r3944realms.ltdmanager.core.init
|
||||||
|
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
object ModuleLoader {
|
object ModuleLoader {
|
||||||
|
val configFilePath = Paths.get("config/modules.yaml")
|
||||||
|
init {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,15 @@
|
||||||
package top.r3944realms.ltdmanager.core.init
|
package top.r3944realms.ltdmanager.core.init
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.module.BaseModule
|
||||||
|
|
||||||
object ModuleRegistry {
|
object ModuleRegistry {
|
||||||
|
private val registry: MutableMap<String, BaseModule> = mutableMapOf()
|
||||||
|
|
||||||
|
fun register(baseModule: BaseModule) {
|
||||||
|
registry.putIfAbsent(baseModule.name, baseModule)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(moduleName: String): BaseModule? {
|
||||||
|
return registry[moduleName]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import top.r3944realms.ltdmanager.GlobalManager
|
import top.r3944realms.ltdmanager.GlobalManager
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||||
|
|
@ -104,7 +106,7 @@ class GameClientOperation(
|
||||||
|
|
||||||
pattern.replace(originalUrl) { matchResult ->
|
pattern.replace(originalUrl) { matchResult ->
|
||||||
// 保留原始 URL 中的路径部分(如果有的话)
|
// 保留原始 URL 中的路径部分(如果有的话)
|
||||||
val path = matchResult.groupValues[1] ?: ""
|
val path = matchResult.groupValues[1]
|
||||||
"$configUrl$path"
|
"$configUrl$path"
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -130,8 +132,7 @@ class GameClientOperation(
|
||||||
albumId = "BFx",
|
albumId = "BFx",
|
||||||
expiration = "PT5M"
|
expiration = "PT5M"
|
||||||
)
|
)
|
||||||
if (response.image?.url != null) {
|
if (response is CheveretoUploadResponse){
|
||||||
// 发送图床 URL 给玩家
|
|
||||||
napCatClient.sendUnit(
|
napCatClient.sendUnit(
|
||||||
SendPrivateMsgRequest(
|
SendPrivateMsgRequest(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -141,8 +142,15 @@ class GameClientOperation(
|
||||||
ID.long(playerId)
|
ID.long(playerId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else if (response is FailedCheveretoResponse.Default){
|
||||||
LoggerUtil.logger.error("上传二维码返回 JSON 未包含 URL")
|
napCatClient.sendUnit(
|
||||||
|
SendPrivateMsgRequest(
|
||||||
|
listOf(
|
||||||
|
MessageElement.text("无法上传图片,请联系管理员:${response.httpStatusCode} , ${response.failedMessage}"),
|
||||||
|
),
|
||||||
|
ID.long(playerId)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// 启动 60 秒倒计时任务
|
// 启动 60 秒倒计时任务
|
||||||
bindingTimeoutJob = launch {
|
bindingTimeoutJob = launch {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package top.r3944realms.ltdmanager
|
||||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
import top.r3944realms.ltdmanager.module.*
|
import top.r3944realms.ltdmanager.module.*
|
||||||
|
|
||||||
|
// DSL
|
||||||
fun main() = GlobalManager.runBlockingMain {
|
fun main() = GlobalManager.runBlockingMain {
|
||||||
val commonGroupId:Long = 538751386
|
val commonGroupId:Long = 538751386
|
||||||
val whitelistGroupId:Long = 920719236
|
val whitelistGroupId:Long = 920719236
|
||||||
|
|
@ -43,7 +43,7 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
)
|
)
|
||||||
val toolConfig = YamlConfigLoader.loadToolConfig()
|
val toolConfig = YamlConfigLoader.loadToolConfig()
|
||||||
val corconModule = RconPlayerListModule(
|
val corconModule = RconPlayerListModule(
|
||||||
moduleName = "WhiteListGroup",
|
moduleName = "CommonGroup",
|
||||||
groupMessagePollingModule = commonGroupMsgPollingModule,
|
groupMessagePollingModule = commonGroupMsgPollingModule,
|
||||||
rconTimeOut = 2_000L,
|
rconTimeOut = 2_000L,
|
||||||
cooldownMillis = 10_000L,
|
cooldownMillis = 10_000L,
|
||||||
|
|
@ -98,30 +98,30 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
// "Apply for an invitation code"
|
// "Apply for an invitation code"
|
||||||
// )
|
// )
|
||||||
// )
|
// )
|
||||||
val commonMcServerStatusModule = McServerStatusModule(
|
// val commonMcServerStatusModule = McServerStatusModule(
|
||||||
moduleName = "CommonGroup",
|
// moduleName = "CommonGroup",
|
||||||
groupMessagePollingModule = commonGroupMsgPollingModule,
|
// groupMessagePollingModule = commonGroupMsgPollingModule,
|
||||||
selfId = selfQQId,
|
// selfId = selfQQId,
|
||||||
cooldownMillis = 20_000L,
|
// cooldownMillis = 20_000L,
|
||||||
selfNickName = selfNickName,
|
// selfNickName = selfNickName,
|
||||||
commands = listOf("/m", "/mcs", "seek", "s", "test"),
|
// commands = listOf("/m", "/mcs", "seek", "s", "test"),
|
||||||
presetServer = mapOf(
|
// presetServer = mapOf(
|
||||||
setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
// setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
||||||
setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
|
// setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
val whitelistMcServerStatusModule = McServerStatusModule(
|
// val whitelistMcServerStatusModule = McServerStatusModule(
|
||||||
moduleName = "WhiteListGroup",
|
// moduleName = "WhiteListGroup",
|
||||||
groupMessagePollingModule = whiteListGroupMsgPollingModule,
|
// groupMessagePollingModule = whiteListGroupMsgPollingModule,
|
||||||
selfId = selfQQId,
|
// selfId = selfQQId,
|
||||||
cooldownMillis = 20_000L,
|
// cooldownMillis = 20_000L,
|
||||||
selfNickName = selfNickName,
|
// selfNickName = selfNickName,
|
||||||
commands = listOf("/m", "/mcs", "seek", "s", "test"),
|
// commands = listOf("/m", "/mcs", "seek", "s", "test"),
|
||||||
presetServer = mapOf(
|
// presetServer = mapOf(
|
||||||
setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
// setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
||||||
setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
|
// setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
val dgLabModule = DGLabModule(
|
val dgLabModule = DGLabModule(
|
||||||
moduleName = "DG",
|
moduleName = "DG",
|
||||||
groupMessagePollingModule = commonGroupMsgPollingModule,
|
groupMessagePollingModule = commonGroupMsgPollingModule,
|
||||||
|
|
@ -134,10 +134,10 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
GlobalManager.moduleManager.registerModule(groupModule)
|
GlobalManager.moduleManager.registerModule(groupModule)
|
||||||
GlobalManager.moduleManager.registerModule(commonGroupMsgPollingModule)
|
GlobalManager.moduleManager.registerModule(commonGroupMsgPollingModule)
|
||||||
GlobalManager.moduleManager.registerModule(whiteListGroupMsgPollingModule)
|
GlobalManager.moduleManager.registerModule(whiteListGroupMsgPollingModule)
|
||||||
GlobalManager.moduleManager.registerModule(commonMcServerStatusModule)
|
// GlobalManager.moduleManager.registerModule(commonMcServerStatusModule)
|
||||||
GlobalManager.moduleManager.registerModule(rconModule)
|
GlobalManager.moduleManager.registerModule(rconModule)
|
||||||
GlobalManager.moduleManager.registerModule(corconModule)
|
GlobalManager.moduleManager.registerModule(corconModule)
|
||||||
GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule)
|
// GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule)
|
||||||
// GlobalManager.moduleManager.registerModule(mailModule)
|
// GlobalManager.moduleManager.registerModule(mailModule)
|
||||||
// GlobalManager.moduleManager.registerModule(invitationCodesModule)
|
// GlobalManager.moduleManager.registerModule(invitationCodesModule)
|
||||||
GlobalManager.moduleManager.registerModule(whitelistHelpModule)
|
GlobalManager.moduleManager.registerModule(whitelistHelpModule)
|
||||||
|
|
@ -150,13 +150,13 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
GlobalManager.moduleManager.loadModule(groupModule.name)
|
GlobalManager.moduleManager.loadModule(groupModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(commonGroupMsgPollingModule.name)
|
GlobalManager.moduleManager.loadModule(commonGroupMsgPollingModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(whiteListGroupMsgPollingModule.name)
|
GlobalManager.moduleManager.loadModule(whiteListGroupMsgPollingModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name)
|
// GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(corconModule.name)
|
GlobalManager.moduleManager.loadModule(corconModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(rconModule.name)
|
GlobalManager.moduleManager.loadModule(rconModule.name)
|
||||||
// GlobalManager.moduleManager.loadModule(mailModule.name)
|
// GlobalManager.moduleManager.loadModule(mailModule.name)
|
||||||
// GlobalManager.moduleManager.loadModule(invitationCodesModule.name)
|
// GlobalManager.moduleManager.loadModule(invitationCodesModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(commonHelpModule.name)
|
GlobalManager.moduleManager.loadModule(commonHelpModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name)
|
// GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(whitelistHelpModule.name)
|
GlobalManager.moduleManager.loadModule(whitelistHelpModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(dgLabModule.name)
|
GlobalManager.moduleManager.loadModule(dgLabModule.name)
|
||||||
// GlobalManager.moduleManager.loadModule(banModule.name)
|
// GlobalManager.moduleManager.loadModule(banModule.name)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin
|
package top.r3944realms.ltdmanager.mcms
|
||||||
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
|
|
@ -11,17 +11,17 @@ import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
|
||||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
|
import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.ResponseResult
|
||||||
import top.r3944realms.ltdmanager.utils.Environment
|
import top.r3944realms.ltdmanager.utils.Environment
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class BlessingSkinClient private constructor() : AutoCloseable {
|
class MCSMClient private constructor() : AutoCloseable {
|
||||||
private val client = HttpClient(CIO) {
|
private val client = HttpClient(CIO) {
|
||||||
expectSuccess = false
|
expectSuccess = false
|
||||||
|
|
||||||
|
|
@ -40,7 +40,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
||||||
// 限流控制
|
// 限流控制
|
||||||
private val semaphore = Semaphore(5)
|
private val semaphore = Semaphore(5)
|
||||||
private val requestMutex = Mutex()
|
private val requestMutex = Mutex()
|
||||||
private val requestQueue = PriorityQueue<BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>>(compareBy { it.priority })
|
private val requestQueue = PriorityQueue<MCSMSkinQueueItem<MCSMResponse, FailedMCSMResponse>>(compareBy { it.priority })
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -50,14 +50,14 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
||||||
/**
|
/**
|
||||||
* 提交请求
|
* 提交请求
|
||||||
*/
|
*/
|
||||||
suspend fun <T : BlessingSkinResponse, F : FailedBlessingSkinResponse> submitRequest(
|
suspend fun <T : MCSMResponse, F : FailedMCSMResponse> submitRequest(
|
||||||
request: BlessingSkinRequest<T, F>,
|
request: MCSMRequest<T, F>,
|
||||||
priority: Int = 5,
|
priority: Int = 5,
|
||||||
maxRetries: Int = 3
|
maxRetries: Int = 3
|
||||||
): ResponseResult<T, F> {
|
): ResponseResult<T, F> {
|
||||||
val deferred = CompletableDeferred<ResponseResult<T, F>>()
|
val deferred = CompletableDeferred<ResponseResult<T, F>>()
|
||||||
requestMutex.withLock {
|
requestMutex.withLock {
|
||||||
requestQueue.add(BlessingSkinQueueItem(request, deferred, priority, maxRetries, true))
|
requestQueue.add(MCSMSkinQueueItem(request, deferred, priority, maxRetries, true))
|
||||||
}
|
}
|
||||||
return deferred.await()
|
return deferred.await()
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +83,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
||||||
/**
|
/**
|
||||||
* 处理队列项
|
* 处理队列项
|
||||||
*/
|
*/
|
||||||
private suspend fun processQueueItem(item: BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>) {
|
private suspend fun processQueueItem(item: MCSMSkinQueueItem<MCSMResponse, FailedMCSMResponse>) {
|
||||||
semaphore.withPermit {
|
semaphore.withPermit {
|
||||||
val (request, deferred, _, maxRetries, _) = item
|
val (request, deferred, _, maxRetries, _) = item
|
||||||
var attempt = 0
|
var attempt = 0
|
||||||
|
|
@ -130,7 +130,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
||||||
val result = request.getResponse(responseText, response.status)
|
val result = request.getResponse(responseText, response.status)
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
(deferred as CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).complete(result)
|
(deferred as CompletableDeferred<ResponseResult<MCSMResponse, FailedMCSMResponse>>).complete(result)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -142,15 +142,15 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
LoggerUtil.logger.warn("BlessingSkin请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
LoggerUtil.logger.warn("MCSM请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
||||||
delay((attempt * 1000L)) // 指数退避
|
delay((attempt * 1000L)) // 指数退避
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有重试都失败或不应重试
|
// 所有重试都失败或不应重试
|
||||||
val errorResponse = createFailureResponse(lastError, request)
|
val errorResponse = createFailureResponse(lastError)
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
(deferred as CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).complete(
|
(deferred as CompletableDeferred<ResponseResult<MCSMResponse, FailedMCSMResponse>>).complete(
|
||||||
ResponseResult.Failure(errorResponse)
|
ResponseResult.Failure(errorResponse)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +159,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
||||||
/**
|
/**
|
||||||
* 构建完整的URL,包含查询参数
|
* 构建完整的URL,包含查询参数
|
||||||
*/
|
*/
|
||||||
private fun buildFullUrlWithQueryParams(request: BlessingSkinRequest<*, *>): String {
|
private fun buildFullUrlWithQueryParams(request: MCSMRequest<*, *>): String {
|
||||||
val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/")
|
val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/")
|
||||||
val path = request.path().removePrefix("/")
|
val path = request.path().removePrefix("/")
|
||||||
|
|
||||||
|
|
@ -191,11 +191,10 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
||||||
* 创建失败响应
|
* 创建失败响应
|
||||||
*/
|
*/
|
||||||
private fun createFailureResponse(
|
private fun createFailureResponse(
|
||||||
exception: Exception?,
|
exception: Exception?
|
||||||
request: BlessingSkinRequest<*, *>
|
): FailedMCSMResponse {
|
||||||
): FailedBlessingSkinResponse {
|
return FailedMCSMResponse.ExceptionFailedMCSMResponse(
|
||||||
return FailedBlessingSkinResponse.Default(
|
result = exception?.message ?: "未知错误",
|
||||||
failedResult = exception?.message ?: "未知错误",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,6 +206,6 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun create(): BlessingSkinClient = BlessingSkinClient()
|
fun create(): MCSMClient = MCSMClient()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin
|
package top.r3944realms.ltdmanager.mcms
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||||
|
|
||||||
data class BlessingSkinQueueItem<out T:BlessingSkinResponse,out F:FailedBlessingSkinResponse>(
|
data class MCSMSkinQueueItem<out T:MCSMResponse,out F:FailedMCSMResponse>(
|
||||||
val request: BlessingSkinRequest<T,F>,
|
val request: MCSMRequest<T,F>,
|
||||||
val deferred: CompletableDeferred<*>,
|
val deferred: CompletableDeferred<*>,
|
||||||
var retries: Int,
|
var retries: Int,
|
||||||
val priority: Int,
|
val priority: Int,
|
||||||
val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit
|
val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit
|
||||||
) : Comparable<BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
|
) : Comparable<MCSMSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
|
||||||
override fun compareTo(other: BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
override fun compareTo(other: MCSMSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin.request
|
package top.r3944realms.ltdmanager.mcms.request
|
||||||
|
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
import top.r3944realms.ltdmanager.mcms.response.ResponseResult
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class BlessingSkinRequest<out T : BlessingSkinResponse, out F : FailedBlessingSkinResponse>(
|
abstract class MCSMRequest<out T : MCSMResponse, out F : FailedMCSMResponse>(
|
||||||
@Transient
|
@Transient
|
||||||
open val createTime: Long = System.currentTimeMillis()
|
open val createTime: Long = System.currentTimeMillis()
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,50 @@
|
||||||
package top.r3944realms.ltdmanager.mcms.request.instance
|
package top.r3944realms.ltdmanager.mcms.request.instance
|
||||||
|
|
||||||
class GetInstanceListRequest {
|
import io.ktor.http.*
|
||||||
}
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.ResponseResult
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.instance.GetInstanceListResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class GetInstanceListRequest(
|
||||||
|
private val daemonId: String,
|
||||||
|
private val page: Int,
|
||||||
|
private val pageSize: Int,
|
||||||
|
private val status: String,
|
||||||
|
private val instanceName: String? = null
|
||||||
|
) : MCSMRequest<GetInstanceListResponse, FailedMCSMResponse>() {
|
||||||
|
|
||||||
|
override fun toJSON(): String = "{}" // GET 无请求体
|
||||||
|
|
||||||
|
override fun path(): String = "api/service/remote_service_instances"
|
||||||
|
|
||||||
|
override fun queryParameters(): Map<String, String> =
|
||||||
|
buildMap {
|
||||||
|
put("daemonId", daemonId)
|
||||||
|
put("page", page.toString())
|
||||||
|
put("page_size", pageSize.toString())
|
||||||
|
put("status", status)
|
||||||
|
instanceName?.let { put("instance_name", it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getResponse(
|
||||||
|
responseJson: String,
|
||||||
|
httpStatusCode: HttpStatusCode
|
||||||
|
): ResponseResult<GetInstanceListResponse, FailedMCSMResponse> {
|
||||||
|
return if (httpStatusCode.value in 200..299) {
|
||||||
|
ResponseResult.Success(
|
||||||
|
kotlinx.serialization.json.Json.decodeFromString<GetInstanceListResponse>(responseJson)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ResponseResult.Failure(
|
||||||
|
kotlinx.serialization.json.Json.decodeFromString<FailedMCSMResponse>(responseJson)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun expectedResponseType(): String = GetInstanceListResponse::class.simpleName!!
|
||||||
|
|
||||||
|
override fun expectedFailureType(): String = FailedMCSMResponse::class.simpleName!!
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,53 @@
|
||||||
package top.r3944realms.ltdmanager.mcms.request.instance
|
package top.r3944realms.ltdmanager.mcms.request.instance
|
||||||
|
|
||||||
class StartInstanceRequest {
|
import io.ktor.http.*
|
||||||
}
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.ResponseResult
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.instance.StartInstanceResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动实例请求
|
||||||
|
* GET /api/protected_instance/open
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class StartInstanceRequest(
|
||||||
|
val uuid: String,
|
||||||
|
val daemonId: String
|
||||||
|
) : MCSMRequest<StartInstanceResponse, FailedMCSMResponse>() {
|
||||||
|
|
||||||
|
override fun toJSON(): String =
|
||||||
|
Json.encodeToString(this)
|
||||||
|
|
||||||
|
override fun path(): String =
|
||||||
|
"protected_instance/open"
|
||||||
|
|
||||||
|
override fun queryParameters(): Map<String, String> =
|
||||||
|
mapOf(
|
||||||
|
"uuid" to uuid,
|
||||||
|
"daemonId" to daemonId
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun method(): HttpMethod = HttpMethod.Get
|
||||||
|
|
||||||
|
override fun getResponse(
|
||||||
|
responseJson: String,
|
||||||
|
httpStatusCode: HttpStatusCode
|
||||||
|
): ResponseResult<StartInstanceResponse, FailedMCSMResponse> {
|
||||||
|
|
||||||
|
return if (httpStatusCode.value == 200) {
|
||||||
|
val obj = Json.decodeFromString(StartInstanceResponse.serializer(), responseJson)
|
||||||
|
ResponseResult.Success(obj)
|
||||||
|
} else {
|
||||||
|
val fail = Json.decodeFromString(FailedMCSMResponse.serializer(), responseJson)
|
||||||
|
ResponseResult.Failure(fail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun expectedResponseType(): String = "StartInstanceResponse"
|
||||||
|
|
||||||
|
override fun expectedFailureType(): String = "FailedMCSMResponse"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,29 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin.response
|
package top.r3944realms.ltdmanager.mcms.response
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class FailedBlessingSkinResponse: BlessingSkinResponse() {
|
open class FailedMCSMResponse(
|
||||||
abstract fun failedMessage(): String
|
@Transient
|
||||||
|
val status0: Status = Status.Ok,
|
||||||
|
val data: JsonObject? = null,
|
||||||
|
@Transient
|
||||||
|
val time0: Long = -1,
|
||||||
|
): MCSMResponse(
|
||||||
|
status0, time0
|
||||||
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
class Default(@Transient val failedResult: String? = "未知错误") : FailedBlessingSkinResponse() {
|
data class ExceptionFailedMCSMResponse(
|
||||||
override fun failedMessage(): String = failedResult!!
|
@Transient
|
||||||
|
val status1: Status = Status.Ok,
|
||||||
}
|
val data0: String? = null,
|
||||||
|
@Transient
|
||||||
|
val time1: Long = -1,
|
||||||
|
@Transient
|
||||||
|
val result: String? = null,
|
||||||
|
): FailedMCSMResponse(
|
||||||
|
status1, null, time1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin.response
|
package top.r3944realms.ltdmanager.mcms.response
|
||||||
|
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import kotlinx.serialization.modules.polymorphic
|
import kotlinx.serialization.modules.polymorphic
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
import top.r3944realms.ltdmanager.mcms.response.instance.GetInstanceListResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.instance.StartInstanceResponse
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class BlessingSkinResponse (
|
abstract class MCSMResponse (
|
||||||
|
open val status: Status,
|
||||||
|
open val time: Long,
|
||||||
@Transient
|
@Transient
|
||||||
open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||||
@Transient
|
@Transient
|
||||||
|
|
@ -17,20 +22,29 @@ abstract class BlessingSkinResponse (
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
// 通用的反序列化方法
|
// 通用的反序列化方法
|
||||||
inline fun <reified T : BlessingSkinResponse> decode(jsonString: String): T {
|
inline fun <reified T : MCSMResponse> decode(jsonString: String): T {
|
||||||
return json.decodeFromString(jsonString)
|
return json.decodeFromString(jsonString)
|
||||||
}
|
}
|
||||||
val json: Json by lazy {
|
val json: Json by lazy {
|
||||||
Json {
|
Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
serializersModule = SerializersModule {
|
serializersModule = SerializersModule {
|
||||||
polymorphic(BlessingSkinResponse::class) {
|
polymorphic(MCSMResponse::class) {
|
||||||
subclass(FailedBlessingSkinResponse.Default::class, FailedBlessingSkinResponse.Default.serializer())
|
subclass(GetInstanceListResponse::class, GetInstanceListResponse.serializer())
|
||||||
subclass(InvitationCodeGenerationResponse::class, InvitationCodeGenerationResponse.serializer())
|
subclass(StartInstanceResponse::class, StartInstanceResponse.serializer())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@Serializable
|
||||||
|
enum class Status(val value: String) {
|
||||||
|
@SerialName("200") Ok("200"),
|
||||||
|
@SerialName("400") ParamsNotRight("400"),
|
||||||
|
@SerialName("403") PermissionDenied("403"),
|
||||||
|
@SerialName("500") InternalServerError("500");
|
||||||
|
companion object {
|
||||||
|
fun isOk(value: Status): Boolean = value == Ok
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin.response
|
package top.r3944realms.ltdmanager.mcms.response
|
||||||
|
|
||||||
// 响应结果封装
|
// 响应结果封装
|
||||||
sealed class ResponseResult<out T : BlessingSkinResponse, out F : FailedBlessingSkinResponse> {
|
sealed class ResponseResult<out T : MCSMResponse, out F : FailedMCSMResponse> {
|
||||||
data class Success<T : BlessingSkinResponse>(val response: T) : ResponseResult<T, Nothing>()
|
data class Success<T : MCSMResponse>(val response: T) : ResponseResult<T, Nothing>()
|
||||||
data class Failure<F : FailedBlessingSkinResponse>(val failure: F) : ResponseResult<Nothing, F>()
|
data class Failure<F : FailedMCSMResponse>(val failure: F) : ResponseResult<Nothing, F>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否成功
|
* 检查是否成功
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,60 @@
|
||||||
package top.r3944realms.ltdmanager.mcms.response.instance
|
package top.r3944realms.ltdmanager.mcms.response.instance
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class InstanceListResponse(
|
data class GetInstanceListResponse(
|
||||||
val status: Int,
|
@Transient
|
||||||
val data: InstanceListData?,
|
val status0: Status = Status.Ok,
|
||||||
val time: Long
|
val data: InstanceListData? = null,
|
||||||
) : MCSMResponse
|
@Transient
|
||||||
|
val time0: Long = -1,
|
||||||
|
) : MCSMResponse(status0, time0) {
|
||||||
|
@Serializable
|
||||||
|
data class InstanceListData(
|
||||||
|
val maxPage: Int,
|
||||||
|
val pageSize: Int,
|
||||||
|
val data: List<InstanceDetail>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InstanceDetail(
|
||||||
|
val config: JsonObject? = null, //TODO: 不清楚是干什么的,需验证
|
||||||
|
val info: InstanceInfo,
|
||||||
|
val instanceUuid: String,
|
||||||
|
val processInfo: ProcessInfo,
|
||||||
|
val space: Long,
|
||||||
|
val started: Int,
|
||||||
|
val status: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InstanceInfo(
|
||||||
|
val currentPlayers: Int,
|
||||||
|
val fileLock: Int,
|
||||||
|
val maxPlayers: Int,
|
||||||
|
val openFrpStatus: Boolean,
|
||||||
|
val playersChart: List<PlayerChartItem>,
|
||||||
|
val version: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerChartItem(
|
||||||
|
val time: Long? = null,
|
||||||
|
val players: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ProcessInfo(
|
||||||
|
val cpu: Double,
|
||||||
|
val memory: Long,
|
||||||
|
val ppid: Long,
|
||||||
|
val pid: Long,
|
||||||
|
val ctime: Long,
|
||||||
|
val elapsed: Long,
|
||||||
|
val timestamp: Long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,21 @@
|
||||||
package top.r3944realms.ltdmanager.mcms.response.instance
|
package top.r3944realms.ltdmanager.mcms.response.instance
|
||||||
|
|
||||||
class StartInstanceResponse {
|
import kotlinx.serialization.Serializable
|
||||||
}
|
import kotlinx.serialization.Transient
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StartInstanceResponse(
|
||||||
|
@Transient
|
||||||
|
val status0: Status = Status.Ok,
|
||||||
|
val data: StartInstanceData,
|
||||||
|
@Transient
|
||||||
|
val time0: Long = -1
|
||||||
|
) : MCSMResponse(status0, time0){
|
||||||
|
@Serializable
|
||||||
|
data class StartInstanceData(
|
||||||
|
val instanceUuid: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,17 @@
|
||||||
package top.r3944realms.ltdmanager.module
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
class ApplyWhiteListModule {
|
class ApplyWhitelistModule(
|
||||||
|
moduleName: String,
|
||||||
|
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||||
|
private val cooldownMillis: Long = 120_000,
|
||||||
|
private val keywords: Set<String> = setOf("申请白名单")
|
||||||
|
):
|
||||||
|
BaseModule(Modules.APPLY_WHITELIST,moduleName) {
|
||||||
|
override fun onLoad() {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onUnload() {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,7 +35,7 @@ class BanModule(
|
||||||
private val maxBanMinutes: Int = 15,
|
private val maxBanMinutes: Int = 15,
|
||||||
private val factorX: Int = 2, // 系数 x,禁言倍数
|
private val factorX: Int = 2, // 系数 x,禁言倍数
|
||||||
|
|
||||||
) : BaseModule("BanModule", moduleName), PersistentState<BanModule.BanState> {
|
) : BaseModule(Modules.BAN, moduleName), PersistentState<BanModule.BanState> {
|
||||||
|
|
||||||
private val banCommandParse = CommandParser(muteCommandPrefixList)
|
private val banCommandParse = CommandParser(muteCommandPrefixList)
|
||||||
private val pardonCommandParse = CommandParser(unmuteCommandPrefixList)
|
private val pardonCommandParse = CommandParser(unmuteCommandPrefixList)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package top.r3944realms.ltdmanager.module
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import org.intellij.lang.annotations.MagicConstant
|
||||||
import top.r3944realms.ltdmanager.GlobalManager
|
import top.r3944realms.ltdmanager.GlobalManager
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class DGLabModule(
|
||||||
val adminIds: List<Long> = listOf(),
|
val adminIds: List<Long> = listOf(),
|
||||||
val maxClientNumber: Int = 10,
|
val maxClientNumber: Int = 10,
|
||||||
val commandHead: List<String> = listOf("dglab"),
|
val commandHead: List<String> = listOf("dglab"),
|
||||||
) : BaseModule("DGLabModule", moduleName), PersistentState<DGLabModule.DgLabState> {
|
) : BaseModule(Modules.DG_LAB, moduleName), PersistentState<DGLabModule.DgLabState> {
|
||||||
|
|
||||||
var dgLabManager: DgLab? = null
|
var dgLabManager: DgLab? = null
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class GroupMessagePollingModule(
|
||||||
val targetGroupId: Long,
|
val targetGroupId: Long,
|
||||||
private val pollIntervalMillis: Long = 5_000L,
|
private val pollIntervalMillis: Long = 5_000L,
|
||||||
private val msgHistoryCheck: Int = 15,
|
private val msgHistoryCheck: Int = 15,
|
||||||
) : BaseModule("MessagePollingModule", moduleName) {
|
) : BaseModule(Modules.GROUP_MESSAGE_POLLING, moduleName) {
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
|
|
||||||
// 用 Flow 存消息,其他模块可以订阅
|
// 用 Flow 存消息,其他模块可以订阅
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class GroupRequestHandlerModule(
|
||||||
private val client: NapCatClient,
|
private val client: NapCatClient,
|
||||||
private val targetGroupId: Long,
|
private val targetGroupId: Long,
|
||||||
private val pollIntervalMillis: Long = 30_000L,
|
private val pollIntervalMillis: Long = 30_000L,
|
||||||
) : BaseModule("GroupRequestHandlerModule", moduleName) {
|
) : BaseModule(Modules.GROUP_REQUEST_HANDLER, moduleName) {
|
||||||
|
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class HelpModule(
|
||||||
private val selfNickName: String,
|
private val selfNickName: String,
|
||||||
private val keywords: List<String> = listOf("help", "帮助"),
|
private val keywords: List<String> = listOf("help", "帮助"),
|
||||||
private val cooldownMillis: Long = 30_000L
|
private val cooldownMillis: Long = 30_000L
|
||||||
) : BaseModule("HelpModule", moduleName), PersistentState<HelpModule.HelpState> {
|
) : BaseModule(Modules.HELP, moduleName), PersistentState<HelpModule.HelpState> {
|
||||||
|
|
||||||
// 命令解析器
|
// 命令解析器
|
||||||
private val commandParser = CommandParser(keywords)
|
private val commandParser = CommandParser(keywords)
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.r3944realms.ltdmanager.blessingskin.data.InvitationCode
|
||||||
import top.r3944realms.ltdmanager.blessingskin.request.invitecode.GenerateInvitationCodeRequest
|
import top.r3944realms.ltdmanager.blessingskin.request.invitecode.GenerateInvitationCodeRequest
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
import top.r3944realms.ltdmanager.core.mail.mail
|
import top.r3944realms.ltdmanager.core.mail.mail
|
||||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||||||
|
|
@ -77,7 +78,7 @@ class InvitationCodesModule(
|
||||||
selfId: Long,
|
selfId: Long,
|
||||||
private val cooldownMillis: Long = 120_000,
|
private val cooldownMillis: Long = 120_000,
|
||||||
private val keywords: Set<String> = setOf("申请邀请码")
|
private val keywords: Set<String> = setOf("申请邀请码")
|
||||||
) : BaseModule("InvitationCodesModule", moduleName), PersistentState<InvitationCodesModule.LastTriggerMapState> {
|
) : BaseModule(Modules.INVITATION_CODE, moduleName), PersistentState<InvitationCodesModule.LastTriggerMapState> {
|
||||||
|
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
private val stateFile: File = getStateFileInternal("invitation_codes_quarry_state.json", name)
|
private val stateFile: File = getStateFileInternal("invitation_codes_quarry_state.json", name)
|
||||||
|
|
@ -430,23 +431,33 @@ class InvitationCodesModule(
|
||||||
/**
|
/**
|
||||||
* 1. 创建邀请码
|
* 1. 创建邀请码
|
||||||
*/
|
*/
|
||||||
private suspend fun createInvitationCodes(amount: Int): List<InvitationCodeGenerationResponse.InvitationCode>? {
|
private suspend fun createInvitationCodes(amount: Int): List<InvitationCode>? {
|
||||||
return try {
|
return try {
|
||||||
val response = blessingSkinClient.submitRequest(
|
val response = blessingSkinClient.submitRequest(
|
||||||
GenerateInvitationCodeRequest(amount = amount, token = apiToken)
|
GenerateInvitationCodeRequest(amount = amount, token = apiToken)
|
||||||
)
|
)
|
||||||
|
response
|
||||||
|
.onFailure {
|
||||||
|
|
||||||
|
}
|
||||||
|
.onSuccess {
|
||||||
|
|
||||||
|
}
|
||||||
when (response) {
|
when (response) {
|
||||||
is ResponseResult.Success -> {
|
is ResponseResult.Success -> {
|
||||||
if (response.response.success) {
|
if (response.response is InvitationCodeGenerationResponse) {
|
||||||
response.response.data
|
if (response.response.success) {
|
||||||
|
response.response.data
|
||||||
|
} else
|
||||||
|
LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}")
|
||||||
|
null
|
||||||
} else {
|
} else {
|
||||||
LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}")
|
LoggerUtil.logger.warn("[$name] 返回非预期对象类型: ${response.response.javaClass}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ResponseResult.Failure -> {
|
is ResponseResult.Failure -> {
|
||||||
LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedResult}")
|
LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedMessage}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -460,7 +471,7 @@ class InvitationCodesModule(
|
||||||
* 2. 验证数量匹配
|
* 2. 验证数量匹配
|
||||||
*/
|
*/
|
||||||
private fun validateCodeCountMatch(
|
private fun validateCodeCountMatch(
|
||||||
invitationCodes: List<InvitationCodeGenerationResponse.InvitationCode>?,
|
invitationCodes: List<InvitationCode>?,
|
||||||
needNewTokenIdAndMsgPairs: List<Pair<Long, MsgHistorySpecificMsg>>
|
needNewTokenIdAndMsgPairs: List<Pair<Long, MsgHistorySpecificMsg>>
|
||||||
) {
|
) {
|
||||||
if (invitationCodes == null) {
|
if (invitationCodes == null) {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class MailModule(
|
||||||
private val enableAuth: Boolean = true,
|
private val enableAuth: Boolean = true,
|
||||||
private val enableTLS: Boolean = true,
|
private val enableTLS: Boolean = true,
|
||||||
private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s)
|
private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s)
|
||||||
) : BaseModule("MailModule", moduleName) {
|
) : BaseModule(Modules.MAIL, moduleName) {
|
||||||
|
|
||||||
private lateinit var session: Session
|
private lateinit var session: Session
|
||||||
private val queue = LinkedBlockingQueue<Mail>() // 邮件队列
|
private val queue = LinkedBlockingQueue<Mail>() // 邮件队列
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class McServerStatusModule(
|
||||||
setOf("hp", "hypixel") to "mc.hypixel.net",
|
setOf("hp", "hypixel") to "mc.hypixel.net",
|
||||||
setOf("pm", "mineplex") to "play.mineplex.com"
|
setOf("pm", "mineplex") to "play.mineplex.com"
|
||||||
)
|
)
|
||||||
) : BaseModule("McServerStatusModule", moduleName), PersistentState<McServerStatusModule.CooldownState> {
|
) : BaseModule(Modules.MC_SERVER_STATUS, moduleName), PersistentState<McServerStatusModule.CooldownState> {
|
||||||
private val stateFile:File = getStateFileInternal("mc_server_status_state.json", name)
|
private val stateFile:File = getStateFileInternal("mc_server_status_state.json", name)
|
||||||
private val stateBackupFile:File = getStateFileInternal("mc_server_status_state.json.bak", name)
|
private val stateBackupFile:File = getStateFileInternal("mc_server_status_state.json.bak", name)
|
||||||
private val commandParser: CommandParser = CommandParser(commands)
|
private val commandParser: CommandParser = CommandParser(commands)
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class ModGroupHandlerModule(
|
||||||
private val targetGroupId: Long,
|
private val targetGroupId: Long,
|
||||||
private val answers: List<String> = listOf("正确答案"),
|
private val answers: List<String> = listOf("正确答案"),
|
||||||
private val pollIntervalMillis: Long = 30_000L
|
private val pollIntervalMillis: Long = 30_000L
|
||||||
) : BaseModule("ModGroupHandlerModule", moduleName), PersistentState<ModGroupHandlerModule.RejectRecords> {
|
) : BaseModule(Modules.MOD_GROUP_HANDLER, moduleName), PersistentState<ModGroupHandlerModule.RejectRecords> {
|
||||||
|
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
private val stateFile: File = getStateFileInternal("reject_records.json", name)
|
private val stateFile: File = getStateFileInternal("reject_records.json", name)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,26 @@
|
||||||
package top.r3944realms.ltdmanager.module
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
object Modules {
|
object Modules {
|
||||||
|
private val MODULES: MutableList<String> = LinkedList();
|
||||||
|
val BAN: String = register("BanModule")
|
||||||
|
val APPLY_WHITELIST: String = register("ApplyWhitelistModule")
|
||||||
|
val DG_LAB: String = register("DGLabModule")
|
||||||
|
val GROUP_MESSAGE_POLLING: String = register("GroupMessagePollingModule")
|
||||||
|
val GROUP_REQUEST_HANDLER: String = register("GroupRequestHandlerModule")
|
||||||
|
val HELP: String = register("HelpModule")
|
||||||
|
val MAIL: String = register("MailModule")
|
||||||
|
val MC_SERVER_STATUS: String = register("MCServerStatusModule")
|
||||||
|
val MOD_GROUP_HANDLER: String = register("ModGroupHandlerModule")
|
||||||
|
val RCON_PLAYER_LIST: String = register("RconPlayerListModule")
|
||||||
|
val INVITATION_CODE: String = register("InvitationCodeModule")
|
||||||
|
val STATE: String = register("StateModule")
|
||||||
|
fun register(name: String): String {
|
||||||
|
MODULES.add(name)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
fun getModules(): Array<String> {
|
||||||
|
return MODULES.toTypedArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,7 +35,7 @@ class RconPlayerListModule(
|
||||||
private val rconPath: String,
|
private val rconPath: String,
|
||||||
private val rconConfigPath: String,
|
private val rconConfigPath: String,
|
||||||
private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家")
|
private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家")
|
||||||
) : BaseModule("RconPlayerListModule", moduleName), PersistentState<LastTriggerState> {
|
) : BaseModule(Modules.RCON_PLAYER_LIST, moduleName), PersistentState<LastTriggerState> {
|
||||||
private val cooldownManager by lazy {
|
private val cooldownManager by lazy {
|
||||||
CooldownManager(
|
CooldownManager(
|
||||||
cooldownMillis = cooldownMillis,
|
cooldownMillis = cooldownMillis,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ class StateModule(
|
||||||
moduleName: String,
|
moduleName: String,
|
||||||
private val onlineName: String,
|
private val onlineName: String,
|
||||||
private val offlineName: String,
|
private val offlineName: String,
|
||||||
): BaseModule("StateModule", moduleName) {
|
): BaseModule(Modules.STATE, moduleName) {
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
override fun onLoad() {
|
override fun onLoad() {
|
||||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
package top.r3944realms.ltdmanager.module.exception
|
package top.r3944realms.ltdmanager.module.exception
|
||||||
|
|
||||||
class InvalidConfigException: Exception() {
|
class ConfigError(type: Type = Type.OTHER, private val pos: String, vararg args: Any) : Exception() {
|
||||||
enum class Type(template: String) {
|
private val errorType: Type = type
|
||||||
|
private val arguments = args
|
||||||
|
|
||||||
|
override val message: String
|
||||||
|
get() = String.format(errorType.template, *arguments, pos)
|
||||||
|
|
||||||
|
|
||||||
|
enum class Type(val template: String) {
|
||||||
|
INVALID_PARAMETER("Invalid Parameter: %s in %s."),
|
||||||
|
MISSING_PARAMETER("Missing Parameter: %s in %s."),
|
||||||
|
NOT_EXPECTED_VALUE("Expect for %s but was %s in %s."),
|
||||||
|
OTHER("%s in %s")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ object ConfigInitializer {
|
||||||
* @param fileName YAML 文件名,如 application.yml
|
* @param fileName YAML 文件名,如 application.yml
|
||||||
* @param configDir 配置目录,如 config
|
* @param configDir 配置目录,如 config
|
||||||
*/
|
*/
|
||||||
fun initConfig(fileName: String = "application.yml", configDir: String = "config") {
|
fun initConfig(fileName: String = "application.yml", configDir: String = "config", shouldExit: Boolean = true) {
|
||||||
val dirPath = Paths.get(configDir)
|
val dirPath = Paths.get(configDir)
|
||||||
if (!Files.exists(dirPath)) {
|
if (!Files.exists(dirPath)) {
|
||||||
Files.createDirectories(dirPath)
|
Files.createDirectories(dirPath)
|
||||||
|
|
@ -28,8 +28,10 @@ object ConfigInitializer {
|
||||||
if (resourceStream != null) {
|
if (resourceStream != null) {
|
||||||
Files.copy(resourceStream, filePath, StandardCopyOption.REPLACE_EXISTING)
|
Files.copy(resourceStream, filePath, StandardCopyOption.REPLACE_EXISTING)
|
||||||
LoggerUtil.logger.info("已生成默认配置文件: $filePath")
|
LoggerUtil.logger.info("已生成默认配置文件: $filePath")
|
||||||
LoggerUtil.logger.info("第一次启动,请修改配置后再启动")
|
if (shouldExit) {
|
||||||
exitProcess(-1);
|
LoggerUtil.logger.info("第一次启动,请修改配置后再启动")
|
||||||
|
exitProcess(-1);
|
||||||
|
}
|
||||||
} else throw Error("Jar内部资源文件缺失")
|
} else throw Error("Jar内部资源文件缺失")
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ websocket:
|
||||||
url: "wss://127.0.0.1:3002"
|
url: "wss://127.0.0.1:3002"
|
||||||
# 格式为 ENC(XXX),若不是则会在加载完成配置后自动加密
|
# 格式为 ENC(XXX),若不是则会在加载完成配置后自动加密
|
||||||
encrypted-token: "123123cc"
|
encrypted-token: "123123cc"
|
||||||
|
mcsm:
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
rcon:
|
rcon:
|
||||||
mc-rcon-tool-path: "/path/to/rcon"
|
mc-rcon-tool-path: "/path/to/rcon"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user