diff --git a/.idea/misc.xml b/.idea/misc.xml index 3c8e914..8a923ed 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/build.gradle.kts b/build.gradle.kts index 5e3b662..79c6bda 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,6 +24,13 @@ repositories { maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } + maven { + url = uri("https://libraries.minecraft.net/") + } + // 第三方 repo,比如 MohistMC 或 GlareMasters Pub + maven { + url = uri("https://repo.glaremasters.me/repository/public/") + } } //TODO: 0872d1c0-829c-e1d7-6782-89e45c8a6b76 dependencies { @@ -37,6 +44,10 @@ repositories { implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3") // 推荐使用kotlinx.serialization替代Gson implementation("io.ktor:ktor-client-content-negotiation:2.3.12") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + // 如果需要日志拦截器(推荐用于调试) + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + // 数据库相关 implementation("org.jetbrains.exposed:exposed-core:0.41.1") implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1") @@ -71,6 +82,9 @@ repositories { //生成 二维码 implementation("com.google.zxing:core:[3.5.3,)") + //命令解析 + implementation("com.mojang:brigadier:1.2.9") + // 测试 testImplementation(kotlin("test")) testImplementation("io.ktor:ktor-client-mock:2.3.3") diff --git a/gradle.properties b/gradle.properties index 28b6dbe..74b81e4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,5 +3,5 @@ org.gradle.downloadSources=false org.gradle.parallel=true org.gradle.degree_of_parallelism=16 project_group=top.r3944realms.ltdmanager -project_version=1.10-SNAPSHOT -dg_lab_version=4.2.11.18 +project_version=1.14-SNAPSHOT +dg_lab_version=4.3.13.18 diff --git a/libs/DgLab-common-4.3.13.18.jar b/libs/DgLab-common-4.3.13.18.jar index bf86aa7..c6e4393 100644 Binary files a/libs/DgLab-common-4.3.13.18.jar and b/libs/DgLab-common-4.3.13.18.jar differ diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/GlobalManager.kt b/src/main/kotlin/top/r3944realms/ltdmanager/GlobalManager.kt index 5fb7fee..a05908f 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/GlobalManager.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/GlobalManager.kt @@ -2,6 +2,7 @@ package top.r3944realms.ltdmanager import kotlinx.coroutines.* import top.r3944realms.ltdmanager.blessingskin.BlessingSkinClient +import top.r3944realms.ltdmanager.chevereto.CheveretoClient import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool import top.r3944realms.ltdmanager.mcserver.McSrvStatusClient import top.r3944realms.ltdmanager.module.ModuleManager @@ -29,6 +30,9 @@ object GlobalManager { val blessingSkinClient: BlessingSkinClient by lazy { BlessingSkinClient.create() } + val cheveretoClient: CheveretoClient by lazy { + CheveretoClient.create() + } val moduleManager: ModuleManager by lazy { ModuleManager() } @@ -67,7 +71,8 @@ object GlobalManager { "NapCatClient" to { napCatClient.close() }, "McSrvStatusClient" to { mcSrvStatusClient.close() }, "BlessingSkinClient" to { blessingSkinClient.close() }, - "Hikari 数据源" to { dataSource.close() } + "Hikari 数据源" to { dataSource.close() }, + "CheveretoClient" to { cheveretoClient.close() } ) resources.forEach { (name, closer) -> diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinClient.kt b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinClient.kt index 65c002c..451b7cb 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinClient.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinClient.kt @@ -85,7 +85,7 @@ class BlessingSkinClient private constructor() : AutoCloseable { */ private suspend fun processQueueItem(item: BlessingSkinQueueItem) { semaphore.withPermit { - val (request, deferred, _, maxRetries, expectsResponse) = item + val (request, deferred, _, maxRetries, _) = item var attempt = 0 var lastError: Exception? = null diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoClient.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoClient.kt index c9bdd54..56eee15 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoClient.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoClient.kt @@ -6,6 +6,7 @@ import io.ktor.client.engine.cio.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.client.request.forms.* +import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.* @@ -13,95 +14,223 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withPermit -import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import top.r3944realms.ltdmanager.chevereto.data.CheveretoResponse +import top.r3944realms.ltdmanager.core.config.YamlConfigLoader import java.io.ByteArrayInputStream import java.io.Closeable import java.io.File import java.util.* +import kotlin.collections.ArrayDeque -object CheveretoUploader { +class CheveretoClient private constructor() : Closeable { private val client = HttpClient(CIO) { install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - }) + json(Json { ignoreUnknownKeys = true }) + } + } + private val imgTuConfig = YamlConfigLoader.loadTuImgConfig() + private val apiUrl = imgTuConfig.url!! + private val apiKey = imgTuConfig.decryptedPassword!! + // 限流,同时最多 3 个上传 + private val semaphore = Semaphore(3) + + // 普通队列 (按 priority 排序) + private val queue = PriorityQueue>(compareBy { it.priority }) + private val queueMutex = Mutex() + + // 紧急队列 (FIFO,最多 10 个) + private val urgentQueue = ArrayDeque>(10) + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + init { + scope.launch { + while (isActive) { + val item = queueMutex.withLock { + when { + urgentQueue.isNotEmpty() -> urgentQueue.removeFirst() + queue.isNotEmpty() -> queue.poll() + else -> null + } + } + if (item != null) processItem(item) + else delay(20) + } } } /** - * 上传本地文件 + * 上传 File */ suspend fun uploadFile( - apiUrl: String, - apiKey: String, file: File, title: String? = null, - description: String? = null + description: String? = null, + tags: String? = null, + albumId: String? = null, + categoryId: String? = null, + width: Int? = null, + expiration: String? = null, + nsfw: Int? = null, + format: String = "json", + useFileDate: Int? = null, + priority: Int = 5 ): CheveretoResponse { - return client.submitFormWithBinaryData( - url = apiUrl, - formData = formData { - append("source", file.readBytes(), Headers.build { - append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"${file.name}\"") - }) - append("format", "json") - title?.let { append("title", it) } - description?.let { append("description", it) } + val deferred = CompletableDeferred() + val source = suspend { + safeUpload { + submitFormWithBinaryData( + url = apiUrl, + formData = formData { + append("source", file.readBytes(), Headers.build { + append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"${file.name}\"") + }) + append("format", format) + title?.let { append("title", it) } + description?.let { append("description", it) } + tags?.let { append("tags", it) } + 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) + } } - ) { - headers { - append("X-API-Key", apiKey) + } + queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) } + return deferred.await() + } + + + /** + * 上传 ByteArrayInputStream + */ + suspend fun uploadStream( + inputStream: ByteArrayInputStream, + fileName: String, + title: String? = null, + description: String? = null, + tags: String? = null, + albumId: String? = null, + categoryId: String? = null, + width: Int? = null, + expiration: String? = null, + nsfw: Int? = null, + format: String = "json", + useFileDate: Int? = null, + priority: Int = 5 + ): CheveretoResponse { + val deferred = CompletableDeferred() + val source = suspend { + val bytes = inputStream.readBytes() + safeUpload { + submitFormWithBinaryData( + url = apiUrl, + formData = formData { + append("source", bytes, Headers.build { + append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"$fileName\"") + }) + append("format", format) + title?.let { append("title", it) } + description?.let { append("description", it) } + tags?.let { append("tags", it) } + 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) + } } - }.body() + } + queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) } + return deferred.await() } /** * 上传网络图片 URL */ - suspend fun uploadFromUrl( - apiUrl: String, - apiKey: String, - imageUrl: String + suspend fun uploadUrl( + url: String, + title: String? = null, + description: String? = null, + tags: String? = null, + albumId: String? = null, + categoryId: String? = null, + width: Int? = null, + expiration: String? = null, + nsfw: Int? = null, + format: String = "json", + useFileDate: Int? = null, + priority: Int = 5 ): CheveretoResponse { - return client.submitForm( - url = apiUrl, - formParameters = Parameters.build { - append("source", imageUrl) - append("format", "json") + val deferred = CompletableDeferred() + val source = suspend { + safeUpload { + submitForm( + url = apiUrl, + formParameters = Parameters.build { + append("source", url) + append("format", format) + title?.let { append("title", it) } + description?.let { append("description", it) } + tags?.let { append("tags", it) } + 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) + } } - ) { - headers { - append("X-API-Key", apiKey) + } + queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) } + return deferred.await() + } + + private suspend fun processItem(item: CheveretoQueueItem) { + semaphore.withPermit { + try { + val result = item.source() + item.deferred.complete(result) + } catch (e: Exception) { + item.deferred.completeExceptionally(e) } - }.body() + } } /** - * 上传 ByteArrayInputStream + * 包装上传,失败时打印原始响应 */ - suspend fun uploadFromStream( - apiUrl: String, - apiKey: String, - inputStream: ByteArrayInputStream, - fileName: String, - title: String? = null, - description: String? = null - ): CheveretoResponse { - val bytes = inputStream.readBytes() - return client.submitFormWithBinaryData( - url = apiUrl, - formData = formData { - append("source", bytes, Headers.build { - append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"$fileName\"") - }) - append("format", "json") - title?.let { append("title", it) } - description?.let { append("description", it) } - } - ) { - headers { append("X-API-Key", apiKey) } - }.body() + private suspend fun safeUpload(block: suspend HttpClient.() -> HttpResponse): CheveretoResponse { + val response = client.block() + return try { + response.body() + } catch (e: Exception) { + val raw = response.bodyAsText() + throw RuntimeException("Upload failed (status=${response.status}): $raw", e) + } + } + + + override fun close() { + scope.cancel() + runBlocking { client.close() } + } + + companion object { + fun create(): CheveretoClient = CheveretoClient() } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoQueueItem.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoQueueItem.kt index f3c2109..a4d4433 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoQueueItem.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoQueueItem.kt @@ -1,4 +1,9 @@ package top.r3944realms.ltdmanager.chevereto -class CheveretoQueueItem { -} \ No newline at end of file +import kotlinx.coroutines.CompletableDeferred + +data class CheveretoQueueItem( + val source: suspend () -> T, + val deferred: CompletableDeferred, + val priority: Int = 5 +) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoImage.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoImage.kt index a3b536e..482eaa1 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoImage.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoImage.kt @@ -1,5 +1,6 @@ -package top.r3944realms.ltdmanager.chevereto +package top.r3944realms.ltdmanager.chevereto.data +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable @@ -10,5 +11,98 @@ data class CheveretoImage( val width: Int, val height: Int, val date: String, - val url: String -) \ No newline at end of file + @SerialName("date_gmt") + val dateGmt: String, + val title: String, + val tags: List? = emptyList(), + val description: String? = null, + val nsfw: Int, + @SerialName("storage_mode") + val storageMode: String, + val md5: String, + @SerialName("source_md5") + val sourceMd5: String? = null, + @SerialName("original_filename") + val originalFilename: String, + @SerialName("original_exifdata") + val originalExifdata: String? = null, + val views: Int, + @SerialName("category_id") + val categoryId: String? = null, + val chain: Int, + @SerialName("thumb_size") + val thumbSize: Int, + @SerialName("medium_size") + val mediumSize: Int, + @SerialName("frame_size") + val frameSize: Int? = null, + @SerialName("expiration_date_gmt") + val expirationDateGmt: String? = null, + val likes: Int, + @SerialName("is_animated") + val isAnimated: Int, + @SerialName("is_approved") + val isApproved: Int, + @SerialName("is_360") + val is360: Int, + val duration: Int? = null, + val type: String? = null, + @SerialName("tags_string") + val tagsString: String? = null, + val file: File? = null, + @SerialName("id_encoded") + val idEncoded: String, + val filename: String, + val mime: String, + val url: String, + val ratio: Double? = null, + @SerialName("size_formatted") + val sizeFormatted: String, + val frame: ImageThumb? = null, + val image: ImageFile, + val thumb: ImageThumb, + @SerialName("url_frame") + val urlFrame: String? = null, + val medium: Medium? = null, + @SerialName("duration_time") + val durationTime: String? = null, + @SerialName("url_viewer") + val urlViewer: String, + @SerialName("path_viewer") + val pathViewer: String? = null, + @SerialName("url_short") + val urlShort: String, + @SerialName("display_url") + val displayUrl: String, + @SerialName("display_width") + val displayWidth: Int, + @SerialName("display_height") + val displayHeight: Int, + @SerialName("views_label") + val viewsLabel: String, + @SerialName("likes_label") + val likesLabel: String, + @SerialName("how_long_ago") + val howLongAgo: String, + @SerialName("date_fixed_peer") + val dateFixedPeer: String, + @SerialName("title_truncated") + val titleTruncated: String, + @SerialName("title_truncated_html") + val titleTruncatedHtml: String, + @SerialName("is_use_loader") + val isUseLoader: Boolean, + @SerialName("display_title") + val displayTitle: String? = null, + @SerialName("delete_url") + val deleteUrl: String +) + + + + + + + + + diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoResponse.kt index 83395b0..1124a07 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoResponse.kt @@ -1,4 +1,4 @@ -package top.r3944realms.ltdmanager.chevereto +package top.r3944realms.ltdmanager.chevereto.data import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -7,6 +7,8 @@ import kotlinx.serialization.Serializable data class CheveretoResponse( @SerialName("status_code") val statusCode: Int, - val success: Map? = null, - val image: CheveretoImage? = null + val success: Success? = null, + val image: CheveretoImage? = null, + @SerialName("status_txt") + val statusTxt:String ?= null ) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/File.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/File.kt index 04c9fb9..474a386 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/File.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/File.kt @@ -1,4 +1,13 @@ package top.r3944realms.ltdmanager.chevereto.data -class File { +import kotlinx.serialization.Serializable + +@Serializable +data class File( + val resource: Resource +) { + @Serializable + data class Resource( + val type: String + ) } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/ImageFile.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/ImageFile.kt index ea0a283..a77ad7e 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/ImageFile.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/ImageFile.kt @@ -1,4 +1,13 @@ package top.r3944realms.ltdmanager.chevereto.data -class ImageFile { -} \ No newline at end of file +import kotlinx.serialization.Serializable + +@Serializable +data class ImageFile( + val filename: String, + val name: String, + val mime: String, + val extension: String, + val url: String, + val size: Long +) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/ImageThumb.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/ImageThumb.kt index 91a6949..ab48580 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/ImageThumb.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/ImageThumb.kt @@ -1,3 +1,13 @@ package top.r3944realms.ltdmanager.chevereto.data -data class ImageThumb() +import kotlinx.serialization.Serializable + +@Serializable +data class ImageThumb( + val filename: String, + val name: String, + val mime: String, + val extension: String, + val url: String, + val size: Int +) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/Medium.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/Medium.kt index b0a10a4..ef7d659 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/Medium.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/Medium.kt @@ -1,4 +1,12 @@ package top.r3944realms.ltdmanager.chevereto.data -class Medium { -} \ No newline at end of file +import kotlinx.serialization.Serializable + +@Serializable +data class Medium( + val filename: String? = null, + val name: String? = null, + val mime: String? = null, + val extension: String? = null, + val url: String? = null +) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/Success.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/Success.kt index 56d1c7f..f7c7d6a 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/Success.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/Success.kt @@ -1,3 +1,9 @@ package top.r3944realms.ltdmanager.chevereto.data -data class Success() +import kotlinx.serialization.Serializable + +@Serializable +data class Success( + val message : String? = null, + val code: Int? = 200, +) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/ImgTuConfig.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/ImgTuConfig.kt index 5aa23cc..0b72651 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/ImgTuConfig.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/ImgTuConfig.kt @@ -1,4 +1,59 @@ package top.r3944realms.ltdmanager.core.config -class ImgTuConfig { +import top.r3944realms.ltdmanager.utils.CryptoUtil +import top.r3944realms.ltdmanager.utils.YamlUpdater + +data class ImgTuConfig( + var url: String? = null, + var encryptedPassword: String? = null +) { + /** + * 获取解密后的Password(如果未加密,返回原值) + */ + val decryptedPassword: String? + get() { + if (encryptedPassword == null) { + return null + } + if (!isEncrypted()) { + return encryptedPassword + } + try { + val cipherText = encryptedPassword!!.substring(4, encryptedPassword!!.length - 1) + return CryptoUtil.decrypt(cipherText) + } catch (e: Exception) { + throw IllegalStateException("Password解密失败", e) + } + } + + /** + * 加密密码(如果未加密),并返回是否成功加密 + */ + fun encryptPassword() { + if (encryptedPassword == null || isEncrypted()) { + return + } + try { + encryptedPassword = "ENC(${CryptoUtil.encrypt(encryptedPassword!!)})" + YamlUpdater.updateYaml( + YamlConfigLoader.configFilePath.toString(), + "img-tu.encrypted-password", + this.encryptedPassword!! + ) + } catch (e: Exception) { + throw IllegalStateException("密码加密失败", e) + } + } + + /** + * 检查Password是否已加密 + */ + private fun isEncrypted(): Boolean { + return encryptedPassword != null && + encryptedPassword!!.startsWith("ENC(") && + encryptedPassword!!.endsWith(")") + } + override fun toString(): String { + return "ImgTuConfig(url=$url, Password=***)" + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/YamlConfigLoader.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/YamlConfigLoader.kt index 306ce2a..7fbe554 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/YamlConfigLoader.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/YamlConfigLoader.kt @@ -35,6 +35,7 @@ object YamlConfigLoader { config?.tools?.rcon?.encryptPassword() config?.blessingSkinServer?.invitationApi?.encryptToken() config?.dgLab?.wsServer?.encryptPassword() + config?.imgTu?.encryptPassword() } private fun loadConfig(): ConfigWrapper { if (!Files.exists(configFilePath)) { @@ -78,6 +79,7 @@ object YamlConfigLoader { fun loadMailConfig(): MailConfig = config.mail fun loadBlessingSkinServerConfig(): BlessingSkinServerConfig = config.blessingSkinServer fun loadDgLabConfig(): DgLabConfig = config.dgLab + fun loadTuImgConfig(): ImgTuConfig = config.imgTu data class ConfigWrapper( var database: DatabaseConfig = DatabaseConfig(), var crypto: CryptoConfig = CryptoConfig(), @@ -88,6 +90,7 @@ object YamlConfigLoader { var mail: MailConfig = MailConfig(), var blessingSkinServer: BlessingSkinServerConfig = BlessingSkinServerConfig(), var dgLab: DgLabConfig = DgLabConfig(), + var imgTu: ImgTuConfig = ImgTuConfig(), ) } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/DgLab.kt b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/DgLab.kt index 81147d5..d436c76 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/DgLab.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/DgLab.kt @@ -1,5 +1,6 @@ -package top.r3944realms.ltdmanager.dglab.manager +package top.r3944realms.ltdmanager.dglab +import com.r3944realms.dg_lab.api.manager.Status import com.r3944realms.dg_lab.api.operation.ClientOperation import com.r3944realms.dg_lab.api.operation.ServerOperation import com.r3944realms.dg_lab.api.websocket.message.role.WebSocketClientRole @@ -11,25 +12,66 @@ import com.r3944realms.dg_lab.websocket.PowerBoxWSServer import com.r3944realms.dg_lab.websocket.sharedData.ClientPowerBoxSharedData import com.r3944realms.dg_lab.websocket.sharedData.ServerPowerBoxSharedData import top.r3944realms.ltdmanager.core.config.YamlConfigLoader +import top.r3944realms.ltdmanager.dglab.manager.ClientManager +import top.r3944realms.ltdmanager.dglab.manager.ServerManager +import top.r3944realms.ltdmanager.dglab.model.game.Player +import top.r3944realms.ltdmanager.dglab.model.game.PlayerManager import kotlin.io.path.Path /** - * 全局DG_Lab单例管理器 + * DG_Lab管理器 */ -object DgLabManager { +class DgLab { // 可空,延迟初始化 - var serverManager: ServerManager? = null - private set + internal var serverManager: ServerManager? = null + get() = field - var clientManager: ClientManager? = null - private set + internal var clientManager: ClientManager? = null + get() = field + private var playerManager: PlayerManager? = null + companion object { + const val SERVER_ROLE_NAME = "Se-IC" + } + fun isSeverOnline(): Boolean = serverManager?.let { it.status == Status.RUNNING } ?: false + + fun isClientOnline(id: String): Boolean = clientManager?.getClient(id)?.let { it.status == Status.RUNNING } ?: false + + fun getPlayerManager(): PlayerManager = playerManager!! + + fun close() { + serverManager?.stop() + clientManager?.stopAll() + } + + fun initOrLoadPlayerManager(idNameMap: Map) { + playerManager = PlayerManager(1) + val idList = idNameMap.map { id -> id.key } + val existingIds = playerManager?.allPlayers()?.map { it.id }?.toSet() ?: emptySet() + val targetIds = idList.toSet() + + // 要删除的 + val toRemove = existingIds - targetIds + // 要新增的 + val toAdd = targetIds - existingIds + + // 删除 + toRemove.forEach { id -> + playerManager?.removePlayer(id) + } + + // 新增 + toAdd.forEach { id -> + playerManager?.addPlayer(Player(id, idNameMap[id] as String,false)) + } + } fun createServerManager(operation: ServerOperation): DGPBServerManager { val loadDgLabConfig = YamlConfigLoader.loadDgLabConfig() + val boxWSServer = PowerBoxWSServer.Builder.getBuilder() .port(loadDgLabConfig.wsServer.localServerPort) - .role(WebSocketServerRole("Se-IC")) + .role(WebSocketServerRole(SERVER_ROLE_NAME)) .operation(operation) .sharedData(ServerPowerBoxSharedData()) .build() @@ -66,7 +108,13 @@ object DgLabManager { fun removeClient(key: String) { clientManager?.removeClient(key) } - + /** + * 获取 服务器管理类 + */ + @Throws(IllegalStateException::class) + fun getServer(): DGPBServerManager { + return serverManager?.getInstance() ?: throw IllegalStateException("Server is not initialized") + } /** * 获取 客户端管理类 */ @@ -86,6 +134,7 @@ object DgLabManager { .role(WebSocketClientRole("QQ-$key")) .operation(operation) .sharedData(ClientPowerBoxSharedData()) + .useRoleMsgMode(true) .build() if (loadDgLabConfig.wsServer.localServerSecure) { diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameClientOperation.kt b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameClientOperation.kt index 1d5cc11..8da26f1 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameClientOperation.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameClientOperation.kt @@ -2,60 +2,219 @@ package top.r3944realms.ltdmanager.dglab.model.game import com.r3944realms.dg_lab.api.operation.ClientOperation import com.r3944realms.dg_lab.api.websocket.message.data.PowerBoxData +import com.r3944realms.dg_lab.api.websocket.message.data.type.PowerBoxDataType +import com.r3944realms.dg_lab.manager.DGPBClientManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import top.r3944realms.ltdmanager.GlobalManager +import top.r3944realms.ltdmanager.core.config.YamlConfigLoader +import top.r3944realms.ltdmanager.napcat.NapCatClient +import top.r3944realms.ltdmanager.napcat.data.ID +import top.r3944realms.ltdmanager.napcat.data.MessageElement +import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest +import top.r3944realms.ltdmanager.napcat.request.other.SendPrivateMsgRequest +import top.r3944realms.ltdmanager.utils.LoggerUtil +import top.r3944realms.ltdmanager.utils.QRCodeUtil +import java.io.ByteArrayInputStream + class GameClientOperation( - val player: Player + val napCatClient: NapCatClient, + val groupId: Long, + val playerManager: PlayerManager, + private val playerId: Long ) : ClientOperation { - + private val scope = CoroutineScope(Dispatchers.IO) + private var qrcode:ByteArrayInputStream? = null; + var clientSelf: DGPBClientManager? = null + private var hasBinding = false + private var bindingTimeoutJob: kotlinx.coroutines.Job? = null // 保存倒计时任务 override fun ClientStartingHandler() { - println("Player ${player.id} is starting the client...") + LoggerUtil.logger.debug("Player $playerId is starting the client...") + scope.launch { + napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端启动中...")), ID.long(playerId))) + } } override fun ClientStartedHandler() { - println("Player ${player.id} client started successfully.") + LoggerUtil.logger.debug("Player $playerId client started successfully.") + scope.launch { + napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端启动完成!")), ID.long(playerId))) + } + playerManager.getPlayer(playerId)?.active = true } - override fun ClientStartingErrorHandler() { - println("Player ${player.id} failed to start client!") + override fun ClientStartingErrorHandler(errMsg: String) { + LoggerUtil.logger.debug("Player $playerId failed to start client! Reason: $errMsg") + scope.launch { + napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端启动中遇到错误:$errMsg!")), ID.long(playerId))) + } + playerManager.getPlayer(playerId)?.active = false } override fun ClientStoppingHandler() { - println("Player ${player.id} is stopping the client...") + LoggerUtil.logger.debug("Player $playerId is stopping the client...") + scope.launch { + napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端关闭中...")), ID.long(playerId))) + } + playerManager.getPlayer(playerId)?.active = false } - override fun ClientStoppingErrorHandler() { - println("Player ${player.id} encountered an error while stopping.") + override fun ClientStoppingErrorHandler(errMsg: String) { + LoggerUtil.logger.debug("Player $playerId encountered an error while stopping. Reason: $errMsg") + scope.launch { + napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端关闭中遇到错误:$errMsg!")), ID.long(playerId))) + } + playerManager.getPlayer(playerId)?.active = false } override fun ClientStoppedHandler() { - println("Player ${player.id} client stopped.") + LoggerUtil.logger.debug("Player $playerId client stopped.") + scope.launch { + napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端成功关闭!")), ID.long(playerId))) + } + bindingTimeoutJob?.cancel() + playerManager.getPlayer(playerId)?.active = false } override fun QrCodeUrlHandler(p0: String?) { - println("Player ${player.id} QR code received: $p0") - } + LoggerUtil.logger.debug("Player $playerId QR code received: $p0") + if (p0.isNullOrBlank()) { + LoggerUtil.logger.warn("二维码 URL 为空,无法生成") + return + } + // 处理 URL,将 IP 和端口替换为配置文件中的服务器 URL + val processedUrl = processQrCodeUrl(p0) + + // 生成二维码文件 + qrcode = QRCodeUtil.generateQRCode(processedUrl, 300, 300) + + } + /** + * 处理二维码 URL,将整个连接地址替换为配置文件中的服务器 URL + */ + private fun processQrCodeUrl(originalUrl: String): String { + return try { + val configUrl = YamlConfigLoader.loadDgLabConfig().wsServer.localServerPublishUrl + + // 使用正则表达式匹配整个 ws:// 或 wss:// 开头的 URL + val pattern = Regex("""wss?://[^:/]+(?::\d+)?(/.*)?""") + + pattern.replace(originalUrl) { matchResult -> + // 保留原始 URL 中的路径部分(如果有的话) + val path = matchResult.groupValues[1] ?: "" + "$configUrl$path" + } + } catch (e: Exception) { + LoggerUtil.logger.error("处理二维码 URL 时出错: ${e.message}", e) + originalUrl // 如果处理失败,返回原 URL + } + } override fun ShowQrCodeHandler() { - println("Player ${player.id} should display QR code.") - } + LoggerUtil.logger.debug("Display QRCode to $playerId.") + if (qrcode == null) { + LoggerUtil.logger.warn("没有可用的二维码路径") + return + } + + scope.launch { + // 上传二维码图片 + val response = GlobalManager.cheveretoClient.uploadStream( + qrcode!!, + "$playerId-Qrcode-${System.currentTimeMillis()}.png", + "Qrcode-$playerId-${System.currentTimeMillis()}", + "5min后将会自动删除", + albumId = "BFx", + expiration = "PT5M" + ) + if (response.image?.url != null) { + // 发送图床 URL 给玩家 + napCatClient.sendUnit( + SendPrivateMsgRequest( + listOf( + MessageElement.text("请在60s内绑定APP,否则将自动断开连接"), + MessageElement.image(response.image.url, "二维码") + ), + ID.long(playerId) + ) + ) + } else { + LoggerUtil.logger.error("上传二维码返回 JSON 未包含 URL") + } + // 启动 60 秒倒计时任务 + bindingTimeoutJob = launch { + kotlinx.coroutines.delay(60_000) + val player = playerManager.getPlayer(playerId) + if (player != null && !hasBinding) { + LoggerUtil.logger.warn("Player $playerId 在 60 秒内未绑定,正在停止客户端") + napCatClient.sendUnit( + SendPrivateMsgRequest( + listOf( + MessageElement.text("请在60s内未绑定APP,准备停止客户端"), + ), + ID.long(playerId) + ) + ) + try { + clientSelf?.stop() + } catch (e: Exception) { + LoggerUtil.logger.error("停止客户端失败: ", e) + } finally { + player.active = false + } + } + } + } + } override fun ConnectSuccessfulNoticeHandler() { - println("Player ${player.id} connected successfully.") + LoggerUtil.logger.debug("Player $playerId connected successfully.") + bindingTimeoutJob?.cancel() + bindingTimeoutJob = null + val player = playerManager.getPlayer(playerId) + player?.active = true + scope.launch { + napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("恭喜,绑定成功")), ID.long(playerId))) + napCatClient.sendUnit(SendGroupMsgRequest(listOf(MessageElement.text("$playerId 加入战局")), ID.long(groupId))) + } } override fun DisconnectHandler(p0: PowerBoxData?) { - println("Player ${player.id} disconnected: $p0") + LoggerUtil.logger.debug("Player {} disconnected: {}", playerId, p0) + scope.launch { + napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("连接断开, $p0")), ID.long(playerId))) + napCatClient.sendUnit(SendGroupMsgRequest(listOf(MessageElement.text("$playerId 离开战局")), ID.long(groupId))) + } } override fun ErrorHandler(p0: PowerBoxData?) { - println("Player ${player.id} error occurred: $p0") + LoggerUtil.logger.debug("Player {} error occurred: {}", playerId, p0) + scope.launch { + if(p0 != null && p0.message.isNotEmpty()) + napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("遇到错误, $p0")), ID.long(playerId))) + } } override fun HeartBeatHandler(p0: PowerBoxData?) { - println("Heartbeat from player ${player.id}: $p0") +// LoggerUtil.logger.debug("Heartbeat from player {}: {}", playerId, p0) +// scope.launch { +// napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("连接断开, $p0")), ID.long(playerId))) +// } } override fun OtherMessageHandler(p0: PowerBoxData?) { - println("Other message for player ${player.id}: $p0") +// LoggerUtil.logger.debug("Other message for player {}: {}", playerId, p0) +// scope.launch { +// napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("连接断开, $p0")), ID.long(playerId))) +// } + when (p0?.commandType) { + PowerBoxDataType.STRENGTH -> TODO() + PowerBoxDataType.PULSE -> TODO() + PowerBoxDataType.CLEAR -> TODO() + PowerBoxDataType.FEEDBACK -> TODO() + else -> return + } } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameServerOperation.kt b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameServerOperation.kt index e5a46ac..c3efe68 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameServerOperation.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameServerOperation.kt @@ -1,5 +1,84 @@ package top.r3944realms.ltdmanager.dglab.model.game +import com.r3944realms.dg_lab.api.websocket.message.PowerBoxMessage +import com.r3944realms.dg_lab.api.websocket.message.role.PlaceholderRole +import com.r3944realms.dg_lab.api.websocket.message.role.WebSocketServerRole import com.r3944realms.dg_lab.websocket.handler.server.DefaultServerOperation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import top.r3944realms.ltdmanager.dglab.DgLab +import top.r3944realms.ltdmanager.dglab.manager.ServerManager +import top.r3944realms.ltdmanager.napcat.NapCatClient +import top.r3944realms.ltdmanager.napcat.data.ID +import top.r3944realms.ltdmanager.napcat.data.MessageElement +import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest -class GameServerOperation : DefaultServerOperation() \ No newline at end of file +class GameServerOperation(private val msgClient: NapCatClient, val groupId: Long) : DefaultServerOperation() { + private val scope = CoroutineScope(Dispatchers.IO) + var serverManager: ServerManager? = null + override fun ServerStartingHandler() { + scope.launch { + msgClient.sendUnit( + SendGroupMsgRequest(listOf(MessageElement.text("服务器启动中...")), ID.long(groupId)) + ) + } + } + + override fun ServerStartedHandler() { + scope.launch { + msgClient.sendUnit( + SendGroupMsgRequest(listOf(MessageElement.text("服务器已启动")), ID.long(groupId)) + ) + } + } + override fun ServerStoppingHandler() { + scope.launch { + msgClient.sendUnit( + SendGroupMsgRequest(listOf(MessageElement.text("服务器关闭中...")), ID.long(groupId)) + ) + } + } + + override fun ServerStoppedHandler() { + scope.launch { + msgClient.sendUnit( + SendGroupMsgRequest(listOf(MessageElement.text("服务器已关闭")), ID.long(groupId)) + ) + } + } + + override fun ServerStoppingErrorHandler(errMsg: String) { + scope.launch { + msgClient.sendUnit( + SendGroupMsgRequest(listOf(MessageElement.text("服务器关闭过程中遇到错误: $errMsg")), ID.long(groupId)) + ) + } + } + + override fun ServerStartingErrorHandler(errMsg: String?) { + scope.launch { + msgClient.sendUnit( + SendGroupMsgRequest(listOf(MessageElement.text("服务器开启过程中遇到错误: $errMsg")), ID.long(groupId)) + ) + } + } + + override fun ClientSessionBuildInHandler(clientId: String?) { + scope.launch{ + delay(1000) + serverManager?.getInstance()?.send( + clientId, + PowerBoxMessage.createPowerBoxMessage( + "bind", + clientId, + "", + "", + WebSocketServerRole(DgLab.SERVER_ROLE_NAME), + PlaceholderRole("Temp-$clientId") + ) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/Player.kt b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/Player.kt index 7defc55..92e3fc3 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/Player.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/Player.kt @@ -1,8 +1,13 @@ package top.r3944realms.ltdmanager.dglab.model.game +import kotlinx.serialization.Serializable + /** - * 玩家类,目前仅包含一个 ID + * 玩家类 */ +@Serializable data class Player( - val id: String + val id: Long, + var name: String, + var active: Boolean, ) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/PlayerManager.kt b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/PlayerManager.kt index b98ac82..b00eaf6 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/PlayerManager.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/PlayerManager.kt @@ -1,4 +1,73 @@ package top.r3944realms.ltdmanager.dglab.model.game -class PlayerManager { +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import top.r3944realms.ltdmanager.module.PersistentState +import top.r3944realms.ltdmanager.utils.LoggerUtil +import java.io.File +import java.util.concurrent.ConcurrentHashMap + +class PlayerManager(id: Long): PersistentState { + @Contextual + private val map = ConcurrentHashMap() + @Transient + private val stateFile: File = getStateFileInternal("dglab_player_data.json", "dglab$id") + @Transient + private val stateBackupFile: File = getStateFileInternal("dglab_player_data.json.bak","dglab$id") + override fun getStateFileInternal(): File = stateFile + + private var playerState = loadState() + @Serializable + data class PlayerState( + val map: Map = emptyMap() + ) + + override fun getState(): PlayerState = playerState + /** 添加或更新玩家 */ + fun addPlayer(player: Player) { + map[player.id] = player + } + + /** 根据 ID 获取玩家 */ + fun getPlayer(id: Long): Player? = map[id] + + /** 删除玩家 */ + fun removePlayer(id: Long): Player? = map.remove(id) + + /** 判断是否存在玩家 */ + fun contains(id: Long): Boolean = map.containsKey(id) + + /** 获取所有玩家 */ + fun allPlayers(): List = map.values.toList() + + /** 获取所有在线玩家的数量 */ + fun getOnlinePlayerSize(): Int = map.values.filter { it.active }.size + + + override fun saveState(state: PlayerState) { + try { + if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true) + stateFile.writeText(Json.encodeToString(state)) + } catch (e: Exception) { + LoggerUtil.logger.error("[dglab] 保存玩家数据&状态失败", e) + } + } + + override fun loadState(): PlayerState { + return try { + val fileToRead = when { + stateFile.exists() -> stateFile + stateBackupFile.exists() -> stateBackupFile + else -> null + } ?: return PlayerState() + + Json.decodeFromString(fileToRead.readText()) + } catch (e: Exception) { + LoggerUtil.logger.warn("[dglab] 读取玩家数据&状态失败", e) + PlayerState() + } + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/pulseware/CustomPulseDataConverter.kt b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/pulseware/CustomPulseDataConverter.kt index 39f0dd8..de0c80a 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/pulseware/CustomPulseDataConverter.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/pulseware/CustomPulseDataConverter.kt @@ -3,14 +3,46 @@ package top.r3944realms.ltdmanager.dglab.model.pulseware import com.r3944realms.dg_lab.api.message.data.PulseWave import com.r3944realms.dg_lab.api.message.data.PulseWaveList - object CustomPulseDataConverter { + + /** + * 将频率转换为 Dg-Lab 格式 + * + * @param frequency 频率值 + * @return Dg-Lab 格式的数字 + */ + private fun convertFrequency(frequency: Int): Int { + return when { + frequency <= 10 -> 10 + frequency <= 100 -> frequency + frequency <= 600 -> (frequency - 100) / 5 + 100 + frequency <= 1000 -> (frequency - 600) / 10 + 200 + else -> 10 + } + } + + /** + * 将频率数组转换为 Dg-Lab 格式 + * + * @param frequencies 频率数组 + * @return 转换后的频率数组 + */ + private fun convertFrequencies(frequencies: IntArray): IntArray { + return IntArray(4) { index -> + if (index < frequencies.size) { + convertFrequency(frequencies[index]) + } else { + 10 // 默认值 + } + } + } + /** * 将自定义波形数据转换为 PulseWaveList * - * @param customPulseData Map, List[][]>> + * @param customPulseData Map>> * 每个 int[][] 包含两个长度为 4 的 int 数组,第一个是 frequencies,第二个是 strengths - * @return Map, PulseWaveList> + * @return Map */ fun convert(customPulseData: Map>>): Map { val pulseWaveLists: MutableMap = HashMap() @@ -26,7 +58,10 @@ object CustomPulseDataConverter { // 确保每个数组长度为4 require(!(freqs.size != 4 || strengths.size != 4)) { "每个波形段必须包含 4 个频率和 4 个强度值" } - val wave = PulseWave.fromArrays(freqs, strengths) + // 转换频率为 Dg-Lab 格式 + val convertedFreqs = convertFrequencies(freqs) + + val wave = PulseWave.fromArrays(convertedFreqs, strengths) waveList.add(wave) } @@ -35,12 +70,53 @@ object CustomPulseDataConverter { return pulseWaveLists } - fun PulseWave.toSerializable(): PulseWaveSerializable = - PulseWaveSerializable(f1(), f2(), f3(), f4(), s1(), s2(), s3(), s4()) - fun PulseWaveSerializable.toPulseWave(): PulseWave = + /** + * 转换单个 PulseWave 的频率 + */ + private fun convertPulseWaveFrequencies(pulseWave: PulseWave): PulseWave { + val freqs = intArrayOf( + convertFrequency(pulseWave.f1()), + convertFrequency(pulseWave.f2()), + convertFrequency(pulseWave.f3()), + convertFrequency(pulseWave.f4()) + ) + val strengths = intArrayOf( + pulseWave.s1(), + pulseWave.s2(), + pulseWave.s3(), + pulseWave.s4() + ) + return PulseWave.fromArrays(freqs, strengths) + } + + /** + * 转换整个 PulseWaveList 的频率 + */ + fun convertPulseWaveListFrequencies(pulseWaveList: PulseWaveList): PulseWaveList { + val convertedList = PulseWaveList() + convertedList.name = pulseWaveList.name + + for (i in 0 until pulseWaveList.list.size) { + val convertedWave = convertPulseWaveFrequencies(pulseWaveList.list[i]) + convertedList.add(convertedWave) + } + + return convertedList + } + + fun PulseWave.toSerializable(): PulseWaveSerializable = + PulseWaveSerializable( + convertFrequency(f1()), + convertFrequency(f2()), + convertFrequency(f3()), + convertFrequency(f4()), + s1(), s2(), s3(), s4() + ) + + private fun PulseWaveSerializable.toPulseWave(): PulseWave = PulseWave.fromArrays( - intArrayOf(f1, f2, f3, f4), + intArrayOf(convertFrequency(f1), convertFrequency(f2), convertFrequency(f3), convertFrequency(f4)), intArrayOf(s1, s2, s3, s4) ) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/pulseware/DefaultPulseData.kt b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/pulseware/DefaultPulseData.kt index c4266a9..3e9909c 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/pulseware/DefaultPulseData.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/pulseware/DefaultPulseData.kt @@ -4,7 +4,39 @@ import com.r3944realms.dg_lab.api.message.data.PulseWave import com.r3944realms.dg_lab.api.message.data.PulseWaveList object DefaultPulseData { + /** + * 将频率转换为 Dg-Lab 格式 + * + * @param frequency 频率值 + * @return Dg-Lab 格式的数字 + */ + private fun convertFrequency(frequency: Int): Int { + return when { + frequency <= 10 -> 10 + frequency <= 100 -> frequency + frequency <= 600 -> (frequency - 100) / 5 + 100 + frequency <= 1000 -> (frequency - 600) / 10 + 200 + else -> 10 + } + } + /** + * 转换频率数组为 Dg-Lab 格式 + */ + private fun convertFrequencies(frequencies: IntArray): IntArray { + return IntArray(frequencies.size) { index -> + convertFrequency(frequencies[index]) + } + } + + /** + * 创建经过频率转换的波形段 + */ + private fun createWaveSegment(frequencies: IntArray, strengths: IntArray): PulseWave { + val convertedFreqs = convertFrequencies(frequencies) + return PulseWave.fromArrays(convertedFreqs, strengths) + } + fun allPulseWaveLists(): Map { return mapOf( "呼吸" to Breath, @@ -47,7 +79,7 @@ object DefaultPulseData { // 转成 PulseWave 并加入列表 for (seg in segments) { - list.add(PulseWave.fromArrays(seg[0], seg[1])) + list.add(createWaveSegment(seg[0], seg[1])) } list @@ -68,7 +100,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(84, 82, 80, 76)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(68, 68, 68, 68)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } @@ -85,7 +117,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 1)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(2, 2, 2, 2)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } val FastPinch: PulseWaveList by lazy { @@ -96,7 +128,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)), arrayOf(intArrayOf(0, 0, 0, 0), intArrayOf(0, 0, 0, 0)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } val PinchGradual: PulseWaveList by lazy { @@ -115,7 +147,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } @@ -140,7 +172,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } val Compress: PulseWaveList by lazy { @@ -169,7 +201,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } val RhythmStep: PulseWaveList by lazy { @@ -203,7 +235,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } @@ -216,7 +248,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } @@ -231,7 +263,7 @@ object DefaultPulseData { arrayOf(intArrayOf(0, 0, 0, 0), intArrayOf(0, 0, 0, 0)), arrayOf(intArrayOf(0, 0, 0, 0), intArrayOf(0, 0, 0, 0)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } val WaveRipple: PulseWaveList by lazy { @@ -246,7 +278,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(50, 50, 50, 50)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } @@ -259,7 +291,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(80, 90, 100, 100)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } @@ -273,7 +305,7 @@ object DefaultPulseData { arrayOf(intArrayOf(20, 20, 20, 20), intArrayOf(50, 50, 50, 50)), arrayOf(intArrayOf(15, 15, 15, 15), intArrayOf(0, 0, 0, 0)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } val SignalLight: PulseWaveList by lazy { @@ -285,7 +317,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } @@ -296,7 +328,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 30, 60, 100)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 70, 40, 0)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } @@ -307,7 +339,7 @@ object DefaultPulseData { arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 50, 100, 100)), arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 50, 0, 0)) ) - segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) } + segments.forEach { list.add(createWaveSegment(it[0], it[1])) } list } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/main.kt b/src/main/kotlin/top/r3944realms/ltdmanager/main.kt index 525a10e..e665938 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/main.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/main.kt @@ -1,37 +1,50 @@ package top.r3944realms.ltdmanager import top.r3944realms.ltdmanager.core.config.YamlConfigLoader -import top.r3944realms.ltdmanager.module.McServerStatusModule import top.r3944realms.ltdmanager.module.* fun main() = GlobalManager.runBlockingMain { - val groupId:Long = 538751386 + val commonGroupId:Long = 538751386 + val whitelistGroupId:Long = 920719236 val selfQQId = 3327379836 val selfNickName = "闲趣老土豆" // 创建模块实例 val groupModule = GroupRequestHandlerModule( moduleName = "WhiteListGroup", client = GlobalManager.napCatClient, - targetGroupId = groupId + targetGroupId = whitelistGroupId ) - val groupMsgPollingModule = GroupMessagePollingModule( - moduleName = "WhiteListGroup", - targetGroupId = groupId, + val commonGroupMsgPollingModule = GroupMessagePollingModule( + moduleName = "CommonGroupMsgPolling", + targetGroupId = commonGroupId, pollIntervalMillis = 5_000L, msgHistoryCheck = 15 ) - val helpModule = HelpModule( + val whiteListGroupMsgPollingModule = GroupMessagePollingModule( + moduleName = "WhiteListGroup", + targetGroupId = whitelistGroupId, + pollIntervalMillis = 5_000L, + msgHistoryCheck = 15 + ) + val commonHelpModule = HelpModule( + moduleName = "CommonGroup", + keywords = listOf("help", "帮助"), + groupMessagePollingModule = commonGroupMsgPollingModule, + selfId = selfQQId, + selfNickName = selfNickName, + ) + val whitelistHelpModule = HelpModule( moduleName = "WhiteListGroup", keywords = listOf("help", "帮助"), - groupMessagePollingModule = groupMsgPollingModule, + groupMessagePollingModule = whiteListGroupMsgPollingModule, selfId = selfQQId, selfNickName = selfNickName, ) val toolConfig = YamlConfigLoader.loadToolConfig() - val rconModule = RconPlayerListModule( + val corconModule = RconPlayerListModule( moduleName = "WhiteListGroup", - groupMessagePollingModule = groupMsgPollingModule, + groupMessagePollingModule = commonGroupMsgPollingModule, rconTimeOut = 2_000L, cooldownMillis = 10_000L, selfId = selfQQId, @@ -46,76 +59,106 @@ fun main() = GlobalManager.runBlockingMain { "列表","服务器状态", "TPS", "tps", "list", "List" ) ) - val mailConfig = YamlConfigLoader.loadMailConfig() - val mailModule = MailModule( + val rconModule = RconPlayerListModule( moduleName = "WhiteListGroup", - host = mailConfig.host.toString(), - authToken = mailConfig.decryptedPassword.toString(), - port = mailConfig.port!!, - senderEmailAddress = mailConfig.mailAddress!!, - ) - val blessingSkinConfig = YamlConfigLoader.loadBlessingSkinServerConfig() - val invitationCodesModule = InvitationCodesModule( - moduleName = "WhiteListGroup", - groupMessagePollingModule = groupMsgPollingModule, - mailModule = mailModule, - apiToken = blessingSkinConfig.invitationApi?.decryptedToken!!, + groupMessagePollingModule = whiteListGroupMsgPollingModule, + rconTimeOut = 2_000L, + cooldownMillis = 10_000L, selfId = selfQQId, + selfNickName = selfNickName, + rconPath = toolConfig.rcon.mcRconToolPath.toString(), + rconConfigPath = toolConfig.rcon.mcRconToolConfigPath.toString(), keywords = setOf( - "申请皮肤站注册邀请码", - "申请土豆服务器注册邀请码", - "申请LTD邀请码", - "Apply for an invitation code" + //形容 + "土豆", "马铃薯", "Potato", "potato", "POTATO", + "Potatoes", "potatoes", "POTATOES", "🥔", + //正经 + "列表","服务器状态", "TPS", "tps", "list", "List" ) ) - val mcServerStatusModule = McServerStatusModule( - moduleName = "WhiteListGroup", - groupMessagePollingModule = groupMsgPollingModule, +// val mailConfig = YamlConfigLoader.loadMailConfig() +// val mailModule = MailModule( +// moduleName = "WhiteListGroup", +// host = mailConfig.host.toString(), +// authToken = mailConfig.decryptedPassword.toString(), +// port = mailConfig.port!!, +// senderEmailAddress = mailConfig.mailAddress!!, +// ) +// val blessingSkinConfig = YamlConfigLoader.loadBlessingSkinServerConfig() +// val invitationCodesModule = InvitationCodesModule( +// moduleName = "WhiteListGroup", +// groupMessagePollingModule = commonGroupMsgPollingModule, +// mailModule = mailModule, +// apiToken = blessingSkinConfig.invitationApi?.decryptedToken!!, +// selfId = selfQQId, +// keywords = setOf( +// "申请皮肤站注册邀请码", +// "申请土豆服务器注册邀请码", +// "申请LTD邀请码", +// "Apply for an invitation code" +// ) +// ) + val commonMcServerStatusModule = McServerStatusModule( + moduleName = "CommonGroup", + groupMessagePollingModule = commonGroupMsgPollingModule, selfId = selfQQId, cooldownMillis = 20_000L, selfNickName = selfNickName, - commands = listOf("/m", "/mcs", "seek", "s"), + commands = listOf("/m", "/mcs", "seek", "s", "test"), presetServer = mapOf( - setOf("先行土豆", "先行", "pre", "Pre", "BF", "bf", "p", "P") to "n2.akiracloud.net:10599", - 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" ) ) - val banModule = BanModule( + val whitelistMcServerStatusModule = McServerStatusModule( moduleName = "WhiteListGroup", - groupMessagePollingModule = groupMsgPollingModule, + groupMessagePollingModule = whiteListGroupMsgPollingModule, selfId = selfQQId, - adminsId = listOf(1283411677), - muteCommandPrefixList = listOf("口球", "mute", "Mute", "禁言"), - unmuteCommandPrefixList = listOf("解禁", "unmute", "Unmute", "解除禁言"), - minBanMinutes = 1, - maxBanMinutes = 15, + cooldownMillis = 20_000L, + selfNickName = selfNickName, + commands = listOf("/m", "/mcs", "seek", "s", "test"), + presetServer = mapOf( + setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106", + setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top" + ) + ) + val dgLabModule = DGLabModule( + moduleName = "DG", + groupMessagePollingModule = commonGroupMsgPollingModule, + selfId = selfQQId, + adminIds = listOf(2561098830L), + commandHead = listOf("dglab") ) -// val modGroupHandlerModule = ModGroupHandlerModule( -// moduleName = "ModGroup", -// targetGroupId = 339340846, -// answers = listOf("戏鸢", "一只戏鸢", "折戏鸢", "LostInLinearPast", "lostinlinearpast"), -// pollIntervalMillis = 15_000L, -// ) // 注册模块到全局模块管理器 GlobalManager.moduleManager.registerModule(groupModule) - GlobalManager.moduleManager.registerModule(groupMsgPollingModule) - GlobalManager.moduleManager.registerModule(mcServerStatusModule) + GlobalManager.moduleManager.registerModule(commonGroupMsgPollingModule) + GlobalManager.moduleManager.registerModule(whiteListGroupMsgPollingModule) + GlobalManager.moduleManager.registerModule(commonMcServerStatusModule) GlobalManager.moduleManager.registerModule(rconModule) - GlobalManager.moduleManager.registerModule(mailModule) - GlobalManager.moduleManager.registerModule(invitationCodesModule) - GlobalManager.moduleManager.registerModule(helpModule) - GlobalManager.moduleManager.registerModule(banModule) + GlobalManager.moduleManager.registerModule(corconModule) + GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule) +// GlobalManager.moduleManager.registerModule(mailModule) +// GlobalManager.moduleManager.registerModule(invitationCodesModule) + GlobalManager.moduleManager.registerModule(whitelistHelpModule) + GlobalManager.moduleManager.registerModule(commonHelpModule) + GlobalManager.moduleManager.registerModule(dgLabModule) +// GlobalManager.moduleManager.registerModule(banModule) // GlobalManager.moduleManager.registerModule(modGroupHandlerModule) // 加载模块 GlobalManager.moduleManager.loadModule(groupModule.name) - GlobalManager.moduleManager.loadModule(groupMsgPollingModule.name) - GlobalManager.moduleManager.loadModule(mcServerStatusModule.name) + GlobalManager.moduleManager.loadModule(commonGroupMsgPollingModule.name) + GlobalManager.moduleManager.loadModule(whiteListGroupMsgPollingModule.name) + GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name) + GlobalManager.moduleManager.loadModule(corconModule.name) GlobalManager.moduleManager.loadModule(rconModule.name) - GlobalManager.moduleManager.loadModule(mailModule.name) - GlobalManager.moduleManager.loadModule(invitationCodesModule.name) - GlobalManager.moduleManager.loadModule(helpModule.name) - GlobalManager.moduleManager.loadModule(banModule.name) +// GlobalManager.moduleManager.loadModule(mailModule.name) +// GlobalManager.moduleManager.loadModule(invitationCodesModule.name) + GlobalManager.moduleManager.loadModule(commonHelpModule.name) + GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name) + GlobalManager.moduleManager.loadModule(whitelistHelpModule.name) + GlobalManager.moduleManager.loadModule(dgLabModule.name) +// GlobalManager.moduleManager.loadModule(banModule.name) // GlobalManager.moduleManager.loadModule(modGroupHandlerModule.name) } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt index c61f853..30a3068 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt @@ -12,8 +12,8 @@ import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter import top.r3944realms.ltdmanager.napcat.data.ID import top.r3944realms.ltdmanager.napcat.data.MessageElement import top.r3944realms.ltdmanager.napcat.data.MessageType +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg import top.r3944realms.ltdmanager.napcat.event.group.GetGroupShutListEvent -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent import top.r3944realms.ltdmanager.napcat.request.group.GetGroupShutListRequest import top.r3944realms.ltdmanager.napcat.request.group.SetGroupBanRequest import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest @@ -78,7 +78,7 @@ class BanModule( scope?.cancel() } - private suspend fun handleMessages(messages: List) { + private suspend fun handleMessages(messages: List) { // 先过一遍过滤器,只有符合条件的才进入后续处理 val filtered = triggerFilter.filter(messages) for (msg in filtered) { @@ -91,7 +91,7 @@ class BanModule( * - text 段直接拼接 * - 如果消息段里包含 @(在 MessageData 中为 qq 字段),则拼成 "@{qq}",方便 parseMentionToUserId 解析 */ - private fun GetFriendMsgHistoryEvent.SpecificMsg.plainText(): String { + private fun MsgHistorySpecificMsg.plainText(): String { return this.message.joinToString(" ") { seg -> // 如果 message element 包含 qq 字段(即@用户),优先使用它 seg.data.qq?.let { "@${it}" } ?: (seg.data.text ?: "") @@ -100,7 +100,7 @@ class BanModule( /** * 从消息段中提取所有被 @ 的用户 ID */ - private fun GetFriendMsgHistoryEvent.SpecificMsg.getMentionedUserIds(): List { + private fun MsgHistorySpecificMsg.getMentionedUserIds(): List { return this.message .filter { it.type == MessageType.At && it.data.qq != null } .mapNotNull { it.data.qq } @@ -111,7 +111,7 @@ class BanModule( } } } - private suspend fun processUnBanCommand(msg: GetFriendMsgHistoryEvent.SpecificMsg) { + private suspend fun processUnBanCommand(msg: MsgHistorySpecificMsg) { try { pardonCommandParse.parseCommand(msg.plainText()) ?: return // 获取所有被 @ 的用户 @@ -149,7 +149,7 @@ class BanModule( saveState(banState) } } - private suspend fun processBanCommand(msg: GetFriendMsgHistoryEvent.SpecificMsg) { + private suspend fun processBanCommand(msg: MsgHistorySpecificMsg) { try { val parsed = banCommandParse.parseCommand(msg.plainText()) ?: return val (_, argument) = parsed @@ -171,9 +171,9 @@ class BanModule( is ID.LongValue -> target.value } - // 权限检查:非管理员不能禁言他人 + // 权限检查:非管理员不能禁言多个他人 if (mentionedUserIds.isNotEmpty() && mentionedUserIds.size != 1 && msg.sender.userId !in adminsId) { - sendGroupMessage("❌ 你没有权限禁言使用禁言多用户功能", msg.realId) + sendGroupMessage("❌ 你没有权限使用禁言多用户功能", msg.realId) continue } @@ -201,7 +201,7 @@ class BanModule( } val selfDuration = durationSeconds * factorX - if (Random.nextInt(100) < chance) { + if (Random.nextInt(0,100) > chance) { // 触发反禁自己 banUser(ID.long(msg.sender.userId), groupMessagePollingModule.targetGroupId, selfDuration) sendGroupMessage( @@ -262,6 +262,7 @@ class BanModule( override fun info(): String { return buildString { append("[$name] 指令禁言模块:\n") + append(" 管理员用户ID: ${adminsId}\n") append(" - 用户发送 ${banCommandParse.getCommands().joinToString("、")} 来禁言自己或指定其他用户(需管理员权限)。\n") append(" - 支持指定禁言分钟数或随机分钟数,范围 $minBanMinutes-$maxBanMinutes 分钟。\n") append(" - 支持对单个 @ 用户禁言,有概率反禁自己(骰子点数决定概率)。\n") diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/BaseModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/BaseModule.kt index bba80a6..d66a051 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/BaseModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/BaseModule.kt @@ -84,22 +84,46 @@ abstract class BaseModule(baseName : String = "BaseModule", idName : String = "" * 提供访问全局 NapCatClient 的快捷方式 */ protected val napCatClient get() = GlobalManager.napCatClient + /** * 提供访问全局 blessingSkinClient 的快捷方式 */ protected val blessingSkinClient get() = GlobalManager.blessingSkinClient + /** * 提供访问全局 mcSrvStatusClient 的快捷方式 */ protected val mcSrvStatusClient get() = GlobalManager.mcSrvStatusClient + /** * 提供访问全局 加载模块 的快捷方式 */ protected val moduleMap get() = GlobalManager.moduleManager.getModules() + /** * 获取数据库连接 * 使用 try-with-resources 时会自动关闭 */ protected fun getConnection() = GlobalManager.getConnection() + /** + * 安全获取 NapCatClient,避免空指针异常 + */ + protected fun getNapCatClientOrNull() = try { + GlobalManager.napCatClient + } catch (e: Exception) { + LoggerUtil.logger.warn("获取NapCatClient失败", e) + null + } + /** + * 安全获取 NapCatClient,如果获取失败则抛出详细异常 + */ + protected fun getNapCatClientOrThrow(): Any { + val client = try { + GlobalManager.napCatClient + } catch (e: Exception) { + throw IllegalStateException("无法获取NapCatClient,请检查GlobalManager初始化状态", e) + } + return client ?: throw IllegalStateException("NapCatClient为null,请检查GlobalManager初始化") + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/DGLabModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/DGLabModule.kt index 35950ac..213bf45 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/DGLabModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/DGLabModule.kt @@ -1,14 +1,489 @@ package top.r3944realms.ltdmanager.module +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.arguments.IntegerArgumentType +import com.mojang.brigadier.arguments.LongArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal +import com.mojang.brigadier.builder.RequiredArgumentBuilder.argument +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.r3944realms.dg_lab.api.message.IPowerBoxMsg +import com.r3944realms.dg_lab.api.message.argType.ChangePolicy +import com.r3944realms.dg_lab.api.message.argType.Channel +import com.r3944realms.dg_lab.api.websocket.message.MessageDirection +import com.r3944realms.dg_lab.manager.DGPBClientManager +import kotlinx.coroutines.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import top.r3944realms.ltdmanager.GlobalManager +import top.r3944realms.ltdmanager.dglab.DgLab +import top.r3944realms.ltdmanager.dglab.model.game.GameClientOperation +import top.r3944realms.ltdmanager.dglab.model.game.GameServerOperation +import top.r3944realms.ltdmanager.dglab.model.game.Player +import top.r3944realms.ltdmanager.dglab.model.pulseware.DefaultPulseData +import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter +import top.r3944realms.ltdmanager.module.common.filter.type.IgnoreSelfFilter +import top.r3944realms.ltdmanager.module.common.filter.type.KeywordFilter +import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter +import top.r3944realms.ltdmanager.napcat.NapCatClient +import top.r3944realms.ltdmanager.napcat.data.ID +import top.r3944realms.ltdmanager.napcat.data.MessageElement +import top.r3944realms.ltdmanager.napcat.data.MessageType +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg +import top.r3944realms.ltdmanager.napcat.event.group.GetGroupMemberListEvent +import top.r3944realms.ltdmanager.napcat.request.group.GetGroupMemberListRequest +import top.r3944realms.ltdmanager.napcat.request.message.SetMsgEmojiLikeRequest +import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest +import top.r3944realms.ltdmanager.utils.LoggerUtil +import java.io.File +import kotlin.math.abs + +/** + * 数据 {QQ} + */ class DGLabModule( moduleName: String, -): - BaseModule("DGLabModule", moduleName) { - override fun onLoad() { + private val groupMessagePollingModule : GroupMessagePollingModule, + private val selfId: Long, + val adminIds: List = listOf(), + val maxClientNumber: Int = 10, + val commandHead: List = listOf("dglab"), +) : BaseModule("DGLabModule", moduleName), PersistentState { + + var dgLabManager: DgLab? = null + private var scope: CoroutineScope? = null + private var dglabCommandDispatcher: CommandDispatcher = CommandDispatcher().apply { + for (command in commandHead) register( + literal(command) + .then(literal("server").requires { adminIds.contains(it.id) } + .then(literal("start").executes { startDgLab() }) + .then(literal("stop").executes { stopDgLab() }) + .then(literal("stopAllClient").executes { stopAllDgLabClient() }) + ) + .then(literal("client") + .then(literal("start").executes { startClient(it.source.id) }) + .then(literal("stop").executes { stopClient(it.source.id) }) + ) + .then(literal("strength") + .then(argument("channel", StringArgumentType.string()) + .then(literal("add") + .then(argument("value", IntegerArgumentType.integer(-200, 200)) + .executes { strengthAdd(it.source.id, StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) } + ) + ) + .then(literal("set") + .then(argument("value", IntegerArgumentType.integer(0, 200)) + .executes { strengthSet(it.source.id, StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) } + ) + ) + ) + .then(argument("player", LongArgumentType.longArg()) + .then(argument("channel", StringArgumentType.string()) + .then(literal("add") + .then(argument("value", IntegerArgumentType.integer(-200, 200)) + .executes { strengthAdd(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) } + ) + ) + .then(literal("set") + .then(argument("value", IntegerArgumentType.integer(0, 200)) + .executes { strengthSet(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) } + ) + ) + ) + ) + ) + .then(literal("pulse") + .then(argument("channel", StringArgumentType.string()) + .then(literal("clear").executes { pulseClear(it.source.id, StringArgumentType.getString(it, "channel")) }) + .then(literal("set") + .then(argument("pulseName", StringArgumentType.string()) + .then(argument("timer", IntegerArgumentType.integer(0, Int.MAX_VALUE)) + .executes { pulseSet(it.source.id, StringArgumentType.getString(it, "channel"), StringArgumentType.getString(it, "pulseName"), IntegerArgumentType.getInteger(it, "timer")) } + ) + ) + ) + ) + .then(argument("player", LongArgumentType.longArg()) + .then(argument("channel", StringArgumentType.string()) + .then(literal("clear").executes { pulseClear(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel")) }) + .then(literal("set") + .then(argument("pulseName", StringArgumentType.string()) + .then(argument("timer", IntegerArgumentType.integer(0, Int.MAX_VALUE)) + .executes { pulseSet(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel"), StringArgumentType.getString(it, "pulseName"), IntegerArgumentType.getInteger(it, "timer")) } + ) + ) + ) + ) + ) + ) + ) +// .then(literal("info").executes {} +// .then(argument("player", StringArgumentType.string()).executes {}) +// ) } + private val stateFile: File = getStateFileInternal("dg_lab_state.json", name) + private val stateBackupFile: File = getStateFileInternal("dg_lab_state.json.bak", name) + private var dgLabState = loadState() + override fun getState(): DgLabState = dgLabState + override fun getStateFileInternal(): File = stateFile + private val triggerFilter by lazy { + TriggerMessageFilter( + listOf( + IgnoreSelfFilter(selfId), + NewMessageFilter { userId -> + dgLabState.getLastTriggerTime(userId) to dgLabState.getLastTriggerRealId(userId) + }, + KeywordFilter(commandHead.toSet()) + ) + ) + } + + override fun onLoad() { + LoggerUtil.logger.info("[$name] 模块已装载,监听群组: ${groupMessagePollingModule.targetGroupId}") + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope!!.launch { + LoggerUtil.logger.info("[$name] 轮询协程启动") + dgLabManager = DgLab() + val gameServerOperation = GameServerOperation(napCatClient, groupMessagePollingModule.targetGroupId) + dgLabManager?.createServerManager(gameServerOperation)?.let { dgLabManager?.initServerManager(it) } + gameServerOperation.serverManager = dgLabManager?.serverManager + init() + groupMessagePollingModule.messagesFlow.collect { messages -> + if (loaded) handleMessages(messages) + } + } + } override suspend fun onUnload() { + saveState(dgLabState) + dgLabManager?.close() + scope?.cancel() + LoggerUtil.logger.info("[$name] 模块已卸载完成") + } + private suspend fun handleMessages(messages: List) { + if (messages.isEmpty()) return + + // 先对所有消息进行 @ 提及处理 + val processedMessages = messages.map { msg -> + val processedText = processMessageMentionsToLong(msg) + msg to processedText + } + + val triggerMsgs = processedMessages + .filter { (msg, _) -> filterTriggerMessages(listOf(msg)).isNotEmpty() } + .map { (msg, processedText) -> Triple(msg, msg.userId, processedText) } + + if (triggerMsgs.isEmpty()) return + + var refPlayer: Player? = null + var refMsg: MsgHistorySpecificMsg? = null + try { + triggerMsgs.forEach { (msg, userId, processedText) -> + refMsg = msg + LoggerUtil.logger.info("[$name] 原始消息用户: $userId") + LoggerUtil.logger.info("[$name] 处理后的命令: $processedText") + + refPlayer = dgLabManager?.getPlayerManager()?.getPlayer(userId) + dgLabState = dgLabState.updateOrCreate(userId, msg.realId, msg.time) + val execute = dglabCommandDispatcher.execute(processedText, refPlayer) + scope?.launch { + GlobalManager.napCatClient.sendUnit( + SetMsgEmojiLikeRequest( + if (execute == 0) 1.0 else 2.0, ID.long(msg.realId), true + ) + ) + } + } + } catch (e: CommandSyntaxException) { + val reader = e.input // 用户输入 + val cursor = e.cursor + val partialInput = reader.substring(0, cursor) + if (refPlayer != null) { + val node = dglabCommandDispatcher.parse( + partialInput, + dgLabManager?.getPlayerManager()?.getPlayer(refPlayer!!.id) + ).context.nodes.lastOrNull()?.node + val usage = if (node != null) { + val values = dglabCommandDispatcher.getSmartUsage(node, refPlayer).values + if(!values.isEmpty()) "目前节点可使用的子命令: $values" + else "目前节点无用法" + + } else { + "未找到用法" + } + + sendFailedMessage( + napCatClient, + text = "指令解析错误:\n ${e.message}\n\n$usage", + qq = refMsg?.userId, + realId = refMsg?.realId, + time = refMsg?.time + ) + } + } + catch (e: Exception) { + sendFailedMessage(napCatClient, text = "系统错误,请联系管理员: ${e.message}") + } finally { + saveState(dgLabState) + } + } + /** + * 处理整个消息中的 @ 提及,转换为 Long 类型,并清理多余空格 + */ + private fun processMessageMentionsToLong(msg: MsgHistorySpecificMsg): String { + val processedText = msg.message.joinToString(" ") { seg -> + when (seg.type) { + MessageType.At -> { + // 处理 @ 提及,转换为 Long + seg.data.qq?.let { qq -> + when (qq) { + is ID.StringValue -> qq.value.toLong().toString() + is ID.LongValue -> qq.value.toString() + } + } ?: seg.data.text ?: "" + } + MessageType.Text -> { + seg.data.text ?: "" + } + else -> "" + } + }.trim() + + // 清理多余空格:将多个连续空格替换为单个空格 + return processedText.replace(Regex("\\s+"), " ") + } + private suspend fun filterTriggerMessages( + messages: List + ): List = triggerFilter.filter(messages) + private suspend fun init() { + val getGroupMemberListEvent = napCatClient.send( + GetGroupMemberListRequest( + ID.long(groupMessagePollingModule.targetGroupId), + false + ) + ) + dgLabManager?.initOrLoadPlayerManager(getGroupMemberListEvent.data.filter { !it.isRobot } + .associate { it.userId to it.nickname }) + dgLabManager?.initClientManager() + } +// private fun getHelp(): Int { +// scope?.launch { +// sendMessage() +// } +// return 1 +// } + private fun startDgLab(): Int { + dgLabManager?.getServer()?.start() + return 1 + } + private fun stopDgLab(): Int { + dgLabManager?.getServer()?.stop() + return 1 + } + private fun stopAllDgLabClient(): Int { + dgLabManager?.clientManager?.stopAll() + return 1 + } + private fun startClient(qq: Long): Int { + if (dgLabManager?.getPlayerManager()?.getOnlinePlayerSize()!! > maxClientNumber) { + scope!!.launch { + sendFailedMessage(napCatClient, text = "无法启动新的客户端, 因为已到达最大连接数${maxClientNumber}") + } + return -1 + } + val operation = GameClientOperation( + napCatClient, + groupMessagePollingModule.targetGroupId, + dgLabManager!!.getPlayerManager(), + qq + ) + val dgpbClientManager = dgLabManager?.getClientOrCreate( + qq.toString(), + operation + ) + operation.clientSelf = dgpbClientManager + dgpbClientManager?.start() + + return 1 + } + private fun stopClient(qq: Long): Int { + dgLabManager?.getClient(qq.toString())?.stop() + return 1 + } + private fun strengthAdd(qq: Long, channel: String, value: Int): Int { + val client = dgLabManager?.getClient(qq.toString()) ?: return -1 + val changePolicy = if(value >= 0) ChangePolicy.INCREASE else ChangePolicy.DECREASE + val strengthValue = abs(value) + + when(channel) { + "a" -> sendStrengthChange(client, Channel.A, changePolicy, strengthValue) + "b" -> sendStrengthChange(client, Channel.B, changePolicy, strengthValue) + "ab" -> { + sendStrengthChange(client, Channel.A, changePolicy, strengthValue) + sendStrengthChange(client, Channel.B, changePolicy, strengthValue) + } + } + return 0 + } + private fun strengthSet(qq: Long, channel: String, value: Int): Int { + val client = dgLabManager?.getClient(qq.toString()) ?: return -1 + when(channel) { + "a" -> sendStrengthChange(client, Channel.A, ChangePolicy.GOTO, value) + "b" -> sendStrengthChange(client, Channel.B, ChangePolicy.GOTO, value) + "ab" -> { + sendStrengthChange(client, Channel.A, ChangePolicy.GOTO, value) + sendStrengthChange(client, Channel.B, ChangePolicy.GOTO, value) + } + } + return 0 + } + + private fun sendStrengthChange(client: DGPBClientManager, channel: Channel, policy: ChangePolicy, value: Int) { + client.send(IPowerBoxMsg.StrengthChange(channel, policy, value) + .toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION)) + } + + private fun pulseClear(qq: Long, channel: String): Int { + val client = dgLabManager?.getClient(qq.toString()) ?: return -1 + when(channel) { + "a" -> client.send(IPowerBoxMsg.Clear(Channel.A) + .toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION)) + "b" -> client.send(IPowerBoxMsg.Clear(Channel.B) + .toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION)) + "ab" -> { + client.send(IPowerBoxMsg.Clear(Channel.A) + .toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION)) + client.send(IPowerBoxMsg.Clear(Channel.B) + .toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION)) + } + } + return 0 + } + private fun pulseSet(qq: Long, channel: String, pulseName: String, timer: Int): Int { + val client = dgLabManager?.getClient(qq.toString()) ?: return -1 + val pulse = DefaultPulseData.allPulseWaveLists()[pulseName] ?: return -2 + when(channel) { + "a" -> client.send(IPowerBoxMsg.Pulse(Channel.A, pulse, timer) + .toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION)) + "b" -> client.send(IPowerBoxMsg.Pulse(Channel.B, pulse, timer) + .toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION)) + "ab" -> { + client.send(IPowerBoxMsg.Pulse(Channel.A, pulse, timer) + .toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION)) + client.send(IPowerBoxMsg.Pulse(Channel.B, pulse, timer) + .toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION)) + } + } + return 0 + } + + private suspend fun sendMessage( + client: NapCatClient, + qq: Long, + realId: Long, + time: Long, + text: String = "正常消息" + ) { + LoggerUtil.logger.info("[$name] 发送消息: realId=$realId, text=$text") + + val request = SendGroupMsgRequest( + MessageElement.reply(ID.long(realId), text), + ID.long(groupMessagePollingModule.targetGroupId) + ) + client.sendUnit(request) + LoggerUtil.logger.info("[$name] 已发送 消息") + + // 更新触发的最大 realId + dgLabState = dgLabState.updateOrCreate(qq, realId, time) + } + + private suspend fun sendFailedMessage( + client: NapCatClient, + qq: Long? = null, + realId: Long? = null, + time: Long? = null, + text: String = "失败消息" + ) { + LoggerUtil.logger.info("[$name] 发送失败消息: realId=$realId, text=$text") + if (realId != null && qq != null && time != null) { + val request = SendGroupMsgRequest( + MessageElement.reply(ID.long(realId), text), + ID.long(groupMessagePollingModule.targetGroupId) + ) + client.sendUnit(request) + LoggerUtil.logger.info("[$name] 已发送 失败消息") + + // 更新触发的最大 realId + dgLabState = dgLabState.updateOrCreate(qq, realId, time) + } else { + val request = SendGroupMsgRequest( + listOf(MessageElement.text(text)), + ID.long(groupMessagePollingModule.targetGroupId) + ) + client.sendUnit(request) + LoggerUtil.logger.info("[$name] 已发送 失败消息[无指定对象]") + } + } + // -------- 持久化 ----------- + @Serializable + data class DgLabDetail( + val realId : Long, + val time: Long, + ) + + + @Serializable + data class DgLabState( + val map: Map = emptyMap() + ) { + fun getLastTriggerTime(userId: Long): Long = map[userId]?.time ?: -1 + fun getLastTriggerRealId(userId: Long): Long = map[userId]?.realId ?: -1 + + /** + * 更新或创建某个用户的触发信息 + * - 如果传了 realId,则更新 realId + * - 如果传了 time,则更新 time + * - 其他字段保持原值 + */ + fun updateOrCreate( + userId: Long, + realId: Long? = null, + time: Long? = null + ): DgLabState { + val old = map[userId] + val newDetail = DgLabDetail( + realId = realId ?: old?.realId ?: -1, + time = time ?: old?.time ?: -1 + ) + val newMap = map.toMutableMap().apply { put(userId, newDetail) } + return copy(map = newMap) + } + } + + override fun saveState(state: DgLabState) { + try { + if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true) + stateFile.writeText(Json.encodeToString(state)) + } catch (e: Exception) { + LoggerUtil.logger.error("[$name] 保存状态失败", e) + } + } + + override fun loadState(): DgLabState { + return try { + val fileToRead = when { + stateFile.exists() -> stateFile + stateBackupFile.exists() -> stateBackupFile + else -> null + } ?: return DgLabState() + + Json.decodeFromString(fileToRead.readText()) + } catch (e: Exception) { + LoggerUtil.logger.warn("[$name] 读取状态失败", e) + DgLabState() + } } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupMessagePollingModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupMessagePollingModule.kt index a987e99..f9d5780 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupMessagePollingModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupMessagePollingModule.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import top.r3944realms.ltdmanager.napcat.data.ID -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg import top.r3944realms.ltdmanager.napcat.event.message.GetGroupMsgHistoryEvent import top.r3944realms.ltdmanager.napcat.request.message.GetGroupMsgHistoryRequest import top.r3944realms.ltdmanager.utils.LoggerUtil @@ -19,11 +19,11 @@ class GroupMessagePollingModule( private var scope: CoroutineScope? = null // 用 Flow 存消息,其他模块可以订阅 - private val _messagesFlow = MutableSharedFlow>( + private val _messagesFlow = MutableSharedFlow>( replay = 1, // 保留最近一份消息 extraBufferCapacity = 1 ) - val messagesFlow: SharedFlow> = _messagesFlow.asSharedFlow() + val messagesFlow: SharedFlow> = _messagesFlow.asSharedFlow() override fun onLoad() { LoggerUtil.logger.info("[$name] 启动消息轮询 (群: $targetGroupId)") @@ -31,12 +31,12 @@ class GroupMessagePollingModule( scope!!.launch { while (isActive && loaded) { try { - val event = napCatClient.send( + val event = getNapCatClientOrNull()?.send( GetGroupMsgHistoryRequest( count = msgHistoryCheck, groupId = ID.long(targetGroupId) ) - ) as? GetGroupMsgHistoryEvent + ) val messages = event?.data?.messages ?: emptyList() LoggerUtil.logger.debug("[$name] 拉取到 ${messages.size} 条消息") diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupRequestHandlerModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupRequestHandlerModule.kt index 4facc32..7989729 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupRequestHandlerModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupRequestHandlerModule.kt @@ -137,7 +137,7 @@ class GroupRequestHandlerModule( try { getConnection().use { conn -> val stmt = conn.prepareStatement( - "SELECT status FROM minecraft_manager_ltd.players WHERE qq=?" + "SELECT status FROM minecraft_manager_ltd_8.players WHERE qq=?" ) stmt.setLong(1, actor) val rs = stmt.executeQuery() diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt index cb77df1..46c669d 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt @@ -17,7 +17,7 @@ import top.r3944realms.ltdmanager.napcat.NapCatClient import top.r3944realms.ltdmanager.napcat.data.ID import top.r3944realms.ltdmanager.napcat.data.MessageElement import top.r3944realms.ltdmanager.napcat.data.MessageType -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg import top.r3944realms.ltdmanager.napcat.request.message.SendForwardMsgRequest import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest import top.r3944realms.ltdmanager.utils.LoggerUtil @@ -37,7 +37,7 @@ class HelpModule( // 命令解析器 private val commandParser = CommandParser(keywords) - private val GetFriendMsgHistoryEvent.SpecificMsg.textContent: String + private val MsgHistorySpecificMsg.textContent: String get() = message.joinToString("") { it.data.text ?: "" } // 持久化文件 @@ -100,7 +100,7 @@ class HelpModule( LoggerUtil.logger.info("[$name] 模块已卸载完成") } - private suspend fun handleMessages(messages: List) { + private suspend fun handleMessages(messages: List) { val filtered = triggerFilter.filter(messages) val triggerMsg = filtered.maxByOrNull { it.time } ?: return @@ -117,7 +117,7 @@ class HelpModule( } } - private suspend fun sendAllModulesHelp(msg: GetFriendMsgHistoryEvent.SpecificMsg) { + private suspend fun sendAllModulesHelp(msg: MsgHistorySpecificMsg) { val messages = moduleMap.map { (name, module) -> val textBuilder = StringBuilder() textBuilder.appendLine("===== $name =====") @@ -153,7 +153,7 @@ class HelpModule( updateTriggerState(msg) } - private suspend fun sendModuleHelp(msg: GetFriendMsgHistoryEvent.SpecificMsg, moduleName: String, module: BaseModule) { + private suspend fun sendModuleHelp(msg: MsgHistorySpecificMsg, moduleName: String, module: BaseModule) { val textBuilder = StringBuilder() textBuilder.appendLine("===== $moduleName =====") textBuilder.appendLine(module.info()) @@ -187,7 +187,7 @@ class HelpModule( updateTriggerState(msg) } - private suspend fun sendText(msg: GetFriendMsgHistoryEvent.SpecificMsg, text: String) { + private suspend fun sendText(msg: MsgHistorySpecificMsg, text: String) { val request = SendGroupMsgRequest( MessageElement.reply(ID.long(msg.realId), text), ID.long(groupMessagePollingModule.targetGroupId) @@ -196,7 +196,7 @@ class HelpModule( updateTriggerState(msg) } - private fun updateTriggerState(msg: GetFriendMsgHistoryEvent.SpecificMsg) { + private fun updateTriggerState(msg: MsgHistorySpecificMsg) { lastTriggerState.lastTriggeredRealId = msg.realId lastTriggerState.lastTriggerTime = msg.time saveState(lastTriggerState) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt index 86b18fa..d88bb7f 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt @@ -20,7 +20,7 @@ import top.r3944realms.ltdmanager.module.exception.InvitationCodeException import top.r3944realms.ltdmanager.napcat.NapCatClient import top.r3944realms.ltdmanager.napcat.data.ID import top.r3944realms.ltdmanager.napcat.data.MessageElement -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest import top.r3944realms.ltdmanager.utils.HtmlTemplateUtil import top.r3944realms.ltdmanager.utils.LoggerUtil @@ -165,14 +165,14 @@ class InvitationCodesModule( // ========================= // 消息处理主流程 // ========================= - private suspend fun handleMessages(messages: List) { + private suspend fun handleMessages(messages: List) { if (messages.isEmpty()) return val triggerMsgs = filterTriggerMessages(messages) if (triggerMsgs.isEmpty()) return try { - val hadValidCodeButNotUsed = mutableListOf>() - val needNewCode = mutableListOf>() + val hadValidCodeButNotUsed = mutableListOf>() + val needNewCode = mutableListOf>() getIdAndSelectSituation(triggerMsgs, hadValidCodeButNotUsed, needNewCode) createAndSearchInvitationCodeIdsThenUpdateDate(needNewCode) @@ -186,8 +186,8 @@ class InvitationCodesModule( /** 过滤出符合条件的触发消息 */ private suspend fun filterTriggerMessages( - messages: List - ): List { + messages: List + ): List { // 先应用通用过滤器 val filtered = triggerFilter.filter(messages) @@ -198,9 +198,9 @@ class InvitationCodesModule( .mapNotNull { (_, msgs) -> msgs.maxByOrNull { it.time } } } - private suspend fun getIdAndSelectSituation(msgs: List, - hadVaildCodeButNotUseList : MutableList>, - needNewCodeList: MutableList>) { + private suspend fun getIdAndSelectSituation(msgs: List, + hadVaildCodeButNotUseList : MutableList>, + needNewCodeList: MutableList>) { if (msgs.isEmpty()) return val qqIds = msgs.map { it.userId } @@ -273,7 +273,7 @@ class InvitationCodesModule( sendFailedMessage(napCatClient, text = "批量查询用户资格信息失败,请联系管理员: ${e.message}") } } - private suspend fun hadVaildCodeButNotUseListHandler(list: List>) { + private suspend fun hadVaildCodeButNotUseListHandler(list: List>) { if (list.isEmpty()) return val whiteListIds = list.map { it.first } @@ -405,7 +405,7 @@ class InvitationCodesModule( lastTriggerMapState = lastTriggerMapState.updateLastTrigger(qq, realId, -1) } private suspend fun createAndSearchInvitationCodeIdsThenUpdateDate( - needNewTokenIdAndMsgPairs: List>, + needNewTokenIdAndMsgPairs: List>, ) { if (needNewTokenIdAndMsgPairs.isEmpty()) return @@ -461,7 +461,7 @@ class InvitationCodesModule( */ private fun validateCodeCountMatch( invitationCodes: List?, - needNewTokenIdAndMsgPairs: List> + needNewTokenIdAndMsgPairs: List> ) { if (invitationCodes == null) { throw InvitationCodeException.ApiFailureException("获取邀请码请求失败") diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt index 2a6c157..60454d0 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt @@ -17,7 +17,7 @@ import top.r3944realms.ltdmanager.napcat.NapCatClient import top.r3944realms.ltdmanager.napcat.data.ID import top.r3944realms.ltdmanager.napcat.data.MessageElement import top.r3944realms.ltdmanager.napcat.data.MessageType -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg import top.r3944realms.ltdmanager.napcat.request.message.SendForwardMsgRequest import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest import top.r3944realms.ltdmanager.utils.LoggerUtil @@ -112,7 +112,7 @@ class McServerStatusModule( LoggerUtil.logger.info("[$name] 模块已卸载完成") } - private suspend fun handleMessages(messages: List) { + private suspend fun handleMessages(messages: List) { if (messages.isEmpty()) return val triggerMsgs = filterTriggerMessages(messages) if (triggerMsgs.isEmpty()) return @@ -129,8 +129,8 @@ class McServerStatusModule( } private suspend fun filterTriggerMessages( - messages: List - ): List = triggerFilter.filter(messages) + messages: List + ): List = triggerFilter.filter(messages) private suspend fun sendFailedMessage( client: NapCatClient, @@ -169,7 +169,7 @@ class McServerStatusModule( - private suspend fun processCommand(msg: GetFriendMsgHistoryEvent.SpecificMsg) { + private suspend fun processCommand(msg: MsgHistorySpecificMsg) { // 找出文本内容 val text = msg.message .firstOrNull { it.type == MessageType.Text } @@ -226,7 +226,7 @@ class McServerStatusModule( // ---------------- 转发消息封装 ---------------- private suspend fun sendStatusForwardMessage( client: NapCatClient, - msg: GetFriendMsgHistoryEvent.SpecificMsg, + msg: MsgHistorySpecificMsg, address: String, status: McServerStatus, realId: Long, diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/ModGroupHandlerModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/ModGroupHandlerModule.kt index cf5d0ae..1855b6a 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/ModGroupHandlerModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/ModGroupHandlerModule.kt @@ -93,7 +93,7 @@ class ModGroupHandlerModule( } saveState(state) } - fun getRejectRecord(userId: Long): RejectRecord? { + private fun getRejectRecord(userId: Long): RejectRecord? { return getState().records[userId] } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/PersistentState.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/PersistentState.kt index 9098176..afca376 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/PersistentState.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/PersistentState.kt @@ -9,8 +9,8 @@ interface PersistentState { fun saveState(state: T) fun loadState(): T // 默认实现:统一管理 data 目录下的文件 - fun getStateFileInternal(name: String, moduleName: String): File { - val dataDir = File("data", FileNameFilter.filterFileName(moduleName)) + fun getStateFileInternal(name: String, subName: String): File { + val dataDir = File("data", FileNameFilter.filterFileName(subName)) if (!dataDir.exists()) dataDir.mkdirs() return File(dataDir, name) } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/RconPlayerListModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/RconPlayerListModule.kt index 08bc764..f8c8e6f 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/RconPlayerListModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/RconPlayerListModule.kt @@ -17,7 +17,7 @@ import top.r3944realms.ltdmanager.napcat.NapCatClient import top.r3944realms.ltdmanager.napcat.data.ID import top.r3944realms.ltdmanager.napcat.data.MessageElement import top.r3944realms.ltdmanager.napcat.data.MessageType -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg import top.r3944realms.ltdmanager.napcat.request.message.SendForwardMsgRequest import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest import top.r3944realms.ltdmanager.utils.CmdUtil @@ -110,7 +110,7 @@ class RconPlayerListModule( LoggerUtil.logger.info("[$name] 模块已卸载完成") } - private suspend fun handleMessages(messages: List) { + private suspend fun handleMessages(messages: List) { val filtered = triggerFilter.filter(messages) // RCON 模块只取最新的一条消息 @@ -124,7 +124,7 @@ class RconPlayerListModule( } } } - private suspend fun processTrigger(msg: GetFriendMsgHistoryEvent.SpecificMsg) { + private suspend fun processTrigger(msg: MsgHistorySpecificMsg) { LoggerUtil.logger.info("[$name] 执行 RCON 查询") val commands = listOf("forge tps", "list") diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/StateModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/StateModule.kt index 6c52f5f..32dc7db 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/StateModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/StateModule.kt @@ -1,4 +1,27 @@ package top.r3944realms.ltdmanager.module -class StateModule { +import kotlinx.coroutines.* +import top.r3944realms.ltdmanager.napcat.request.account.SetQQProfileRequest + +//TODO: 有问题不要使用 #unload得考虑下怎么写 +class StateModule( + moduleName: String, + private val onlineName: String, + private val offlineName: String, +): BaseModule("StateModule", moduleName) { + private var scope: CoroutineScope? = null + override fun onLoad() { + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope!!.launch { + if (loaded) updateProfile(onlineName) + } + } + private suspend fun updateProfile(name: String) { + napCatClient.sendUrgentUnit(SetQQProfileRequest(name)) + } + + override suspend fun onUnload() { + updateProfile(offlineName) + scope!!.cancel() + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/AdvancedCommandParser.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/AdvancedCommandParser.kt index 4a0dea0..a8a3d21 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/AdvancedCommandParser.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/AdvancedCommandParser.kt @@ -1,4 +1,342 @@ package top.r3944realms.ltdmanager.module.common +/** + * 高级命令解析器 + * 支持自定义参数语法和参数验证 + */ class AdvancedCommandParser { -} \ No newline at end of file + private val commands = mutableListOf() + + /** + * 命令定义类 + */ + data class CommandDefinition( + val name: String, + val aliases: List = emptyList(), + val syntax: String = "", + val description: String = "", + val parameterPattern: Regex = DEFAULT_PARAMETER_PATTERN + ) { + val allCommandForms: List get() = listOf(name) + aliases + } + + /** + * 解析结果 + */ + data class ParseResult( + val command: String, + val arguments: Map = emptyMap(), + val rawArguments: List = emptyList(), + val isValid: Boolean = true, + val errorMessage: String? = null, + val commandDefinition: CommandDefinition? = null + ) + + companion object { + // 默认参数模式:<参数名> 或 [可选参数名] + val DEFAULT_PARAMETER_PATTERN = Regex("""<(\w+)>|\[(\w+)]""") + + // 常用参数模式 + /** + * 必需参数 + */ + val ANGLE_BRACKETS = Regex("""<(\w+)>""") // + + /** + * 可选参数 + */ + val SQUARE_BRACKETS = Regex("""\[(\w+)]""") // [param] + + /** + * 自定义参数类型 + */ + val CURLY_BRACES = Regex("""\{(\w+)}""") // {param} + } + + /** + * 注册命令 + */ + fun registerCommand( + name: String, + aliases: List = emptyList(), + syntax: String = "", + description: String = "", + parameterPattern: Regex = DEFAULT_PARAMETER_PATTERN + ): AdvancedCommandParser { + commands.add(CommandDefinition(name, aliases, syntax, description, parameterPattern)) + return this + } + + /** + * 批量注册命令 + */ + fun registerCommands(vararg commandDefs: CommandDefinition): AdvancedCommandParser { + commands.addAll(commandDefs) + return this + } + /** + * 智能分割参数,正确处理引号内的空格 + */ + private fun smartSplit(input: String): List { + val result = mutableListOf() + val current = StringBuilder() + var inQuotes = false + var quoteChar: Char? = null + var escapeNext = false + + for (char in input) { + when { + escapeNext -> { + current.append(char) + escapeNext = false + } + char == '\\' -> { + escapeNext = true + } + char == '"' || char == '\'' -> { + if (inQuotes && char == quoteChar) { + // 结束引号 + inQuotes = false + quoteChar = null + } else if (!inQuotes) { + // 开始引号 + inQuotes = true + quoteChar = char + } else { + current.append(char) + } + } + char == ' ' && !inQuotes -> { + // 空格分隔,但不是引号内 + if (current.isNotEmpty()) { + result.add(current.toString()) + current.clear() + } + } + else -> { + current.append(char) + } + } + } + + if (current.isNotEmpty()) { + result.add(current.toString()) + } + + return result + } + /** + * 解析命令 + */ + private fun parse(input: String): ParseResult { + val trimmedInput = input.trim() + if (trimmedInput.isEmpty()) { + return ParseResult("", isValid = false, errorMessage = "输入为空") + } + + // 分割命令和参数 + val parts = smartSplit(trimmedInput) + val commandPart = parts[0] + + // 查找匹配的命令定义 + val commandDef = commands.find { def -> + def.allCommandForms.any { it.equals(commandPart, ignoreCase = true) } + } + + if (commandDef == null) { + return ParseResult( + commandPart, + isValid = false, + errorMessage = "未知命令: $commandPart" + ) + } + + // 解析参数 + val arguments = parseArguments(commandDef, parts.drop(1)) + val rawArgs = parts.drop(1) + + return ParseResult( + command = commandDef.name, + arguments = arguments, + rawArguments = rawArgs, + commandDefinition = commandDef + ) + } + + /** + * 解析参数 + */ + private fun parseArguments(commandDef: CommandDefinition, args: List): Map { + val parameters = extractParameterNames(commandDef.syntax, commandDef.parameterPattern) + val result = mutableMapOf() + + if (parameters.isEmpty()) { + args.forEachIndexed { index, value -> result["arg${index + 1}"] = value } + return result + } + + val positionals = mutableListOf() + val namedParams = mutableMapOf() + var i = 0 + + // 第一遍:处理命名参数 + while (i < args.size) { + when { + args[i].startsWith("--") -> { + val paramName = args[i].substring(2) + if (paramName in parameters) { + if (i + 1 < args.size && !args[i + 1].startsWith("-")) { + namedParams[paramName] = args[i + 1] + i += 2 + } else { + namedParams[paramName] = "true" + i += 1 + } + } else { + positionals.add(args[i]) + i += 1 + } + } + args[i].startsWith("-") && args[i].length > 1 && !args[i].startsWith("--") -> { + val paramName = args[i].substring(1) + if (paramName in parameters) { + if (i + 1 < args.size && !args[i + 1].startsWith("-")) { + namedParams[paramName] = args[i + 1] + i += 2 + } else { + namedParams[paramName] = "true" + i += 1 + } + } else { + positionals.add(args[i]) + i += 1 + } + } + else -> { + positionals.add(args[i]) + i += 1 + } + } + } + + // 第二遍:映射位置参数 + var posIndex = 0 + for (paramName in parameters) { + if (paramName !in namedParams && posIndex < positionals.size) { + result[paramName] = positionals[posIndex] + posIndex++ + } else if (paramName in namedParams) { + result[paramName] = namedParams[paramName]!! + } + } + + // 处理额外参数 + for (j in posIndex until positionals.size) { + result["extraArg${j - posIndex + 1}"] = positionals[j] + } + + return result + } + + /** + * 从语法字符串中提取参数名 + */ + private fun extractParameterNames(syntax: String, pattern: Regex): List { + if (syntax.isEmpty()) return emptyList() + + return pattern.findAll(syntax).map { matchResult -> + matchResult.groupValues[1].ifEmpty { matchResult.groupValues[2] } + }.toList() + } + + /** + * 验证参数是否符合要求 + */ + fun validateArguments(result: ParseResult): ParseResult { + if (!result.isValid) return result + + val commandDef = result.commandDefinition ?: return result.copy( + isValid = false, + errorMessage = "命令定义不存在" + ) + + val requiredParams = extractParameterNames(commandDef.syntax, ANGLE_BRACKETS) + val missingParams = requiredParams.filter { it !in result.arguments } + + return if (missingParams.isNotEmpty()) { + result.copy( + isValid = false, + errorMessage = "缺少必需参数: ${missingParams.joinToString()}" + ) + } else { + result + } + } + + /** + * 获取命令的帮助信息(增强版) + */ + fun getCommandHelp(commandName: String): String? { + val commandDef = commands.find { it.name == commandName || commandName in it.aliases } + return commandDef?.let { def -> + buildString { + appendLine("命令: ${def.name}") + if (def.aliases.isNotEmpty()) { + appendLine("别名: ${def.aliases.joinToString()}") + } + appendLine("用法: ${def.name} ${def.syntax}") + appendLine("描述: ${def.description}") + + // 显示参数说明 + val params = extractParameterNames(def.syntax, def.parameterPattern) + if (params.isNotEmpty()) { + appendLine("参数:") + params.forEach { param -> + val isRequired = def.syntax.contains("<$param>") + appendLine(" ${if (isRequired) "<$param>" else "[$param]"} - ${if (isRequired) "必需" else "可选"}") + } + } + } + } + } + /** + * 获取所有注册的命令 + */ + fun getRegisteredCommands(): List = commands.toList() + /** + * 获取所有命令的帮助信息 + */ + fun getAllCommandsHelp(): String { + return buildString { + appendLine("可用命令:") + appendLine("=".repeat(10)) + commands.forEach { def -> + appendLine("${def.name} - ${def.description}") + if (def.aliases.isNotEmpty()) { + appendLine(" 别名: ${def.aliases.joinToString()}") + } + appendLine(" 用法: ${def.name} ${def.syntax}") + appendLine() + } + } + } + /** + * 检查输入是否包含有效命令 + */ + fun containsCommand(input: String): Boolean { + val trimmedInput = input.trim() + if (trimmedInput.isEmpty()) return false + + val commandPart = trimmedInput.split("\\s+".toRegex())[0] + return commands.any { def -> + def.allCommandForms.any { it.equals(commandPart, ignoreCase = true) } + } + } + + /** + * 快速解析(包含验证) + */ + fun parseAndValidate(input: String): ParseResult { + return validateArguments(parse(input)) + } +} diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/MessageFilter.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/MessageFilter.kt index c9d11a6..08f8dd6 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/MessageFilter.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/MessageFilter.kt @@ -1,7 +1,7 @@ package top.r3944realms.ltdmanager.module.common.filter -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg interface MessageFilter { - suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean + suspend fun test(msg: MsgHistorySpecificMsg): Boolean } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/TriggerMessageFilter.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/TriggerMessageFilter.kt index 394461b..abef93b 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/TriggerMessageFilter.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/TriggerMessageFilter.kt @@ -1,12 +1,12 @@ package top.r3944realms.ltdmanager.module.common.filter -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg class TriggerMessageFilter(private val filters: List) { - suspend fun filter(messages: List) - : List { + suspend fun filter(messages: List) + : List { - val result = mutableListOf() + val result = mutableListOf() for (msg in messages) { if (filters.all { it.test(msg) }) { result.add(msg) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/AdvancedCommonFilter.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/AdvancedCommonFilter.kt index 0763092..80860a9 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/AdvancedCommonFilter.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/AdvancedCommonFilter.kt @@ -1,4 +1,17 @@ package top.r3944realms.ltdmanager.module.common.filter.type -class AdvancedCommonFilter { +import top.r3944realms.ltdmanager.module.common.AdvancedCommandParser +import top.r3944realms.ltdmanager.module.common.filter.MessageFilter +import top.r3944realms.ltdmanager.napcat.data.MessageType +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg + +class AdvancedCommonFilter(private val advancedCommandParser: AdvancedCommandParser): MessageFilter { + override suspend fun test(msg: MsgHistorySpecificMsg): Boolean { + return msg.message.any { seg -> + seg.type == MessageType.Text && seg.data.text?.let { text -> + advancedCommandParser.getRegisteredCommands().map { it.name }.any { name -> text.startsWith(name) } + } == true + } + + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/CommandFilter.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/CommandFilter.kt index 738d712..1b9b0ed 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/CommandFilter.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/CommandFilter.kt @@ -3,11 +3,11 @@ package top.r3944realms.ltdmanager.module.common.filter.type import top.r3944realms.ltdmanager.module.common.CommandParser import top.r3944realms.ltdmanager.module.common.filter.MessageFilter import top.r3944realms.ltdmanager.napcat.data.MessageType -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg /** 命令解析器匹配 */ class CommandFilter(private val parser: CommandParser) : MessageFilter { - override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean { + override suspend fun test(msg: MsgHistorySpecificMsg): Boolean { return msg.message.any { seg -> seg.type == MessageType.Text && seg.data.text?.let { parser.containsCommand(it) } == true } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/CooldownFilter.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/CooldownFilter.kt index 0244489..c9480ba 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/CooldownFilter.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/CooldownFilter.kt @@ -2,14 +2,14 @@ package top.r3944realms.ltdmanager.module.common.filter.type import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager import top.r3944realms.ltdmanager.module.common.filter.MessageFilter -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg class CooldownFilter( private val cooldownManager: CooldownManager<*>, - private val sendCooldown: suspend (GetFriendMsgHistoryEvent.SpecificMsg, Long) -> Unit + private val sendCooldown: suspend (MsgHistorySpecificMsg, Long) -> Unit ) : MessageFilter { - override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean { + override suspend fun test(msg: MsgHistorySpecificMsg): Boolean { val result = cooldownManager.checkAndHandle(msg.userId, msg.realId) if (!result.allowed && result.notify) { sendCooldown(msg, result.remaining) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/IgnoreSelfFilter.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/IgnoreSelfFilter.kt index 60b49ec..a12c89b 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/IgnoreSelfFilter.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/IgnoreSelfFilter.kt @@ -1,11 +1,11 @@ package top.r3944realms.ltdmanager.module.common.filter.type import top.r3944realms.ltdmanager.module.common.filter.MessageFilter -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg /** 忽略机器人自己的消息 */ class IgnoreSelfFilter(private val selfId: Long) : MessageFilter { - override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean { + override suspend fun test(msg: MsgHistorySpecificMsg): Boolean { return msg.userId != selfId } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/KeywordFilter.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/KeywordFilter.kt index a4a39c2..5c953d0 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/KeywordFilter.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/KeywordFilter.kt @@ -2,11 +2,11 @@ package top.r3944realms.ltdmanager.module.common.filter.type import top.r3944realms.ltdmanager.module.common.filter.MessageFilter import top.r3944realms.ltdmanager.napcat.data.MessageType -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg /** 文本关键词匹配 */ class KeywordFilter(private val keywords: Set) : MessageFilter { - override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean { + override suspend fun test(msg: MsgHistorySpecificMsg): Boolean { return msg.message.any { seg -> seg.type == MessageType.Text && seg.data.text?.let { text -> keywords.any { keyword -> text.startsWith(keyword) } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/MultiCommandFilter.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/MultiCommandFilter.kt index c049489..d23eba3 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/MultiCommandFilter.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/MultiCommandFilter.kt @@ -3,11 +3,11 @@ package top.r3944realms.ltdmanager.module.common.filter.type import top.r3944realms.ltdmanager.module.common.CommandParser import top.r3944realms.ltdmanager.module.common.filter.MessageFilter import top.r3944realms.ltdmanager.napcat.data.MessageType -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg /** 多命令解析器匹配 */ class MultiCommandFilter(private val parsers: List) : MessageFilter { - override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean { + override suspend fun test(msg: MsgHistorySpecificMsg): Boolean { return msg.message.any { seg -> seg.type == MessageType.Text && seg.data.text?.let { text -> parsers.any { parser -> parser.containsCommand(text) } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/NewMessageFilter.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/NewMessageFilter.kt index 136dfbc..bbd4003 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/NewMessageFilter.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/common/filter/type/NewMessageFilter.kt @@ -1,7 +1,7 @@ package top.r3944realms.ltdmanager.module.common.filter.type import top.r3944realms.ltdmanager.module.common.filter.MessageFilter -import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg import top.r3944realms.ltdmanager.utils.Environment import top.r3944realms.ltdmanager.utils.LoggerUtil @@ -9,7 +9,7 @@ import top.r3944realms.ltdmanager.utils.LoggerUtil class NewMessageFilter( private val getLastTrigger: (Long) -> Pair // (time, realId) ) : MessageFilter { - override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean { + override suspend fun test(msg: MsgHistorySpecificMsg): Boolean { val (lastTime, lastRealId) = getLastTrigger(msg.userId) val result = msg.time > lastTime || (msg.time == lastTime && msg.realId > lastRealId) if (Environment.isDevelopment()) LoggerUtil.logger.debug("NewMessageFilter: msg.time=${msg.time}, msg.realId=${msg.realId}, lastTime=$lastTime, lastRealId=$lastRealId, result=$result") diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/GroupMember.kt b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/GroupMember.kt index 13b39f8..5d78019 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/GroupMember.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/GroupMember.kt @@ -1,4 +1,35 @@ package top.r3944realms.ltdmanager.napcat.data -class GroupMember { -} \ No newline at end of file +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GroupMember( + @SerialName("group_id") + val groupId: Long, + @SerialName("user_id") + val userId: Long, + val nickname: String, + val card: String, + val sex: String, + val age: Int, + val area: String, + val level: String, + @SerialName("qq_level") + val qqLevel: Int, + @SerialName("join_time") + val joinTime: Long, + @SerialName("last_sent_time") + val lastSentTime: Long, + @SerialName("title_expire_time") + val titleExpireTime: Long, + val unfriendly: Boolean, + @SerialName("card_changeable") + val cardChangeable: Boolean, + @SerialName("is_robot") + val isRobot: Boolean, + @SerialName("shut_up_timestamp") + val shutUpTimestamp: Long, + val role: String, + val title: String +) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistoryContent.kt b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistoryContent.kt index 2cc76b5..0598bdf 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistoryContent.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistoryContent.kt @@ -1,4 +1,14 @@ +@file:Suppress("EXTERNAL_SERIALIZER_USELESS") + package top.r3944realms.ltdmanager.napcat.data.msghistory -class MsgHistoryContent { +import kotlinx.serialization.Serializable +import top.r3944realms.ltdmanager.napcat.serializer.MsgHistoryContentSerializer + +@Serializable(with = MsgHistoryContentSerializer::class) +sealed class MsgHistoryContent { + @Serializable + class StringValue(val value: String) : MsgHistoryContent() + @Serializable + class SpecificMsgList(val value: List) : MsgHistoryContent() } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistoryMessage.kt b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistoryMessage.kt index 4ee9600..cfe95a7 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistoryMessage.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistoryMessage.kt @@ -1,4 +1,33 @@ package top.r3944realms.ltdmanager.napcat.data.msghistory -class MsgHistoryMessage { -} \ No newline at end of file +import kotlinx.serialization.Serializable +import top.r3944realms.ltdmanager.napcat.data.MessageType + +/** + * 文本消息 + * + * 艾特消息 + * + * 表情消息 + * + * 图片消息 + * + * 文件消息 + * + * 回复消息 + * + * JSON消息 + * + * 语音消息 + * + * 视频消息 + * + * markdown消息 + * + * 消息forward + */ +@Serializable +data class MsgHistoryMessage ( + val data: MsgHistoryMessageData, + val type: MessageType +) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistorySpecificMsg.kt b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistorySpecificMsg.kt index b915db2..389032e 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistorySpecificMsg.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/data/msghistory/MsgHistorySpecificMsg.kt @@ -1,4 +1,55 @@ package top.r3944realms.ltdmanager.napcat.data.msghistory -class MsgHistorySpecificMsg { -} \ No newline at end of file +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import top.r3944realms.ltdmanager.napcat.data.Sender + +/** + * 消息详情 + */ +@Serializable +data class MsgHistorySpecificMsg ( + val font: Long, + + @SerialName("group_id") + val groupId: Long? = null, + + val message: List, + + @SerialName("message_format") + val messageFormat: String, + + @SerialName("message_id") + val messageId: Long, + + @SerialName("message_seq") + val messageSeq: Long, + + @SerialName("message_type") + val messageType: String, + + @SerialName("post_type") + val postType: String, + + @SerialName("raw_message") + val rawMessage: String, + + @SerialName("real_id") + val realId: Long, + + @SerialName("real_seq") + val realSeq: String, + + @SerialName("self_id") + val selfId: Long, + + val sender: Sender, + + @SerialName("sub_type") + val subType: String, + + val time: Long, + + @SerialName("user_id") + val userId: Long +) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/group/GetGroupMemberListEvent.kt b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/group/GetGroupMemberListEvent.kt index c32f1ea..099a0a4 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/group/GetGroupMemberListEvent.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/group/GetGroupMemberListEvent.kt @@ -3,7 +3,7 @@ package top.r3944realms.ltdmanager.napcat.event.group import kotlinx.serialization.Serializable import kotlinx.serialization.Transient -import kotlinx.serialization.json.JsonArray +import top.r3944realms.ltdmanager.napcat.data.GroupMember /** * GetGroupMemberList事件 @@ -22,7 +22,7 @@ data class GetGroupMemberListEvent( @Transient val echo0: String? = null, - val data: JsonArray + val data: List ) : AbstractGroupEvent(status0, retcode0, message0, wording0, echo0) { override fun subtype(): String { diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/message/GetFriendMsgHistoryEvent.kt b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/message/GetFriendMsgHistoryEvent.kt index 94d6090..da4b5c8 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/message/GetFriendMsgHistoryEvent.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/message/GetFriendMsgHistoryEvent.kt @@ -1,12 +1,9 @@ package top.r3944realms.ltdmanager.napcat.event.message -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient -import top.r3944realms.ltdmanager.napcat.data.ID -import top.r3944realms.ltdmanager.napcat.data.MessageType -import top.r3944realms.ltdmanager.napcat.data.Sender +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg import top.r3944realms.ltdmanager.napcat.event.group.AbstractGroupEvent /** @@ -30,106 +27,9 @@ data class GetFriendMsgHistoryEvent( ) : AbstractGroupEvent(status0, retcode0, message0, wording0, echo0) { @Serializable data class Data ( - val messages: List + val messages: List ) - @Serializable - sealed class Content { - class StringValue(val value: String) : Content() - class SpecificMsgList(val value: List) : Content() - } - @Serializable - data class MessageData ( - val text: String? = null, - val name: String? = null, - val qq: ID? = null, - val id: ID? = null, - val file: String? = null, - /** - * 外显 - */ - val summary: String? = null, - - val data: String? = null, - val content: Content? = null - ) - /** - * 文本消息 - * - * 艾特消息 - * - * 表情消息 - * - * 图片消息 - * - * 文件消息 - * - * 回复消息 - * - * JSON消息 - * - * 语音消息 - * - * 视频消息 - * - * markdown消息 - * - * 消息forward - */ - @Serializable - data class Message ( - val data: MessageData, - val type: MessageType - ) - /** - * 消息详情 - */ - @Serializable - data class SpecificMsg ( - val font: Long, - - @SerialName("group_id") - val groupId: Long? = null, - - val message: List, - - @SerialName("message_format") - val messageFormat: String, - - @SerialName("message_id") - val messageId: Long, - - @SerialName("message_seq") - val messageSeq: Long, - - @SerialName("message_type") - val messageType: String, - - @SerialName("post_type") - val postType: String, - - @SerialName("raw_message") - val rawMessage: String, - - @SerialName("real_id") - val realId: Long, - - @SerialName("real_seq") - val realSeq: String, - - @SerialName("self_id") - val selfId: Long, - - val sender: Sender, - - @SerialName("sub_type") - val subType: String, - - val time: Long, - - @SerialName("user_id") - val userId: Long - ) override fun subtype(): String { return "get_friend_msg_history" } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/message/GetGroupMsgHistoryEvent.kt b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/message/GetGroupMsgHistoryEvent.kt index a7d2411..56c01af 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/message/GetGroupMsgHistoryEvent.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/event/message/GetGroupMsgHistoryEvent.kt @@ -3,6 +3,7 @@ package top.r3944realms.ltdmanager.napcat.event.message import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg import top.r3944realms.ltdmanager.napcat.event.group.AbstractGroupEvent /** @@ -22,9 +23,13 @@ data class GetGroupMsgHistoryEvent( @Transient val echo0: String? = null, - val data: GetFriendMsgHistoryEvent.Data + val data: Data ) : AbstractGroupEvent(status0, retcode0, message0, wording0, echo0) { - + + @Serializable + data class Data ( + val messages: List + ) override fun subtype(): String { return "get_group_msg_history" } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/request/group/GetGroupMemberListRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/request/group/GetGroupMemberListRequest.kt index 1d74431..7d733b0 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/request/group/GetGroupMemberListRequest.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/request/group/GetGroupMemberListRequest.kt @@ -18,8 +18,6 @@ data class GetGroupMemberListRequest( @SerialName("no_cache") val noCache: Boolean, - @SerialName("user_id") - val userId: ID ) : AbstractGroupRequest() { override fun toJSON(): String = Json.encodeToString(this) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/serializer/MsgHistoryContentSerializer.kt b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/serializer/MsgHistoryContentSerializer.kt index 4e2fba1..649d507 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/napcat/serializer/MsgHistoryContentSerializer.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/napcat/serializer/MsgHistoryContentSerializer.kt @@ -1,4 +1,68 @@ package top.r3944realms.ltdmanager.napcat.serializer -object MsgHistoryContentSerializer { +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.* +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistoryContent +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg + +object MsgHistoryContentSerializer : KSerializer { + + // 创建宽松的 JSON 配置 + private val json = Json { + ignoreUnknownKeys = true + coerceInputValues = true + isLenient = true + } + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("MsgHistoryContent") + + override fun serialize(encoder: Encoder, value: MsgHistoryContent) { + val jsonEncoder = encoder as? JsonEncoder + ?: throw SerializationException("只能使用JSON编码器") + + when (value) { + is MsgHistoryContent.SpecificMsgList -> { + val jsonArray = JsonArray(value.value.map { specificMsg -> + json.encodeToJsonElement(specificMsg) + }) + jsonEncoder.encodeJsonElement(jsonArray) + } + is MsgHistoryContent.StringValue -> { + jsonEncoder.encodeJsonElement(JsonPrimitive(value.value)) + } + } + } + + override fun deserialize(decoder: Decoder): MsgHistoryContent { + val jsonDecoder = decoder as? JsonDecoder + ?: throw SerializationException("只能使用JSON解码器") + + return when (val jsonElement = jsonDecoder.decodeJsonElement()) { + is JsonArray -> { + try { + val specificMsgList = jsonElement.map { element -> + json.decodeFromJsonElement(element) + } + MsgHistoryContent.SpecificMsgList(specificMsgList) + } catch (e: Exception) { + throw SerializationException("无法将JsonArray解析为List: ${e.message}") + } + } + is JsonPrimitive -> { + if (jsonElement.isString) { + MsgHistoryContent.StringValue(jsonElement.content) + } else { + throw SerializationException("不支持的非字符串原始类型") + } + } + else -> { + throw SerializationException("不支持的JSON元素类型: ${jsonElement::class.simpleName}") + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/utils/QRCodeUtil.kt b/src/main/kotlin/top/r3944realms/ltdmanager/utils/QRCodeUtil.kt index f77ee74..5c3c610 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/utils/QRCodeUtil.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/utils/QRCodeUtil.kt @@ -57,4 +57,24 @@ object QRCodeUtil { } return image } + @Throws(IOException::class, WriterException::class) + fun generateQRCodeToFile( + text: String, + width: Int, + height: Int, + filePath: String + ) { + val hints: MutableMap = EnumMap(EncodeHintType::class.java) + hints[EncodeHintType.CHARACTER_SET] = CHARSET + + // 生成二维码矩阵 + val bitMatrix = MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints) + + // 转成 BufferedImage + val image = toBufferedImage(bitMatrix) + + // 保存到文件 + val outputFile = java.io.File(filePath) + ImageIO.write(image, FORMAT, outputFile) + } } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 10c0551..106c0ff 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -101,3 +101,6 @@ dg-lab: enable-debug: false ide-host: "127.0.0.1" ide-port: 5678 +img-tu: + url: https://mysite.com/api/1/upload + encrypted-password: 11223344bbcc diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/command/Mojang.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/command/Mojang.kt index ffb70cf..40a3b9e 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/command/Mojang.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/command/Mojang.kt @@ -1,2 +1,34 @@ package top.r394realms.ltdmanagertest.command +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.arguments.IntegerArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal +import com.mojang.brigadier.builder.RequiredArgumentBuilder.argument + +fun main() { + val dispatcher = CommandDispatcher() // String 表示消息来源 + + dispatcher.register( + literal("say") + .then(argument("message", StringArgumentType.greedyString()) + .executes { ctx -> + val msg = StringArgumentType.getString(ctx, "message") + println("[BOT] $msg") + 1 + }) + ) + dispatcher.register( + literal("add") + .then(argument("a", IntegerArgumentType.integer()) + .then(argument("b", IntegerArgumentType.integer()) + .executes { ctx -> + val a = IntegerArgumentType.getInteger(ctx, "a") + val b = IntegerArgumentType.getInteger(ctx, "b") + println("[BOT] $a + $b = ${a + b}") + 1 + })) + ) + dispatcher.execute("say Hello World", "user123") + dispatcher.execute("add 3 7", "user123") +} \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/command/ParameterExtractionDemo.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/command/ParameterExtractionDemo.kt index 649aa0e..9a0d446 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/command/ParameterExtractionDemo.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/command/ParameterExtractionDemo.kt @@ -1,4 +1,197 @@ package top.r394realms.ltdmanagertest.command +import top.r3944realms.ltdmanager.module.common.AdvancedCommandParser + +/** + * 参数提取演示类 + */ class ParameterExtractionDemo { + + companion object { + // 默认参数模式:<参数名> 或 [可选参数名] + val DEFAULT_PARAMETER_PATTERN = Regex("""<(\w+)>|\[(\w+)]""") + + // 常用参数模式 + val ANGLE_BRACKETS = Regex("""<(\w+)>""") // - 必需参数 + val SQUARE_BRACKETS = Regex("""\[(\w+)]""") // [param] - 可选参数 + val CURLY_BRACES = Regex("""\{(\w+)}""") // {param} - 自定义参数 + } + + /** + * 从语法字符串中提取参数名 + */ + fun extractParameterNames(syntax: String, pattern: Regex): List { + if (syntax.isEmpty()) return emptyList() + + return pattern.findAll(syntax).map { matchResult -> + // 从捕获组中提取参数名(处理不同的括号类型) + matchResult.groupValues[1].ifEmpty { matchResult.groupValues[2] } + }.toList() + } + + /** + * 演示场景1:只需要必需参数(参数验证) + */ + fun demoRequiredParameters() { + println("=== 场景1:必需参数验证 ===") + + val syntax = "send [target] [priority]" + + val parser = AdvancedCommandParser().apply { + registerCommand( + "ls", + syntax = syntax, + parameterPattern = ANGLE_BRACKETS, + ) + } + println(parser.getCommandHelp("ls")) + + // 模拟用户输入验证 + val testCases = listOf( + "ls send Hello", // 有效:提供了必需参数 + "ls send Hello @all", // 有效:提供了必需参数和可选参数 + "ls send" // 无效:缺少必需参数 + ) + + testCases.forEach { input -> + println("输入: $input") + val result = parser.parseAndValidate(input) + if (result.isValid) { + println("✓ 命令: ${result.command}") + println("✓ 参数:") + result.arguments.forEach { (key, value) -> + println(" $key: $value") + } + } else { + println("✗ 错误: ${result.errorMessage}") + } + println("-".repeat(50)) + } + println() + } + + /** + * 演示场景2:需要所有参数(完整解析) + */ + fun demoAllParameters() { + println("=== 场景2:完整参数解析 ===") + + val syntax = "user [name] [age] [email]" + val parser = AdvancedCommandParser().apply { + registerCommand( + "ls", + syntax = syntax, + ) + } + println(parser.getCommandHelp("ls")) + + // 模拟参数映射 + val testInput = "ls user add 123 John 30 john@example.com" + + println("输入: $testInput") + val result = parser.parseAndValidate(testInput) + if (result.isValid) { + println("✓ 命令: ${result.command}") + println("✓ 参数:") + result.arguments.forEach { (key, value) -> + println(" $key: $value") + } + } else { + println("✗ 错误: ${result.errorMessage}") + } + println("-".repeat(50)) + } + + /** + * 演示场景3:自定义参数格式 + */ + fun demoCustomParameters() { + println("=== 场景3:自定义参数格式 ===") + + val customSyntax = "execute {command} {args} --timeout {timeout} --retry {retries}" + extractParameterNames(customSyntax, CURLY_BRACES) + val parser = AdvancedCommandParser().apply { + registerCommand( + "ls", + syntax = customSyntax, + parameterPattern = CURLY_BRACES, + ) + } + println(parser.getCommandHelp("ls")) + + // 模拟命名参数解析 + val testInput = "ls execute {ls -la} {--help} --timeout 30 --retry 3" + val result = parser.parseAndValidate(testInput) + if (result.isValid) { + println("✓ 命令: ${result.command}") + println("✓ 参数:") + result.arguments.forEach { (key, value) -> + println(" $key: $value") + } + } else { + println("✗ 错误: ${result.errorMessage}") + } + println("-".repeat(5)) + + } + + /** + * 综合演示:完整的命令处理流程 + */ + fun demoCompleteWorkflow() { + println("=== 综合演示:完整工作流程 ===") + + // 定义复杂的命令语法 + val syntax = "ls database [where] [limit] [offset] --format {format}" + val parser1 = AdvancedCommandParser().apply { + registerCommand( + "ls", + syntax = syntax, + parameterPattern = DEFAULT_PARAMETER_PATTERN, + ) + } + + // 模拟真实命令处理 + val testCommand = "ls database select users --where \"age > 18\" --limit 10 --format json" + val result = parser1.parseAndValidate(testCommand) + if (result.isValid) { + println("✓ 命令: ${result.command}") + println("✓ 参数:") + result.arguments.forEach { (key, value) -> + println(" $key: $value") + } + } else { + println("✗ 错误: ${result.errorMessage}") + } + println("-".repeat(5)) + } +} + +/** + * 主函数运行演示 + */ +fun main() { + val demo = ParameterExtractionDemo() + + // 运行各个演示场景 + demo.demoRequiredParameters() + demo.demoAllParameters() + demo.demoCustomParameters() + demo.demoCompleteWorkflow() + + // 额外演示:不同语法模式对比 + println("\n=== 语法模式对比 ===") + val syntaxes = listOf( + "cmd [opt1] [opt2]", + "run {command} {args}", + "test [output] --mode {mode} --verbose {flag}" + ) + + syntaxes.forEach { syntax -> + println("\n语法: $syntax") + println("尖括号参数: ${demo.extractParameterNames(syntax, ParameterExtractionDemo.ANGLE_BRACKETS)}") + println("方括号参数: ${demo.extractParameterNames(syntax, ParameterExtractionDemo.SQUARE_BRACKETS)}") + println("花括号参数: ${demo.extractParameterNames(syntax, ParameterExtractionDemo.CURLY_BRACES)}") + println("所有参数: ${demo.extractParameterNames(syntax, ParameterExtractionDemo.DEFAULT_PARAMETER_PATTERN)}") + } } \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/command/testACP.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/command/testACP.kt index 567ca5f..c7e24d6 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/command/testACP.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/command/testACP.kt @@ -1,4 +1,54 @@ package top.r394realms.ltdmanagertest.command -class testACP { +import top.r3944realms.ltdmanager.module.common.AdvancedCommandParser + +fun main() { + val parser = AdvancedCommandParser().apply { + registerCommand( + name = "send", + aliases = listOf("s"), + syntax = " [target] [priority]", + description = "发送消息到指定目标" + ) + + registerCommand( + name = "user", + aliases = listOf("u"), + syntax = " [name] [email] --role {role}", + description = "用户管理命令" + ) + + registerCommand( + name = "database", + aliases = listOf("db"), + syntax = "
[where] [limit] --format {format}", + description = "数据库操作命令" + ) + } + + // 测试复杂命令 + val testCommands = listOf( + "database select users --where \"age > 18 and name = 'John Doe'\" --limit 10 --format json", + "send \"Hello, World!\" @all --priority high", + "user add 123 --email john@example.com --role admin --name \"John Smith\"", + "invalid command test" + ) + + testCommands.forEach { input -> + println("输入: $input") + val result = parser.parseAndValidate(input) + if (result.isValid) { + println("✓ 命令: ${result.command}") + println("✓ 参数:") + result.arguments.forEach { (key, value) -> + println(" $key: $value") + } + } else { + println("✗ 错误: ${result.errorMessage}") + } + println("-".repeat(50)) + } + + // 显示帮助信息 + println(parser.getCommandHelp("database") ?: "命令未找到") } \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/command/testAdvancedCommandParser.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/command/testAdvancedCommandParser.kt index ffb70cf..5f2ddf1 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/command/testAdvancedCommandParser.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/command/testAdvancedCommandParser.kt @@ -1,2 +1,48 @@ package top.r394realms.ltdmanagertest.command +import top.r3944realms.ltdmanager.module.common.AdvancedCommandParser + +fun main() { + val parser = AdvancedCommandParser().apply { + registerCommand( + name = "send", + aliases = listOf("s"), + syntax = " [target]", + description = "发送消息到指定目标" + ) + + registerCommand( + name = "user", + aliases = listOf("u"), + syntax = " [options]", + description = "用户管理命令" + ) + + registerCommand( + name = "config", + aliases = listOf("cfg"), + syntax = "set | get ", + description = "配置管理", + parameterPattern = AdvancedCommandParser.ANGLE_BRACKETS + ) + } + + // 测试解析 + val testInputs = listOf( + "send Hello World", + "user add 123 --name John", + "config set theme dark", + "invalid command" + ) + + testInputs.forEach { input -> + println("输入: $input") + val result = parser.parseAndValidate(input) + println("结果: $result") + println("---") + } + + // 获取帮助信息 + println("帮助信息:") + println(parser.getCommandHelp("send")) +} \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/help/helpTest.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/help/helpTest.kt index 1c31bd3..22e14bc 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/help/helpTest.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/help/helpTest.kt @@ -1,7 +1,7 @@ package top.r394realms.ltdmanagertest.help import top.r3944realms.ltdmanager.GlobalManager -import top.r3944realms.ltdmanager.module.BanModule +import top.r3944realms.ltdmanager.module.DGLabModule import top.r3944realms.ltdmanager.module.GroupMessagePollingModule import top.r3944realms.ltdmanager.module.HelpModule @@ -22,18 +22,18 @@ fun main() = GlobalManager.runBlockingMain { selfId = selfQQId, selfNickName = selfNickName, ) - val banModule = BanModule( + val dgLabModule = DGLabModule( moduleName = "TestGroup", groupMessagePollingModule = groupMsgPollingModule, selfId = selfQQId, - adminsId = listOf(2561098830), - muteCommandPrefixList = listOf("禁言", "口球", "mute", "Mute", "闭嘴") + adminIds = listOf(2561098830L), + commandHead = listOf("dglab") ) GlobalManager.moduleManager.registerModule(groupMsgPollingModule) GlobalManager.moduleManager.registerModule(helpModule) - GlobalManager.moduleManager.registerModule(banModule) + GlobalManager.moduleManager.registerModule(dgLabModule) GlobalManager.moduleManager.loadModule(groupMsgPollingModule.name) GlobalManager.moduleManager.loadModule(helpModule.name) - GlobalManager.moduleManager.loadModule(banModule.name) + GlobalManager.moduleManager.loadModule(dgLabModule.name) } \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/msg/NapCatMsgTest.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/msg/NapCatMsgTest.kt index 88191f8..fa60bbd 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/msg/NapCatMsgTest.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/msg/NapCatMsgTest.kt @@ -5,12 +5,19 @@ import top.r3944realms.ltdmanager.GlobalManager import top.r3944realms.ltdmanager.module.ModGroupHandlerModule import top.r3944realms.ltdmanager.napcat.NapCatClient import top.r3944realms.ltdmanager.napcat.data.ID +import top.r3944realms.ltdmanager.napcat.data.MessageElement import top.r3944realms.ltdmanager.napcat.data.MessageType import top.r3944realms.ltdmanager.napcat.request.message.SendForwardMsgRequest +import top.r3944realms.ltdmanager.napcat.request.other.SendPrivateMsgRequest fun main() = GlobalManager.runBlockingMain { val napCatClient = NapCatClient.create() - formatAndSendForwardMessage(napCatClient, 2561098830L, "幸福亮亮") +// formatAndSendForwardMessage(napCatClient, 2561098830L, "幸福亮亮") + sendTestMsg(napCatClient) +} +private suspend fun sendTestMsg(napCatClient: NapCatClient) { + val request = SendPrivateMsgRequest(listOf(MessageElement.image("https://pic.xiaobuawa.top/images/2025/09/30/icons8-postgresql-96d4af6da8d4bd8df5.png","图片")),ID.long(2561098830L)) + napCatClient.sendUnit(request) } private suspend fun formatAndSendForwardMessage(napCatClient: NapCatClient ,userId: Long, requesterNick: String) { // 虚拟数据 - 模拟有审核记录的情况 @@ -45,7 +52,7 @@ private suspend fun formatAndSendForwardMessage(napCatClient: NapCatClient ,user // 创建合并转发消息 val forwardRequest = SendForwardMsgRequest( - groupId = ID.long(339340846), + groupId = ID.long(920719236), messages = listOf( SendForwardMsgRequest.TopForwardMsg( data = SendForwardMsgRequest.MessageData( diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/msg/sendMsgTest.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/msg/sendMsgTest.kt index ffdf2ef..f2ac0f5 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/msg/sendMsgTest.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/msg/sendMsgTest.kt @@ -1,4 +1,26 @@ package top.r394realms.ltdmanagertest.msg -class sendMsgTest { +import top.r3944realms.ltdmanager.GlobalManager +import top.r3944realms.ltdmanager.napcat.data.ID +import top.r3944realms.ltdmanager.napcat.event.group.GetGroupMemberListEvent +import top.r3944realms.ltdmanager.napcat.request.group.GetGroupMemberListRequest +import top.r3944realms.ltdmanager.napcat.request.message.SetMsgEmojiLikeRequest + +fun main() = GlobalManager.runBlockingMain { +// val getGroupMemberListEvent = GlobalManager.napCatClient.send( +// GetGroupMemberListRequest( +// ID.long(920719236), +// false +// ) +// ) +// println(getGroupMemberListEvent.data.filter { !it.isRobot }.map { it.userId to it.nickname }.toMap()) + for (i in 61 ..81){ + GlobalManager.napCatClient.sendUnit( + SetMsgEmojiLikeRequest( + i.toDouble(), ID.long(2080109145), true + ) + ) + } + + } \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/test.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/test.kt index 6466e44..663ce29 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/test.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/test.kt @@ -2,20 +2,21 @@ package top.r394realms.ltdmanagertest import top.r3944realms.ltdmanager.GlobalManager import top.r3944realms.ltdmanager.module.GroupRequestHandlerModule +import top.r3944realms.ltdmanager.module.StateModule fun main() = GlobalManager.runBlockingMain { // 创建模块实例 - val groupModule = GroupRequestHandlerModule( - moduleName = "WhiteListGroup", - client = GlobalManager.napCatClient, - targetGroupId = 538751386 + val stateModule = StateModule( + moduleName = "Globe", + onlineName = "[\uD83D\uDFE2] 闲趣老土豆🥔", + offlineName = "[\uD83D\uDD34] 闲趣老土豆🥔" ) // 注册模块到全局模块管理器 - GlobalManager.moduleManager.registerModule(groupModule) + GlobalManager.moduleManager.registerModule(stateModule) // 加载模块 - GlobalManager.moduleManager.loadModule(groupModule.name) + GlobalManager.moduleManager.loadModule(stateModule.name) } \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/testRandom.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/testRandom.kt index 2caf5c4..0b3d9f8 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/testRandom.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/testRandom.kt @@ -1,4 +1,9 @@ package top.r394realms.ltdmanagertest -class testRandom { +import kotlin.random.Random + +fun main() { + for(item in 1..100){ + println(Random.nextInt(100)) + } } \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/util/ImageUploader.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/util/ImageUploader.kt index 8777ee2..5ab9772 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/util/ImageUploader.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/util/ImageUploader.kt @@ -4,7 +4,7 @@ package top.r394realms.ltdmanagertest.util import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody -import top.r3944realms.ltdmanager.GlobalManager +import okhttp3.logging.HttpLoggingInterceptor import top.r3944realms.ltdmanager.utils.LoggerUtil import java.io.File import java.io.IOException @@ -42,6 +42,7 @@ object ImageUploader { val request = Request.Builder() .url("https://pic.xiaobuawa.top/api/1/upload") .header("X-API-Key", apiKey.trim()) // 重要:去除空格 + .header("User-Agent", "OkHttp/4.12.0") // 添加 User-Agent .post(requestBody) .build() @@ -82,6 +83,7 @@ object ImageUploader { val request = Request.Builder() .url("https://pic.xiaobuawa.top/api/1/upload") .header("X-API-Key", apiKey.trim()) + .header("User-Agent", "OkHttp/4.12.0") .post(requestBody) .build() diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/util/img.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/util/img.kt index 7f78883..a15b9c8 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/util/img.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/util/img.kt @@ -1,4 +1,29 @@ package top.r394realms.ltdmanagertest.util -class img { +import top.r3944realms.ltdmanager.GlobalManager +import java.io.ByteArrayInputStream +import java.io.File + +fun main() = GlobalManager.runBlockingMain { + val client = GlobalManager.cheveretoClient; + client.use { cheveretoClient -> + // 1. 测试 File 上传 + val file = File("data/temp/icons8-postgresql-96.png") + val resp1 = cheveretoClient.uploadFile(file, title = "PostgreSQL Logo", tags = "db,icon,test") + println("File 上传结果: ${resp1.statusCode} -> ${resp1.image?.url}") + + // 2. 测试 ByteArrayInputStream 上传 + val bytes = file.readBytes() + val inputStream = ByteArrayInputStream(bytes) + val resp2 = cheveretoClient.uploadStream(inputStream, fileName = "test", title = "From Stream", description = "测试 ByteArrayInputStream 上传") + println("Stream 上传结果: ${resp2.statusCode} -> ${resp2.image?.url}") + + // 3. 测试 URL 上传 + val testUrl = "https://img.icons8.com/color/96/postgresql.png" + val resp3 = cheveretoClient.uploadUrl(testUrl) + println("URL 上传结果: ${resp3.statusCode} -> ${resp3.image?.url}") + if (resp3.statusCode == 400) { + println(resp3.statusTxt) + } + } } \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv2.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv2.kt index a254a9b..b2129b1 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv2.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv2.kt @@ -1,2 +1,82 @@ package top.r394realms.ltdmanagertest.util +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json +import top.r3944realms.ltdmanager.GlobalManager +import top.r3944realms.ltdmanager.chevereto.data.CheveretoResponse +import java.io.File +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +@OptIn(ExperimentalEncodingApi::class) +fun main() = GlobalManager.runBlockingMain { + val client = HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } + } + + val filePath = "./data/temp/icons8-postgresql-96.png" + val file = File(filePath) + if (!file.exists()) { + println("文件不存在: ${file.absolutePath}") + return@runBlockingMain + } + + val apiKey = "XXXX" + + try { + // 构建 multipart/form-data + val formDataContent = formData { + append("source", file.readBytes(), Headers.build { + append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"") + append(HttpHeaders.ContentType, ContentType.Image.PNG.toString()) + }) + append("format", "json") + } + + // 调试输出每个 part + formDataContent.forEach { part -> + println("Part Headers: ${part.headers}") + when (part) { + is PartData.FileItem -> println("Part File: ${part.originalFileName}, size=${part.provider()} bytes") + is PartData.FormItem -> println("Part Form: ${part.value}") + else -> println("Part Other: $part") + } + part.dispose() + } + + // 发送 POST 请求 + val response: HttpResponse = client.submitFormWithBinaryData( + url = "https://pic.xiaobuawa.top/api/1/upload", + formData = formDataContent + ) { + header ("X-API-Key", apiKey.trim()) + } + + val responseText = response.bodyAsText() + println("服务器返回原始内容:\n$responseText") + + if (response.status.isSuccess()) { + val parsed = Json { ignoreUnknownKeys = true } + .decodeFromString(CheveretoResponse.serializer(), responseText) + println("上传成功,图片 URL: ${parsed.image?.url}") + } else { + println("上传失败,HTTP 状态码: ${response.status}") + } + + } catch (e: Exception) { + println("上传过程中出现异常:") + e.printStackTrace() + } finally { + client.close() + } +} diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv3.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv3.kt index a254a9b..a9fc56c 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv3.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv3.kt @@ -1,2 +1,126 @@ package top.r394realms.ltdmanagertest.util +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.utils.io.core.* +import top.r3944realms.ltdmanager.GlobalManager +import top.r3944realms.ltdmanager.utils.LoggerUtil +import java.io.File +import kotlin.io.use + +suspend fun uploadImageWithKtor(filePath: String, apiKey: String): String { + val client = HttpClient(CIO) { + // 添加引擎配置 + engine { + // 增加超时设置 + requestTimeout = 60000 + } + // 添加日志拦截器来调试 + expectSuccess = false // 不自动抛出异常,让我们自己处理 + } + + return client.use { httpClient -> + try { + val file = File(filePath) + + // 检查文件是否存在 + if (!file.exists()) { + throw Exception("文件不存在: $filePath") + } + + LoggerUtil.logger.info("开始上传文件: ${file.name}, 大小: ${file.length()} bytes") + + val response = httpClient.post("https://pic.xiaobuawa.top/api/1/upload") { + // 设置头信息 + headers { + append("X-API-Key", apiKey.trim()) // 去除前后空格 + append("User-Agent", "Mozilla/5.0 (compatible; MyApp/1.0)") + } + + // 使用正确的 multipart 格式 + setBody(MultiPartFormDataContent( + formData { + // 使用 appendInput 而不是 append,更接近 curl 的行为 + appendInput( + "source", + Headers.build { + append(HttpHeaders.ContentType, "image/png") + append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"") + } + ) { + buildPacket { + writeFully(file.readBytes()) + } + } + append("format", "json") + } + )) + } + + val statusCode = response.status.value + val responseText = response.bodyAsText() + + LoggerUtil.logger.info("响应状态码: $statusCode") + LoggerUtil.logger.info("响应内容: $responseText") + + if (statusCode != 200) { + throw Exception("上传失败,状态码: $statusCode, 响应: $responseText") + } + + return@use responseText + } catch (e: Exception) { + LoggerUtil.logger.error("上传过程中发生错误: ${e.message}", e) + throw e + } + } +} + +// 或者使用另一种更简单的方法 +suspend fun uploadImageWithKtorSimple(filePath: String, apiKey: String): String { + val client = HttpClient(CIO) + + return client.use { httpClient -> + val file = File(filePath) + + val response = httpClient.submitFormWithBinaryData( + url = "https://pic.xiaobuawa.top/api/1/upload", + formData = formData { + append("source", file.readBytes(), Headers.build { + append(HttpHeaders.ContentType, "image/png") + append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"") + }) + append("format", "json") + } + ) { + header("X-API-Key", apiKey.trim()) + } + + val responseText = response.bodyAsText() + LoggerUtil.logger.info("简单方法响应: $responseText") + responseText + } +} + +fun main() = GlobalManager.runBlockingMain { + // 注意:API Key 前面不要有空格! + val apiKey = "XXXX" + val filePath = "./data/temp/icons8-postgresql-96.png" + + try { + // 先尝试简单方法 + val result = uploadImageWithKtorSimple(filePath, apiKey) + println("上传成功: $result") + } catch (e: Exception) { + println("简单方法失败,尝试详细方法: ${e.message}") + try { + val result = uploadImageWithKtor(filePath, apiKey) + println("详细方法上传成功: $result") + } catch (e2: Exception) { + println("所有方法都失败: ${e2.message}") + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv4.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv4.kt index bc291e7..0e25d27 100644 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv4.kt +++ b/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv4.kt @@ -1,4 +1,82 @@ package top.r394realms.ltdmanagertest.util -class imgv4 { +import top.r3944realms.ltdmanager.GlobalManager + +fun main() = GlobalManager.runBlockingMain { + // 测试配置 + val apiKey = "XXX" + val filePath = "./data/temp/icons8-postgresql-96.png" + + println("=== 开始测试图片上传 ===") + println("API Key: ${apiKey.take(10)}...") + println("文件路径: $filePath") + + // 测试1: 同步上传 + println("\n--- 测试同步上传 ---") + try { + val result = ImageUploader.uploadImage(filePath, apiKey) + println("✅ 同步上传成功!") + println("响应结果: ${result.take(200)}...") // 只显示前200个字符 + } catch (e: Exception) { + println("❌ 同步上传失败: ${e.message}") + e.printStackTrace() + } + + // 测试2: 异步上传 + println("\n--- 测试异步上传 ---") + ImageUploader.uploadImageAsync(filePath, apiKey) { result -> + result.onSuccess { response -> + println("✅ 异步上传成功!") + println("响应结果: ${response.take(200)}...") + }.onFailure { error -> + println("❌ 异步上传失败: ${error.message}") + error.printStackTrace() + } + } + + // 等待异步操作完成 + println("等待异步操作完成...") + Thread.sleep(10000) + println("=== 测试结束 ===") +} + +// 使用 GlobalManager 的测试版本(如果需要) +fun mainWithGlobalManager() = GlobalManager.runBlockingMain { + val apiKey = "XXXX" + val filePath = "./data/temp/icons8-postgresql-96.png" + + println("=== 使用 GlobalManager 测试图片上传 ===") + + // 测试同步上传 + try { + val result = ImageUploader.uploadImage(filePath, apiKey) + println("✅ 上传成功!") + println("响应: $result") + } catch (e: Exception) { + println("❌ 上传失败: ${e.message}") + e.printStackTrace() + } +} + +// 简单的单元测试函数 +fun testImageUpload() { + val testCases = listOf( + // (文件路径, API Key, 期望结果) + "./data/temp/icons8-postgresql-96.png" to "chv_YmZ_12a0828fd88823ad4ef16a0c551b4a10ae5ce1b3e3eb65b07d87eb30162cbc91ed520334018fce2d6ba06f9d58724cef66d30ab7f6292bd4e33ad5e0d96c6499", + "./data/temp/nonexistent.png" to "chv_YmZ_12a0828fd88823ad4ef16a0c551b4a10ae5ce1b3e3eb65b07d87eb30162cbc91ed520334018fce2d6ba06f9d58724cef66d30ab7f6292bd4e33ad5e0d96c6499", // 不存在的文件 + "./data/temp/icons8-postgresql-96.png" to "invalid_key" // 无效的 API Key + ) + + testCases.forEachIndexed { index, (filePath, apiKey) -> + println("\n测试用例 ${index + 1}:") + println("文件: $filePath") + println("API Key: ${apiKey.take(10)}...") + + try { + val result = ImageUploader.uploadImage(filePath, apiKey) + println("✅ 成功: ${result.take(100)}...") + } catch (e: Exception) { + println("❌ 失败: ${e.message}") + } + } } \ No newline at end of file