Compare commits
10 Commits
2b5af6b52c
...
81e1237635
| Author | SHA1 | Date | |
|---|---|---|---|
| 81e1237635 | |||
| 988ed191ec | |||
| 37eeaf143c | |||
| 4da8263b45 | |||
| e45f2f6272 | |||
| a0f5504404 | |||
| f95c6701e5 | |||
| 88f574eea1 | |||
| 9f83026e56 | |||
| 95e21f8b84 |
|
|
@ -1,11 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
<data-source source="LOCAL" name="@47.116.125.76" uuid="5b1b9d12-d8be-43ba-a647-9d6e467bf201">
|
<data-source source="LOCAL" name="@110.42.70.155" uuid="5b1b9d12-d8be-43ba-a647-9d6e467bf201">
|
||||||
<driver-ref>mysql.8</driver-ref>
|
<driver-ref>mysql.8</driver-ref>
|
||||||
<synchronize>true</synchronize>
|
<synchronize>true</synchronize>
|
||||||
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
||||||
<jdbc-url>jdbc:mysql://47.116.125.76:3308</jdbc-url>
|
<jdbc-url>jdbc:mysql://110.42.70.155:3306</jdbc-url>
|
||||||
<jdbc-additional-properties>
|
<jdbc-additional-properties>
|
||||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
|
|
|
||||||
12
.idea/inspectionProfiles/Project_Default.xml
Normal file
12
.idea/inspectionProfiles/Project_Default.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="customHeaders">
|
||||||
|
<set>
|
||||||
|
<option value="X-API-Key" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ASMIdeaPluginConfiguration">
|
<component name="ASMIdeaPluginConfiguration">
|
||||||
<asm skipDebug="false" skipFrames="false" skipCode="false" expandFrames="false" />
|
<asm skipDebug="false" skipFrames="false" skipCode="false" expandFrames="false" />
|
||||||
|
|
|
||||||
124
.idea/uiDesigner.xml
Normal file
124
.idea/uiDesigner.xml
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Palette2">
|
||||||
|
<group name="Swing">
|
||||||
|
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="Button" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="RadioButton" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="CheckBox" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="Label" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||||
|
<preferred-size width="200" height="200" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||||
|
<preferred-size width="200" height="200" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
|
||||||
|
<preferred-size width="-1" height="20" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
|
||||||
|
</item>
|
||||||
|
</group>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||||
|
|
||||||
|
fun k(v: String) = project.property(v) as String
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.9.23"
|
kotlin("jvm") version "1.9.23"
|
||||||
kotlin("plugin.serialization") version "1.9.23" // 添加序列化插件
|
kotlin("plugin.serialization") version "1.9.23" // 添加序列化插件
|
||||||
|
|
@ -7,8 +9,8 @@ plugins {
|
||||||
id("com.github.johnrengelman.shadow") version "8.0.0" // fat jar
|
id("com.github.johnrengelman.shadow") version "8.0.0" // fat jar
|
||||||
}
|
}
|
||||||
|
|
||||||
group = project.property("project_group") as String
|
group = k("project_group")
|
||||||
version = project.property("project_version") as String
|
version = k("project_version")
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
|
||||||
|
|
@ -22,6 +24,17 @@ repositories {
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://maven.aliyun.com/repository/gradle-plugin")
|
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/")
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
name = "LTD Maven"
|
||||||
|
url = uri("https://nexus.bot.leisuretimedock.top/repository/maven-public/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//TODO: 0872d1c0-829c-e1d7-6782-89e45c8a6b76
|
//TODO: 0872d1c0-829c-e1d7-6782-89e45c8a6b76
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -35,6 +48,10 @@ repositories {
|
||||||
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3") // 推荐使用kotlinx.serialization替代Gson
|
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3") // 推荐使用kotlinx.serialization替代Gson
|
||||||
implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
|
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-core:0.41.1")
|
||||||
implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1")
|
implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1")
|
||||||
|
|
@ -58,6 +75,19 @@ repositories {
|
||||||
|
|
||||||
// 协程
|
// 协程
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||||
|
implementation("org.apache.commons:commons-lang3:3.17.0")
|
||||||
|
implementation("com.google.guava:guava:33.3.0-jre")
|
||||||
|
|
||||||
|
//DG_Lab 依赖库导入
|
||||||
|
implementation("io.netty:netty-all:4.1.109.Final")
|
||||||
|
implementation("com.google.code.gson:gson:2.10.1")
|
||||||
|
implementation("top.r3944realms.dg_lab:Common:${k("dg_lab_version")}")
|
||||||
|
|
||||||
|
//生成 二维码
|
||||||
|
implementation("com.google.zxing:core:[3.5.3,)")
|
||||||
|
|
||||||
|
//命令解析
|
||||||
|
implementation("com.mojang:brigadier:1.2.9")
|
||||||
|
|
||||||
// 测试
|
// 测试
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@ org.gradle.downloadSources=false
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.degree_of_parallelism=16
|
org.gradle.degree_of_parallelism=16
|
||||||
project_group=top.r3944realms.ltdmanager
|
project_group=top.r3944realms.ltdmanager
|
||||||
project_version=1.3-SNAPSHOT
|
project_version=1.16-SNAPSHOT
|
||||||
|
dg_lab_version=4.4.14.18
|
||||||
|
|
|
||||||
BIN
libs/DgLab-common-4.3.13.18.jar
Normal file
BIN
libs/DgLab-common-4.3.13.18.jar
Normal file
Binary file not shown.
|
|
@ -2,7 +2,9 @@ package top.r3944realms.ltdmanager
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import top.r3944realms.ltdmanager.blessingskin.BlessingSkinClient
|
import top.r3944realms.ltdmanager.blessingskin.BlessingSkinClient
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.CheveretoClient
|
||||||
import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool
|
import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool
|
||||||
|
import top.r3944realms.ltdmanager.mcms.MCSMClient
|
||||||
import top.r3944realms.ltdmanager.mcserver.McSrvStatusClient
|
import top.r3944realms.ltdmanager.mcserver.McSrvStatusClient
|
||||||
import top.r3944realms.ltdmanager.module.ModuleManager
|
import top.r3944realms.ltdmanager.module.ModuleManager
|
||||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||||
|
|
@ -19,6 +21,10 @@ object GlobalManager {
|
||||||
MysqlHikariConnectPool()
|
MysqlHikariConnectPool()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun initApplication() {
|
||||||
|
moduleManager.loadConfig()
|
||||||
|
}
|
||||||
|
|
||||||
// NapCat 客户端
|
// NapCat 客户端
|
||||||
val napCatClient: NapCatClient by lazy {
|
val napCatClient: NapCatClient by lazy {
|
||||||
NapCatClient.create()
|
NapCatClient.create()
|
||||||
|
|
@ -29,6 +35,12 @@ object GlobalManager {
|
||||||
val blessingSkinClient: BlessingSkinClient by lazy {
|
val blessingSkinClient: BlessingSkinClient by lazy {
|
||||||
BlessingSkinClient.create()
|
BlessingSkinClient.create()
|
||||||
}
|
}
|
||||||
|
val cheveretoClient: CheveretoClient by lazy {
|
||||||
|
CheveretoClient.create()
|
||||||
|
}
|
||||||
|
val mcsmClient: MCSMClient by lazy {
|
||||||
|
MCSMClient.create()
|
||||||
|
}
|
||||||
|
|
||||||
val moduleManager: ModuleManager by lazy { ModuleManager() }
|
val moduleManager: ModuleManager by lazy { ModuleManager() }
|
||||||
|
|
||||||
|
|
@ -67,7 +79,9 @@ object GlobalManager {
|
||||||
"NapCatClient" to { napCatClient.close() },
|
"NapCatClient" to { napCatClient.close() },
|
||||||
"McSrvStatusClient" to { mcSrvStatusClient.close() },
|
"McSrvStatusClient" to { mcSrvStatusClient.close() },
|
||||||
"BlessingSkinClient" to { blessingSkinClient.close() },
|
"BlessingSkinClient" to { blessingSkinClient.close() },
|
||||||
"Hikari 数据源" to { dataSource.close() }
|
"Hikari 数据源" to { dataSource.close() },
|
||||||
|
"CheveretoClient" to { cheveretoClient.close() },
|
||||||
|
"McsmClient" to { mcsmClient.close() },
|
||||||
)
|
)
|
||||||
|
|
||||||
resources.forEach { (name, closer) ->
|
resources.forEach { (name, closer) ->
|
||||||
|
|
@ -94,4 +108,5 @@ object GlobalManager {
|
||||||
isRunning.set(false)
|
isRunning.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,27 +1,24 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin
|
package top.r3944realms.ltdmanager.blessingskin
|
||||||
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.request.*
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import io.ktor.http.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.sync.withPermit
|
|
||||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
import top.r3944realms.ltdmanager.core.client.IClient
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
import top.r3944realms.ltdmanager.utils.Environment
|
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class BlessingSkinClient private constructor() : AutoCloseable {
|
class BlessingSkinClient private constructor() : IClient<BlessingSkinRequest, BlessingSkinQueueItem, BlessingSkinResponse, FailedBlessingSkinResponse> {
|
||||||
private val client = HttpClient(CIO) {
|
private val client = HttpClient(CIO) {
|
||||||
expectSuccess = false
|
expectSuccess = false
|
||||||
|
|
||||||
|
|
@ -40,170 +37,40 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
||||||
// 限流控制
|
// 限流控制
|
||||||
private val semaphore = Semaphore(5)
|
private val semaphore = Semaphore(5)
|
||||||
private val requestMutex = Mutex()
|
private val requestMutex = Mutex()
|
||||||
private val requestQueue = PriorityQueue<BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>>(compareBy { it.priority })
|
private val requestQueue = PriorityQueue<BlessingSkinQueueItem>(compareBy { it.priority })
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
startQueueProcessor()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun getBaseUrl(): String = blessingSkinServerConfig.url!!
|
||||||
* 提交请求
|
|
||||||
*/
|
override fun getType(): String = "BlessingSkinClient"
|
||||||
suspend fun <T : BlessingSkinResponse, F : FailedBlessingSkinResponse> submitRequest(
|
|
||||||
request: BlessingSkinRequest<T, F>,
|
override fun getClient(): HttpClient = client
|
||||||
priority: Int = 5,
|
|
||||||
maxRetries: Int = 3
|
override fun getSemaphore(): Semaphore = semaphore
|
||||||
): ResponseResult<T, F> {
|
|
||||||
val deferred = CompletableDeferred<ResponseResult<T, F>>()
|
override fun getRequestMutex(): Mutex = requestMutex
|
||||||
requestMutex.withLock {
|
|
||||||
requestQueue.add(BlessingSkinQueueItem(request, deferred, priority, maxRetries, true))
|
override fun getResponseQueue(): PriorityQueue<BlessingSkinQueueItem> = requestQueue
|
||||||
}
|
|
||||||
return deferred.await()
|
override fun getScope(): CoroutineScope = scope
|
||||||
|
|
||||||
|
override fun createFailureResponse(exception: Exception?): IFailedResponse {
|
||||||
|
return FailedBlessingSkinResponse.Default(exception?.stackTraceToString()?:"ERROR")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun addToQueue(
|
||||||
* 启动队列处理器
|
request: BlessingSkinRequest,
|
||||||
*/
|
deferredC: CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>,
|
||||||
private fun startQueueProcessor() {
|
priority: Int,
|
||||||
scope.launch {
|
maxRetries: Int
|
||||||
while (isActive) {
|
): BlessingSkinQueueItem {
|
||||||
val item = requestMutex.withLock {
|
val element = BlessingSkinQueueItem(request, deferredC, priority, maxRetries, false)
|
||||||
requestQueue.poll()
|
requestQueue.add(element)
|
||||||
}
|
return element
|
||||||
if (item == null) {
|
|
||||||
delay(50)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
processQueueItem(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理队列项
|
|
||||||
*/
|
|
||||||
private suspend fun processQueueItem(item: BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>) {
|
|
||||||
semaphore.withPermit {
|
|
||||||
val (request, deferred, _, maxRetries, expectsResponse) = item
|
|
||||||
var attempt = 0
|
|
||||||
var lastError: Exception? = null
|
|
||||||
|
|
||||||
while (attempt < maxRetries) {
|
|
||||||
try {
|
|
||||||
// 构建完整的URL,包括查询参数
|
|
||||||
val fullUrl = buildFullUrlWithQueryParams(request)
|
|
||||||
|
|
||||||
if (!Environment.isProduction()) {
|
|
||||||
LoggerUtil.logger.debug("发送请求到: $fullUrl")
|
|
||||||
LoggerUtil.logger.debug("请求方法: {}", request.method())
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = client.request(fullUrl) {
|
|
||||||
method = request.method()
|
|
||||||
|
|
||||||
|
|
||||||
// 设置请求头
|
|
||||||
headers {
|
|
||||||
request.headers().invoke(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对于非GET请求,设置请求体
|
|
||||||
if (request.method() != HttpMethod.Get) {
|
|
||||||
setBody(request.toJSON())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val responseText: String = response.body()
|
|
||||||
|
|
||||||
if (!Environment.isProduction()) {
|
|
||||||
LoggerUtil.logger.debug("响应状态: {}", response.status)
|
|
||||||
LoggerUtil.logger.debug("响应内容: $responseText")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否是HTML响应(重定向)
|
|
||||||
if (isHtmlResponse(responseText)) {
|
|
||||||
throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析响应
|
|
||||||
val result = request.getResponse(responseText, response.status)
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(deferred as CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).complete(result)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
lastError = e
|
|
||||||
attempt++
|
|
||||||
|
|
||||||
if (!request.shouldRetryOnFailure() || attempt >= maxRetries) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
LoggerUtil.logger.warn("BlessingSkin请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
|
||||||
delay((attempt * 1000L)) // 指数退避
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 所有重试都失败或不应重试
|
|
||||||
val errorResponse = createFailureResponse(lastError, request)
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(deferred as CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).complete(
|
|
||||||
ResponseResult.Failure(errorResponse)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建完整的URL,包含查询参数
|
|
||||||
*/
|
|
||||||
private fun buildFullUrlWithQueryParams(request: BlessingSkinRequest<*, *>): String {
|
|
||||||
val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/")
|
|
||||||
val path = request.path().removePrefix("/")
|
|
||||||
|
|
||||||
// 构建基础URL
|
|
||||||
val urlBuilder = StringBuilder("$baseUrl/$path")
|
|
||||||
|
|
||||||
// 添加查询参数
|
|
||||||
val queryParams = request.queryParameters().entries.joinToString("&") { (key, value) ->
|
|
||||||
"${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryParams.isNotEmpty()) {
|
|
||||||
urlBuilder.append("?").append(queryParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
return urlBuilder.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否是HTML响应
|
|
||||||
*/
|
|
||||||
private fun isHtmlResponse(text: String): Boolean {
|
|
||||||
return text.contains("<!DOCTYPE html>", ignoreCase = true) ||
|
|
||||||
text.contains("<html>", ignoreCase = true) ||
|
|
||||||
text.contains("Redirecting", ignoreCase = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建失败响应
|
|
||||||
*/
|
|
||||||
private fun createFailureResponse(
|
|
||||||
exception: Exception?,
|
|
||||||
request: BlessingSkinRequest<*, *>
|
|
||||||
): FailedBlessingSkinResponse {
|
|
||||||
return FailedBlessingSkinResponse.Default(
|
|
||||||
failedResult = exception?.message ?: "未知错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
scope.cancel()
|
|
||||||
runBlocking {
|
|
||||||
client.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ import kotlinx.coroutines.CompletableDeferred
|
||||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.QueueItem
|
||||||
|
|
||||||
data class BlessingSkinQueueItem<out T:BlessingSkinResponse,out F:FailedBlessingSkinResponse>(
|
data class BlessingSkinQueueItem (
|
||||||
val request: BlessingSkinRequest<T,F>,
|
val request0: BlessingSkinRequest,
|
||||||
val deferred: CompletableDeferred<*>,
|
val deferred0: CompletableDeferred<*>,
|
||||||
var retries: Int,
|
val priority0: Int,
|
||||||
val priority: Int,
|
var retries0: Int,
|
||||||
val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit
|
val expectsResponse0: Boolean
|
||||||
) : Comparable<BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
|
) : QueueItem<BlessingSkinRequest, BlessingSkinResponse, FailedBlessingSkinResponse> (
|
||||||
override fun compareTo(other: BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
request0, deferred0, retries0, priority0, expectsResponse0
|
||||||
}
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package top.r3944realms.ltdmanager.blessingskin.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InvitationCode(
|
||||||
|
val code: String,
|
||||||
|
@SerialName("generated_at")
|
||||||
|
val generatedAt: String,
|
||||||
|
@SerialName("expires_at")
|
||||||
|
val expiresAt: String
|
||||||
|
)
|
||||||
|
|
@ -1,79 +1,13 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin.request
|
package top.r3944realms.ltdmanager.blessingskin.request
|
||||||
|
|
||||||
import io.ktor.http.*
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class BlessingSkinRequest<out T : BlessingSkinResponse, out F : FailedBlessingSkinResponse>(
|
abstract class BlessingSkinRequest(
|
||||||
@Transient
|
@Transient
|
||||||
open val createTime: Long = System.currentTimeMillis()
|
override val createTime: Long = System.currentTimeMillis()
|
||||||
) {
|
): IRequest<BlessingSkinResponse, FailedBlessingSkinResponse>
|
||||||
/**
|
|
||||||
* 转换为JSON字符串
|
|
||||||
*/
|
|
||||||
abstract fun toJSON(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取API路径(不包含基础URL)
|
|
||||||
* 例如: "invitation-codes/generate"
|
|
||||||
*/
|
|
||||||
abstract fun path(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取HTTP方法,默认为GET(因为大多数API使用GET+查询参数)
|
|
||||||
*/
|
|
||||||
open fun method(): HttpMethod = HttpMethod.Get
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义请求头
|
|
||||||
*/
|
|
||||||
open fun headers(): HeadersBuilder.() -> Unit = {
|
|
||||||
// 默认添加Content-Type
|
|
||||||
append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
|
||||||
// 添加Accept头
|
|
||||||
append(HttpHeaders.Accept, "application/json")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取查询参数(用于URL参数)
|
|
||||||
* 例如: mapOf("token" to "abc123", "amount" to "1")
|
|
||||||
*/
|
|
||||||
open fun queryParameters(): Map<String, String> = emptyMap()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取请求体参数(用于POST请求的JSON body)
|
|
||||||
* 例如: mapOf("token" to "abc123", "amount" to 1)
|
|
||||||
*/
|
|
||||||
open fun bodyParameters(): Map<String, Any> = emptyMap()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取请求体内容类型,默认为Application.Json
|
|
||||||
*/
|
|
||||||
open fun contentType(): ContentType = ContentType.Application.Json
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析响应JSON字符串
|
|
||||||
* @param responseJson 响应JSON字符串
|
|
||||||
* @param httpStatusCode HTTP状态码
|
|
||||||
*/
|
|
||||||
abstract fun getResponse(responseJson: String, httpStatusCode: HttpStatusCode): ResponseResult<T, F>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取预期的成功响应类型名称(用于日志和调试)
|
|
||||||
*/
|
|
||||||
abstract fun expectedResponseType(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取预期的失败响应类型名称(用于日志和调试)
|
|
||||||
*/
|
|
||||||
abstract fun expectedFailureType(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否需要在失败时重试(默认重试)
|
|
||||||
*/
|
|
||||||
open fun shouldRetryOnFailure(): Boolean = true
|
|
||||||
}
|
|
||||||
|
|
@ -6,8 +6,8 @@ import kotlinx.serialization.Transient
|
||||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
@ -17,9 +17,7 @@ class GenerateInvitationCodeRequest(
|
||||||
val token: String? = null,
|
val token: String? = null,
|
||||||
@Transient
|
@Transient
|
||||||
val amount: Int? = 1,
|
val amount: Int? = 1,
|
||||||
@Transient
|
) : BlessingSkinRequest() {
|
||||||
override val createTime: Long = System.currentTimeMillis()
|
|
||||||
) : BlessingSkinRequest<InvitationCodeGenerationResponse, FailedBlessingSkinResponse.Default>() {
|
|
||||||
|
|
||||||
override fun toJSON(): String {
|
override fun toJSON(): String {
|
||||||
// 对于GET请求,参数在URL中,body可以为空
|
// 对于GET请求,参数在URL中,body可以为空
|
||||||
|
|
@ -66,7 +64,7 @@ class GenerateInvitationCodeRequest(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ResponseResult.Failure(
|
ResponseResult.Failure(
|
||||||
FailedBlessingSkinResponse.Default(
|
FailedBlessingSkinResponse.Default(
|
||||||
failedResult = "解析响应失败: ${e.message}"
|
failedMessage = "解析响应失败: ${e.message}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,15 @@ import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import kotlinx.serialization.modules.polymorphic
|
import kotlinx.serialization.modules.polymorphic
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class BlessingSkinResponse (
|
abstract class BlessingSkinResponse (
|
||||||
@Transient
|
@Transient
|
||||||
open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||||
@Transient
|
@Transient
|
||||||
open val createTime: Long = System.currentTimeMillis()
|
override val createTime: Long = System.currentTimeMillis()
|
||||||
) {
|
) : IResponse {
|
||||||
companion object {
|
companion object {
|
||||||
// 通用的反序列化方法
|
// 通用的反序列化方法
|
||||||
inline fun <reified T : BlessingSkinResponse> decode(jsonString: String): T {
|
inline fun <reified T : BlessingSkinResponse> decode(jsonString: String): T {
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@ package top.r3944realms.ltdmanager.blessingskin.response
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class FailedBlessingSkinResponse: BlessingSkinResponse() {
|
abstract class FailedBlessingSkinResponse: BlessingSkinResponse(), IFailedResponse {
|
||||||
abstract fun failedMessage(): String
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Default(@Transient val failedResult: String? = "未知错误") : FailedBlessingSkinResponse() {
|
class Default(@Transient override val failedMessage: String = "未知错误") : FailedBlessingSkinResponse() {
|
||||||
override fun failedMessage(): String = failedResult!!
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package top.r3944realms.ltdmanager.blessingskin.response.invitecode
|
package top.r3944realms.ltdmanager.blessingskin.response.invitecode
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.r3944realms.ltdmanager.blessingskin.data.InvitationCode
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||||
@Serializable
|
@Serializable
|
||||||
data class InvitationCodeGenerationResponse(
|
data class InvitationCodeGenerationResponse(
|
||||||
|
|
@ -10,12 +10,4 @@ data class InvitationCodeGenerationResponse(
|
||||||
val data: List<InvitationCode>? = null
|
val data: List<InvitationCode>? = null
|
||||||
) : BlessingSkinResponse() {
|
) : BlessingSkinResponse() {
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class InvitationCode(
|
|
||||||
val code: String,
|
|
||||||
@SerialName("generated_at")
|
|
||||||
val generatedAt: String,
|
|
||||||
@SerialName("expires_at")
|
|
||||||
val expiresAt: String
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.engine.cio.*
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.data.CheveretoSource
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.request.v1.CheveretoUploadRequest
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.IClient
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
|
import top.r3944realms.ltdmanager.utils.Environment
|
||||||
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class CheveretoClient private constructor() :
|
||||||
|
IClient<CheveretoRequest, CheveretoQueueItem, CheveretoResponse, FailedCheveretoResponse> {
|
||||||
|
|
||||||
|
private val client = HttpClient(CIO) {
|
||||||
|
expectSuccess = false
|
||||||
|
// 安装 HttpTimeout 插件
|
||||||
|
install(HttpTimeout) {
|
||||||
|
// 默认超时配置,会被具体请求的配置覆盖
|
||||||
|
requestTimeoutMillis = 30000
|
||||||
|
connectTimeoutMillis = 10000
|
||||||
|
socketTimeoutMillis = 15000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val imgTuConfig = YamlConfigLoader.loadTuImgConfig()
|
||||||
|
private val baseUrl = imgTuConfig.url!!.removeSuffix("/")
|
||||||
|
private val apiKey = imgTuConfig.decryptedPassword!!
|
||||||
|
|
||||||
|
private val semaphore = Semaphore(3)
|
||||||
|
private val queue = PriorityQueue<CheveretoQueueItem>()
|
||||||
|
private val queueMutex = Mutex()
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
|
init {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getType(): String = "CheveretoClient"
|
||||||
|
|
||||||
|
override fun getClient(): HttpClient = client
|
||||||
|
|
||||||
|
override fun getSemaphore(): Semaphore = semaphore
|
||||||
|
|
||||||
|
override fun getRequestMutex(): Mutex = queueMutex
|
||||||
|
|
||||||
|
override fun getResponseQueue(): PriorityQueue<CheveretoQueueItem> = queue
|
||||||
|
|
||||||
|
override fun getScope(): CoroutineScope = scope
|
||||||
|
|
||||||
|
override fun getBaseUrl(): String = baseUrl
|
||||||
|
|
||||||
|
override fun createFailureResponse(exception: Exception?): FailedCheveretoResponse =
|
||||||
|
FailedCheveretoResponse.Default(
|
||||||
|
httpStatusCode = HttpStatusCode.InternalServerError,
|
||||||
|
failedMessage = exception?.message ?: "Unknown error"
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun addToQueue(
|
||||||
|
request: CheveretoRequest,
|
||||||
|
deferredC: CompletableDeferred<ResponseResult<CheveretoResponse, FailedCheveretoResponse>>,
|
||||||
|
priority: Int,
|
||||||
|
maxRetries: Int
|
||||||
|
): CheveretoQueueItem {
|
||||||
|
val item = CheveretoQueueItem(request, deferredC, maxRetries, priority, true)
|
||||||
|
queue.add(item)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun processQueueItem(item: CheveretoQueueItem) {
|
||||||
|
getSemaphore().withPermit {
|
||||||
|
val request = item.request
|
||||||
|
val deferred = item.deferred
|
||||||
|
val maxRetries = item.retries
|
||||||
|
var attempt = 0
|
||||||
|
var lastError: Exception?
|
||||||
|
|
||||||
|
while (attempt < maxRetries) {
|
||||||
|
try {
|
||||||
|
val fullUrl = buildFullUrlWithQueryParams(request)
|
||||||
|
if (!Environment.isProduction()) {
|
||||||
|
LoggerUtil.logger.debug("发送请求到: $fullUrl")
|
||||||
|
LoggerUtil.logger.debug("请求方法: {}", request.method())
|
||||||
|
}
|
||||||
|
val response = getClient().request(fullUrl) {
|
||||||
|
method = request.method()
|
||||||
|
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
headers {
|
||||||
|
request.headers().invoke(this)
|
||||||
|
header("X-API-Key", apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于非GET请求,设置请求体
|
||||||
|
if (request.method() != HttpMethod.Get) {
|
||||||
|
setBody(request.toJSON())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val responseText: String = response.body()
|
||||||
|
|
||||||
|
if (!Environment.isProduction()) {
|
||||||
|
LoggerUtil.logger.debug("响应状态: {}", response.status)
|
||||||
|
LoggerUtil.logger.debug("响应内容: $responseText")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是HTML响应(重定向)
|
||||||
|
if (isHtmlResponse(responseText)) {
|
||||||
|
throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
val result = request.getResponse(responseText, response.status)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(deferred as CompletableDeferred<ResponseResult<IResponse, IFailedResponse>>).complete(result)
|
||||||
|
|
||||||
|
return
|
||||||
|
} catch (e: Exception) {
|
||||||
|
lastError = e
|
||||||
|
attempt++
|
||||||
|
|
||||||
|
if (!request.shouldRetryOnFailure() || attempt >= maxRetries) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerUtil.logger.warn("${getType()} 请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
||||||
|
delay((attempt * 1000L)) // 指数退避
|
||||||
|
}
|
||||||
|
// 所有重试都失败或不应重试
|
||||||
|
val errorResponse = createFailureResponse(lastError)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(deferred as CompletableDeferred<ResponseResult<IResponse, IFailedResponse>>).complete(
|
||||||
|
ResponseResult.Failure(errorResponse)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 上传 File
|
||||||
|
*/
|
||||||
|
suspend fun uploadFile(
|
||||||
|
file: File,
|
||||||
|
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,
|
||||||
|
maxRetries: Int = 3
|
||||||
|
|
||||||
|
): CheveretoResponse {
|
||||||
|
upload(CheveretoUploadRequest(
|
||||||
|
source = CheveretoSource.ByteArraySource(file.readBytes(), file.name),
|
||||||
|
format = format,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
tags = tags,
|
||||||
|
albumId = albumId,
|
||||||
|
categoryId = categoryId,
|
||||||
|
width = width,
|
||||||
|
expiration = expiration,
|
||||||
|
nsfw = nsfw,
|
||||||
|
useFileDate = useFileDate
|
||||||
|
), priority, maxRetries).getRetResponse()
|
||||||
|
throw Exception("Never Reach")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传 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,
|
||||||
|
maxRetries: Int = 3
|
||||||
|
): CheveretoResponse {
|
||||||
|
upload(CheveretoUploadRequest(
|
||||||
|
source = CheveretoSource.ByteArraySource(inputStream.readBytes(), fileName),
|
||||||
|
format = format,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
tags = tags,
|
||||||
|
albumId = albumId,
|
||||||
|
categoryId = categoryId,
|
||||||
|
width = width,
|
||||||
|
expiration = expiration,
|
||||||
|
nsfw = nsfw,
|
||||||
|
useFileDate = useFileDate
|
||||||
|
), priority, maxRetries).getRetResponse()
|
||||||
|
throw Exception("Never Reach")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传网络图片 URL
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
maxRetries: Int = 3
|
||||||
|
): CheveretoResponse {
|
||||||
|
upload(CheveretoUploadRequest(
|
||||||
|
source = CheveretoSource.UrlSource(url),
|
||||||
|
format = format,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
tags = tags,
|
||||||
|
albumId = albumId,
|
||||||
|
categoryId = categoryId,
|
||||||
|
width = width,
|
||||||
|
expiration = expiration,
|
||||||
|
nsfw = nsfw,
|
||||||
|
useFileDate = useFileDate
|
||||||
|
), priority, maxRetries).getRetResponse()
|
||||||
|
throw Exception("Never Reach")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun upload(
|
||||||
|
request: CheveretoUploadRequest, priority: Int, maxRetries: Int
|
||||||
|
): ResponseResult<CheveretoUploadResponse, FailedCheveretoResponse> {
|
||||||
|
return try {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
submitRequest(request, priority, maxRetries) as ResponseResult<CheveretoUploadResponse, FailedCheveretoResponse>
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ResponseResult.Failure(
|
||||||
|
FailedCheveretoResponse.Default(
|
||||||
|
httpStatusCode = HttpStatusCode.InternalServerError,
|
||||||
|
failedMessage = "Byte array upload failed: ${e.message}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(): CheveretoClient = CheveretoClient()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.QueueItem
|
||||||
|
|
||||||
|
data class CheveretoQueueItem(
|
||||||
|
val request0: CheveretoRequest,
|
||||||
|
val deferred0: CompletableDeferred<*>,
|
||||||
|
val priority0: Int,
|
||||||
|
var retries0: Int,
|
||||||
|
val expectsResponse0: Boolean
|
||||||
|
) : QueueItem<CheveretoRequest, CheveretoResponse, FailedCheveretoResponse>(
|
||||||
|
request0, deferred0, retries0, priority0, expectsResponse0
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CheveretoImage(
|
||||||
|
val name: String,
|
||||||
|
val extension: String,
|
||||||
|
val size: Long,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val date: String,
|
||||||
|
@SerialName("date_gmt")
|
||||||
|
val dateGmt: String,
|
||||||
|
val title: String,
|
||||||
|
val tags: List<String>? = 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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class CheveretoSource {
|
||||||
|
@Serializable
|
||||||
|
data class ByteArraySource(val bytes: ByteArray, val fileName: String) : CheveretoSource()
|
||||||
|
@Serializable
|
||||||
|
data class UrlSource(val url: String) : CheveretoSource()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class File(
|
||||||
|
val resource: Resource
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Resource(
|
||||||
|
val type: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.data
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.data
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.data
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SuccessInfo(
|
||||||
|
val message : String? = null,
|
||||||
|
val code: Int? = 200,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.request
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class CheveretoRequest(
|
||||||
|
@Transient
|
||||||
|
override val createTime: Long = System.currentTimeMillis()
|
||||||
|
) : IRequest<CheveretoResponse, FailedCheveretoResponse>
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.request.v1
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.data.CheveretoSource
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CheveretoUploadRequest(
|
||||||
|
private val source: CheveretoSource,
|
||||||
|
private val title: String? = null,
|
||||||
|
private val description: String? = null,
|
||||||
|
private val tags: String? = null,
|
||||||
|
@SerialName("album_id")
|
||||||
|
private val albumId: String? = null,
|
||||||
|
@SerialName("category_id")
|
||||||
|
private val categoryId: String? = null,
|
||||||
|
private val width: Int? = null,
|
||||||
|
private val expiration: String? = null,
|
||||||
|
private val nsfw: Int? = null,
|
||||||
|
private val format: String = "json",
|
||||||
|
@SerialName("use_file_date")
|
||||||
|
private val useFileDate: Int? = null
|
||||||
|
) : CheveretoRequest() {
|
||||||
|
override fun path(): String = "api/1/upload"
|
||||||
|
|
||||||
|
override fun method(): HttpMethod = HttpMethod.Post
|
||||||
|
|
||||||
|
override fun headers(): HeadersBuilder.() -> Unit = {
|
||||||
|
append(HttpHeaders.Accept, "application/json")
|
||||||
|
// 对于文件上传,Content-Type 由 Ktor 自动设置
|
||||||
|
}
|
||||||
|
override fun bodyParameters(): Map<String, Any> {
|
||||||
|
val params = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
title?.let { params["title"] = it }
|
||||||
|
description?.let { params["description"] = it }
|
||||||
|
tags?.let { params["tags"] = it }
|
||||||
|
albumId?.let { params["album_id"] = it }
|
||||||
|
categoryId?.let { params["category_id"] = it }
|
||||||
|
width?.let { params["width"] = it }
|
||||||
|
expiration?.let { params["expiration"] = it }
|
||||||
|
nsfw?.let { params["nsfw"] = it }
|
||||||
|
params["format"] = format
|
||||||
|
useFileDate?.let { params["use_file_date"] = it }
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJSON(): String = Json.encodeToString(this)
|
||||||
|
|
||||||
|
override fun getResponse(
|
||||||
|
responseJson: String,
|
||||||
|
httpStatusCode: HttpStatusCode
|
||||||
|
): ResponseResult<CheveretoUploadResponse, FailedCheveretoResponse> {
|
||||||
|
return try {
|
||||||
|
if (httpStatusCode.isSuccess()) {
|
||||||
|
val successResponse = Json.decodeFromString<CheveretoUploadResponse>(responseJson)
|
||||||
|
ResponseResult.Success(successResponse)
|
||||||
|
} else {
|
||||||
|
ResponseResult.Failure(
|
||||||
|
FailedCheveretoResponse.Default(
|
||||||
|
httpStatusCode = HttpStatusCode.InternalServerError,
|
||||||
|
failedMessage = responseJson.takeIf { it.isNotBlank() }?:"ERROR"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ResponseResult.Failure(
|
||||||
|
FailedCheveretoResponse.Default(
|
||||||
|
httpStatusCode = HttpStatusCode.InternalServerError,
|
||||||
|
failedMessage = "Failed to parse response: ${e.message}. Raw response: $responseJson"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun expectedResponseType(): String = "CheveretoUploadResponse"
|
||||||
|
|
||||||
|
override fun expectedFailureType(): String = "FailedCheveretoResponse"
|
||||||
|
|
||||||
|
override fun shouldRetryOnFailure(): Boolean = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.response
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.polymorphic
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class CheveretoResponse (
|
||||||
|
@Transient
|
||||||
|
override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||||
|
@Transient
|
||||||
|
override val createTime: Long = System.currentTimeMillis()
|
||||||
|
) : IResponse {
|
||||||
|
companion object {
|
||||||
|
// 通用的反序列化方法
|
||||||
|
inline fun <reified T : CheveretoResponse> decode(jsonString: String): T {
|
||||||
|
return json.decodeFromString(jsonString)
|
||||||
|
}
|
||||||
|
val json: Json by lazy {
|
||||||
|
Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
serializersModule = SerializersModule {
|
||||||
|
polymorphic(CheveretoResponse::class) {
|
||||||
|
subclass(FailedCheveretoResponse.Default::class, FailedCheveretoResponse.Default.serializer())
|
||||||
|
subclass(CheveretoUploadResponse::class, CheveretoUploadResponse.serializer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.response
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class FailedCheveretoResponse: CheveretoResponse(), IFailedResponse {
|
||||||
|
@Serializable
|
||||||
|
class Default(@Transient override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, @Transient override val failedMessage: String = "未知错误") : FailedCheveretoResponse()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package top.r3944realms.ltdmanager.chevereto.response.v1
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.data.CheveretoImage
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.data.SuccessInfo
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CheveretoUploadResponse(
|
||||||
|
@SerialName("status_code")
|
||||||
|
val statusCode: Int,
|
||||||
|
val success: SuccessInfo,
|
||||||
|
val image: CheveretoImage,
|
||||||
|
@SerialName("status_txt")
|
||||||
|
val statusTxt: String
|
||||||
|
) : CheveretoResponse()
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
package top.r3944realms.ltdmanager.core.client
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
|
import top.r3944realms.ltdmanager.utils.Environment
|
||||||
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface IClient<R: IRequest<T, F>, Q: QueueItem<R, T, F>, T: IResponse, F: IFailedResponse> : AutoCloseable {
|
||||||
|
fun getType(): String
|
||||||
|
fun getClient(): HttpClient
|
||||||
|
fun getSemaphore(): Semaphore
|
||||||
|
fun getRequestMutex(): Mutex
|
||||||
|
fun getResponseQueue(): PriorityQueue<Q>
|
||||||
|
fun getScope(): CoroutineScope
|
||||||
|
fun getBaseUrl(): String = "http://localhost:1234"
|
||||||
|
fun createFailureResponse(exception: Exception? ): IFailedResponse
|
||||||
|
fun init() {
|
||||||
|
startQueueProcessor()
|
||||||
|
}
|
||||||
|
fun startQueueProcessor() {
|
||||||
|
getScope().launch {
|
||||||
|
while (isActive) {
|
||||||
|
val item = getRequestMutex().withLock {
|
||||||
|
getResponseQueue().poll()
|
||||||
|
}
|
||||||
|
if (item == null) {
|
||||||
|
delay(50)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processQueueItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun addToQueue(request: R,
|
||||||
|
deferredC: CompletableDeferred<ResponseResult<T, F>>,
|
||||||
|
priority: Int = 5,
|
||||||
|
maxRetries: Int = 3): Q
|
||||||
|
/**
|
||||||
|
* 提交请求
|
||||||
|
*/
|
||||||
|
suspend fun submitRequest(
|
||||||
|
request: R,
|
||||||
|
priority: Int = 5,
|
||||||
|
maxRetries: Int = 3
|
||||||
|
): ResponseResult<T, F> {
|
||||||
|
val deferred = CompletableDeferred<ResponseResult<T, F>>()
|
||||||
|
getRequestMutex().withLock {
|
||||||
|
addToQueue(request, deferred, priority, maxRetries)
|
||||||
|
}
|
||||||
|
return deferred.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun processQueueItem(item: Q) {
|
||||||
|
getSemaphore().withPermit {
|
||||||
|
val request = item.request
|
||||||
|
val deferred = item.deferred
|
||||||
|
val maxRetries = item.retries
|
||||||
|
var attempt = 0
|
||||||
|
var lastError: Exception?
|
||||||
|
|
||||||
|
while (attempt < maxRetries) {
|
||||||
|
try {
|
||||||
|
val fullUrl = buildFullUrlWithQueryParams(request)
|
||||||
|
if (!Environment.isProduction()) {
|
||||||
|
LoggerUtil.logger.debug("发送请求到: $fullUrl")
|
||||||
|
LoggerUtil.logger.debug("请求方法: {}", request.method())
|
||||||
|
}
|
||||||
|
val response = getClient().request(fullUrl) {
|
||||||
|
method = request.method()
|
||||||
|
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
headers {
|
||||||
|
request.headers().invoke(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于非GET请求,设置请求体
|
||||||
|
if (request.method() != HttpMethod.Get) {
|
||||||
|
setBody(request.toJSON())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val responseText: String = response.body()
|
||||||
|
|
||||||
|
if (!Environment.isProduction()) {
|
||||||
|
LoggerUtil.logger.debug("响应状态: {}", response.status)
|
||||||
|
LoggerUtil.logger.debug("响应内容: $responseText")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是HTML响应(重定向)
|
||||||
|
if (isHtmlResponse(responseText)) {
|
||||||
|
throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
val result = request.getResponse(responseText, response.status)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(deferred as CompletableDeferred<ResponseResult<IResponse, IFailedResponse>>).complete(result)
|
||||||
|
|
||||||
|
return
|
||||||
|
} catch (e: Exception) {
|
||||||
|
lastError = e
|
||||||
|
attempt++
|
||||||
|
|
||||||
|
if (!request.shouldRetryOnFailure() || attempt >= maxRetries) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerUtil.logger.warn("${getType()} 请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
||||||
|
delay((attempt * 1000L)) // 指数退避
|
||||||
|
}
|
||||||
|
// 所有重试都失败或不应重试
|
||||||
|
val errorResponse = createFailureResponse(lastError)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(deferred as CompletableDeferred<ResponseResult<IResponse, IFailedResponse>>).complete(
|
||||||
|
ResponseResult.Failure(errorResponse)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 构建完整的URL,包含查询参数
|
||||||
|
*/
|
||||||
|
fun buildFullUrlWithQueryParams(request: R): String {
|
||||||
|
val baseUrl = getBaseUrl().removeSuffix("/")
|
||||||
|
val path = request.path().removePrefix("/")
|
||||||
|
|
||||||
|
// 构建基础URL
|
||||||
|
val urlBuilder = StringBuilder("$baseUrl/$path")
|
||||||
|
|
||||||
|
// 添加查询参数
|
||||||
|
val queryParams = request.queryParameters().entries.joinToString("&") { (key, value) ->
|
||||||
|
"${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryParams.isNotEmpty()) {
|
||||||
|
urlBuilder.append("?").append(queryParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlBuilder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否是HTML响应
|
||||||
|
*/
|
||||||
|
fun isHtmlResponse(text: String): Boolean {
|
||||||
|
return text.contains("<!DOCTYPE html>", ignoreCase = true) ||
|
||||||
|
text.contains("<html>", ignoreCase = true) ||
|
||||||
|
text.contains("Redirecting", ignoreCase = true)
|
||||||
|
}
|
||||||
|
override fun close() {
|
||||||
|
getScope().cancel()
|
||||||
|
runBlocking {
|
||||||
|
getClient().close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package top.r3944realms.ltdmanager.core.client
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
|
||||||
|
open class QueueItem<R: IRequest<T, F>, T:IResponse, F:IFailedResponse>(
|
||||||
|
val request: R,
|
||||||
|
val deferred: CompletableDeferred<*>,
|
||||||
|
val retries: Int,
|
||||||
|
val priority: Int,
|
||||||
|
val expectsResponse: Boolean
|
||||||
|
|
||||||
|
) : Comparable<QueueItem<R, T, F>> {
|
||||||
|
// fun getRequest(): R = request
|
||||||
|
// fun getDeferred(): CompletableDeferred<*> = deferred
|
||||||
|
// fun getRetries(): Int = retries
|
||||||
|
// fun getPriority(): Int = priority
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true 表示返回 BlessingSkinResponse, false 表示 Unit
|
||||||
|
*/
|
||||||
|
fun expectsResponse(): Boolean = expectsResponse
|
||||||
|
override fun compareTo(other: QueueItem<R, @UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package top.r3944realms.ltdmanager.core.client.request
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
|
|
||||||
|
interface IRequest<out T: IResponse, out F: IFailedResponse> {
|
||||||
|
// 只使用属性
|
||||||
|
val createTime: Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为JSON字符串
|
||||||
|
*/
|
||||||
|
fun toJSON(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取API路径(不包含基础URL)
|
||||||
|
* 例如: "invitation-codes/generate"
|
||||||
|
*/
|
||||||
|
fun path(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取HTTP方法,默认为GET(因为大多数API使用GET+查询参数)
|
||||||
|
*/
|
||||||
|
fun method(): HttpMethod = HttpMethod.Get
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义请求头
|
||||||
|
*/
|
||||||
|
fun headers(): HeadersBuilder.() -> Unit = {
|
||||||
|
// 默认添加Content-Type
|
||||||
|
append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
// 添加Accept头
|
||||||
|
append(HttpHeaders.Accept, "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取查询参数(用于URL参数)
|
||||||
|
* 例如: mapOf("token" to "abc123", "amount" to "1")
|
||||||
|
*/
|
||||||
|
fun queryParameters(): Map<String, String> = emptyMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求体参数(用于POST请求的JSON body)
|
||||||
|
* 例如: mapOf("token" to "abc123", "amount" to 1)
|
||||||
|
*/
|
||||||
|
fun bodyParameters(): Map<String, Any> = emptyMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求体内容类型,默认为Application.Json
|
||||||
|
*/
|
||||||
|
fun contentType(): ContentType = ContentType.Application.Json
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析响应JSON字符串
|
||||||
|
* @param responseJson 响应JSON字符串
|
||||||
|
* @param httpStatusCode HTTP状态码
|
||||||
|
*/
|
||||||
|
fun getResponse(responseJson: String, httpStatusCode: HttpStatusCode): ResponseResult<T, F>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预期的成功响应类型名称(用于日志和调试)
|
||||||
|
*/
|
||||||
|
fun expectedResponseType(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预期的失败响应类型名称(用于日志和调试)
|
||||||
|
*/
|
||||||
|
fun expectedFailureType(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要在失败时重试(默认重试)
|
||||||
|
*/
|
||||||
|
fun shouldRetryOnFailure(): Boolean = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package top.r3944realms.ltdmanager.core.client.response
|
||||||
|
|
||||||
|
interface IFailedResponse : IResponse {
|
||||||
|
val failedMessage: String
|
||||||
|
val thrownException: Exception
|
||||||
|
get() = Exception(failedMessage)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package top.r3944realms.ltdmanager.core.client.response
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
interface IResponse {
|
||||||
|
val httpStatusCode: HttpStatusCode
|
||||||
|
val createTime: Long
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package top.r3944realms.ltdmanager.core.client.response
|
||||||
|
|
||||||
|
sealed class ResponseResult<out T: IResponse, out F: IFailedResponse> {
|
||||||
|
data class Success<T : IResponse>(val response: T) : ResponseResult<T, Nothing>()
|
||||||
|
data class Failure<F : IFailedResponse>(val failure: F) : ResponseResult<Nothing, F>()
|
||||||
|
/**
|
||||||
|
* 检查是否成功
|
||||||
|
*/
|
||||||
|
fun isSuccess(): Boolean = this is Success
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取成功响应(如果存在)
|
||||||
|
*/
|
||||||
|
fun getSuccessResponse(): T? = (this as? Success)?.response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取失败响应(如果存在)
|
||||||
|
*/
|
||||||
|
fun getFailureResponse(): F? = (this as? Failure)?.failure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功时执行操作
|
||||||
|
*/
|
||||||
|
inline fun onSuccess(action: (T) -> Unit): ResponseResult<T, F> {
|
||||||
|
if (this is Success) action(response)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败时执行操作
|
||||||
|
*/
|
||||||
|
inline fun onFailure(action: (F) -> Unit): ResponseResult<T, F> {
|
||||||
|
if (this is Failure) action(failure)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRetResponse(): T {
|
||||||
|
if (this is Success) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
else if (this is Failure) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return failure as T
|
||||||
|
}
|
||||||
|
throw Exception("Never Reach")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ data class BlessingSkinServerConfig(
|
||||||
try {
|
try {
|
||||||
encryptedToken = "ENC(${CryptoUtil.encrypt(encryptedToken!!)})"
|
encryptedToken = "ENC(${CryptoUtil.encrypt(encryptedToken!!)})"
|
||||||
YamlUpdater.updateYaml(
|
YamlUpdater.updateYaml(
|
||||||
YamlConfigLoader.configFilePath.toString(),
|
YamlConfigLoader.appConfigFilePath.toString(),
|
||||||
"blessing-skin-server.invitation-api.encrypted-token",
|
"blessing-skin-server.invitation-api.encrypted-token",
|
||||||
encryptedToken!!
|
encryptedToken!!
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ data class DatabaseConfig(
|
||||||
try {
|
try {
|
||||||
encryptedPassword = "ENC(${CryptoUtil.encrypt(encryptedPassword!!)})"
|
encryptedPassword = "ENC(${CryptoUtil.encrypt(encryptedPassword!!)})"
|
||||||
YamlUpdater.updateYaml(
|
YamlUpdater.updateYaml(
|
||||||
YamlConfigLoader.configFilePath.toString(),
|
YamlConfigLoader.appConfigFilePath.toString(),
|
||||||
"database.encrypted-password",
|
"database.encrypted-password",
|
||||||
this.encryptedPassword!!
|
this.encryptedPassword!!
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
package top.r3944realms.ltdmanager.core.config
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.utils.CryptoUtil
|
||||||
|
import top.r3944realms.ltdmanager.utils.YamlUpdater
|
||||||
|
|
||||||
|
|
||||||
|
data class DgLabConfig(
|
||||||
|
var wsServer: WsServerConfig = WsServerConfig(),
|
||||||
|
var dgLabClient: DgLabClientConfig = DgLabClientConfig(),
|
||||||
|
var pulseData: PulseDataConfig = PulseDataConfig(),
|
||||||
|
var commandText: CommandTextConfig = CommandTextConfig(),
|
||||||
|
var replyText: ReplyTextConfig = ReplyTextConfig(),
|
||||||
|
var debug: DebugConfig = DebugConfig()
|
||||||
|
) {
|
||||||
|
data class WsServerConfig(
|
||||||
|
var localServerUrl: String = "0.0.0.0",
|
||||||
|
var localServerPort: Int = 4567,
|
||||||
|
var localServerPublishUrl: String = "ws://127.0.0.1:4567",
|
||||||
|
var localServerSecure: Boolean = false,
|
||||||
|
var localServerSslCert: String = "",
|
||||||
|
var localServerSslKey: String = "",
|
||||||
|
var encryptedLocalServerSslPassword: String? = null
|
||||||
|
) {
|
||||||
|
val decryptedLocalServerSslPassword: String?
|
||||||
|
get() {
|
||||||
|
if (encryptedLocalServerSslPassword == null) return null
|
||||||
|
if (!isEncrypted()) return encryptedLocalServerSslPassword
|
||||||
|
return try {
|
||||||
|
val cipherText = encryptedLocalServerSslPassword!!.substring(4, encryptedLocalServerSslPassword!!.length - 1)
|
||||||
|
CryptoUtil.decrypt(cipherText)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("localServerSslPassword 解密失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encryptPassword() {
|
||||||
|
if (encryptedLocalServerSslPassword == null || isEncrypted()) return
|
||||||
|
try {
|
||||||
|
encryptedLocalServerSslPassword = "ENC(${CryptoUtil.encrypt(encryptedLocalServerSslPassword!!)})"
|
||||||
|
YamlUpdater.updateYaml(
|
||||||
|
YamlConfigLoader.appConfigFilePath.toString(),
|
||||||
|
"dg-lab.ws-server.encrypted-local-server-ssl-password",
|
||||||
|
encryptedLocalServerSslPassword!!
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("SSL 密码加密失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isEncrypted(): Boolean {
|
||||||
|
return encryptedLocalServerSslPassword != null &&
|
||||||
|
encryptedLocalServerSslPassword!!.startsWith("ENC(") &&
|
||||||
|
encryptedLocalServerSslPassword!!.endsWith(")")
|
||||||
|
}
|
||||||
|
//TODO: 添加有效性检测
|
||||||
|
fun validate() {
|
||||||
|
require(localServerUrl.isNotBlank()) { "localServerUrl 未配置" }
|
||||||
|
require(localServerPort > 0) { "localServerPort 必须大于 0" }
|
||||||
|
require(localServerPublishUrl.isNotBlank()) { "localServerPublishUrl 未配置" }
|
||||||
|
if (localServerSecure) {
|
||||||
|
require(localServerSslCert.isNotBlank()) { "启用 SSL 时必须配置 localServerSslCert" }
|
||||||
|
require(localServerSslKey.isNotBlank()) { "启用 SSL 时必须配置 localServerSslKey" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DgLabClientConfig(
|
||||||
|
var bindTimeout: Double = 90.0,
|
||||||
|
var registerTimeout: Double = 30.0
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PulseDataConfig(
|
||||||
|
var customPulseData: String = "data/dg-lab-play/customPulseData.json",
|
||||||
|
var durationPerPost: Double = 8.0,
|
||||||
|
var postInterval: Double = 1.0,
|
||||||
|
var sleepAfterClear: Double = 0.5
|
||||||
|
) {
|
||||||
|
fun validate(maxLength: Double) {
|
||||||
|
require(durationPerPost <= maxLength * 0.1) { "PulseDataConfig.durationPerPost 超出最大时长" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CommandTextConfig(
|
||||||
|
var appendPulse: String = "增加波形",
|
||||||
|
var currentPulse: String = "当前波形",
|
||||||
|
var currentStrength: String = "当前强度",
|
||||||
|
var decreaseStrength: String = "减小强度",
|
||||||
|
var dgLabDeviceJoin: String = "绑定郊狼",
|
||||||
|
var exitGame: String = "退出游戏",
|
||||||
|
var increaseStrength: String = "加大强度",
|
||||||
|
var randomPulse: String = "随机波形",
|
||||||
|
var randomStrength: String = "随机强度",
|
||||||
|
var resetPulse: String = "重置波形",
|
||||||
|
var showPlayers: String = "当前玩家",
|
||||||
|
var showPulses: String = "可用波形",
|
||||||
|
var usage: String = "郊狼玩法"
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ReplyTextConfig(
|
||||||
|
var bindTimeout: String = "绑定超时",
|
||||||
|
var currentPlayers: String = "当前玩家:",
|
||||||
|
var currentPulse: String = "当前波形循环为:【{}】",
|
||||||
|
var currentStrength: String = "A通道:{0}/{1} B通道:{2}/{3}",
|
||||||
|
var failedToCreateClient: String = "创建 DG-Lab 控制终端失败",
|
||||||
|
var failedToFetchStrengthInfo: String = "获取通道强度状态失败",
|
||||||
|
var failedToFetchStrengthLimit: String = "获取通道强度上限失败,控制失败",
|
||||||
|
var gameExited: String = "已退出游戏",
|
||||||
|
var invalidPulseParam: String = "波形参数错误,控制失败",
|
||||||
|
var invalidStrengthParam: String = "强度参数错误,控制失败",
|
||||||
|
var invalidTarget: String = "目标玩家不存在或郊狼 App 未绑定",
|
||||||
|
var noAvailablePulse: String = "无可用波形",
|
||||||
|
var noPlayer: String = "当前没有已连接的玩家,你可以绑定试试~",
|
||||||
|
var notBindYet: String = "你目前没有绑定 DG-Lab App",
|
||||||
|
var pleaseAtTarget: String = "使用命令的同时请 @ 想要控制的玩家",
|
||||||
|
var pleaseScanQrcode: String = "请用 DG-Lab App 扫描二维码以连接",
|
||||||
|
var pleaseSetPulseFirst: String = "请先设置郊狼波形:{}",
|
||||||
|
var pulsesEmpty: String = "当前波形循环为空",
|
||||||
|
var successfullyBind: String = "绑定成功,可以开始色色了!",
|
||||||
|
var successfullyDecreased: String = "郊狼强度减小了 {}%",
|
||||||
|
var successfullyIncreased: String = "郊狼强度加强了 {}%!",
|
||||||
|
var successfullySetPulse: String = "郊狼波形成功设置为【{}】!",
|
||||||
|
var successfullySetToStrength: String = "郊狼强度成功设置为 {}%!"
|
||||||
|
)
|
||||||
|
|
||||||
|
data class DebugConfig(
|
||||||
|
var enableDebug: Boolean = false,
|
||||||
|
var ideHost: String = "127.0.0.1",
|
||||||
|
var idePort: Int = 5678
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ data class HttpConfig(
|
||||||
try {
|
try {
|
||||||
encryptedToken = "ENC(${CryptoUtil.encrypt(encryptedToken!!)})"
|
encryptedToken = "ENC(${CryptoUtil.encrypt(encryptedToken!!)})"
|
||||||
YamlUpdater.updateYaml(
|
YamlUpdater.updateYaml(
|
||||||
YamlConfigLoader.configFilePath.toString(),
|
YamlConfigLoader.appConfigFilePath.toString(),
|
||||||
"http.encrypted-token",
|
"http.encrypted-token",
|
||||||
this.encryptedToken!!
|
this.encryptedToken!!
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package top.r3944realms.ltdmanager.core.config
|
||||||
|
|
||||||
|
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.appConfigFilePath.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=***)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ data class MailConfig(
|
||||||
try {
|
try {
|
||||||
encryptedPassword = "ENC(${CryptoUtil.encrypt(encryptedPassword!!)})"
|
encryptedPassword = "ENC(${CryptoUtil.encrypt(encryptedPassword!!)})"
|
||||||
YamlUpdater.updateYaml(
|
YamlUpdater.updateYaml(
|
||||||
YamlConfigLoader.configFilePath.toString(),
|
YamlConfigLoader.appConfigFilePath.toString(),
|
||||||
"mail.encrypted-password",
|
"mail.encrypted-password",
|
||||||
this.encryptedPassword!!
|
this.encryptedPassword!!
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package top.r3944realms.ltdmanager.core.config
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.utils.CryptoUtil
|
||||||
|
import top.r3944realms.ltdmanager.utils.YamlUpdater
|
||||||
|
|
||||||
|
data class McsmConfig(
|
||||||
|
var url: String ?= null,
|
||||||
|
var encryptedApiKey: String ?= null,
|
||||||
|
var instanceID: String ?= null,
|
||||||
|
) {
|
||||||
|
val decryptedApiKey: String?
|
||||||
|
get() {
|
||||||
|
if (encryptedApiKey == null) return null
|
||||||
|
if (!isEncrypted()) return encryptedApiKey
|
||||||
|
try {
|
||||||
|
val cipherText = encryptedApiKey!!.substring(4, encryptedApiKey!!.length - 1)
|
||||||
|
return CryptoUtil.decrypt(cipherText)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("API解密失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密密码(如果未加密),并写回配置文件
|
||||||
|
*/
|
||||||
|
fun encryptApi() {
|
||||||
|
if (encryptedApiKey == null || isEncrypted()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
encryptedApiKey = "ENC(${CryptoUtil.encrypt(encryptedApiKey!!)})"
|
||||||
|
YamlUpdater.updateYaml(
|
||||||
|
YamlConfigLoader.appConfigFilePath.toString(),
|
||||||
|
"mcsm.encrypted-api-key",
|
||||||
|
this.encryptedApiKey!!
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("API加密失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查密码是否已加密
|
||||||
|
*/
|
||||||
|
private fun isEncrypted(): Boolean {
|
||||||
|
return encryptedApiKey != null &&
|
||||||
|
encryptedApiKey!!.startsWith("ENC(") &&
|
||||||
|
encryptedApiKey!!.endsWith(")")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "McsmConfig(url=$url, api-key=***)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
package top.r3944realms.ltdmanager.core.config
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.module.Modules
|
||||||
|
import top.r3944realms.ltdmanager.module.exception.ConfigError
|
||||||
|
|
||||||
|
data class ModuleConfig(
|
||||||
|
var modules: List<Module>? = emptyList()
|
||||||
|
) {
|
||||||
|
data class Module(
|
||||||
|
var name: String = "default",
|
||||||
|
var type: ModuleType = ModuleType.HELP_MODULE,
|
||||||
|
var enabled: Boolean = true,
|
||||||
|
var dependencies: List<Dependency>? = emptyList(),
|
||||||
|
var config: Map<String, Any> = emptyMap()
|
||||||
|
) {
|
||||||
|
data class Dependency(
|
||||||
|
var name: String = "unknown",
|
||||||
|
var type: ModuleType = ModuleType.UNKNOWN_MODULE,
|
||||||
|
) {
|
||||||
|
fun getDepName() :String = "${type.modName}-#$name"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findDependency(type: ModuleType): Dependency? {
|
||||||
|
return dependencies?.find { it.type == type }
|
||||||
|
}
|
||||||
|
inline fun <reified T> typedList(paramName: String): List<T> {
|
||||||
|
val list = anyList(paramName)
|
||||||
|
return list.map { element ->
|
||||||
|
when (T::class) {
|
||||||
|
String::class -> element.toString() as T
|
||||||
|
Int::class -> convertToInt(element, "$paramName.list.element") as T
|
||||||
|
Long::class -> convertToLong(element, "$paramName.list.element") as T
|
||||||
|
Boolean::class -> convertToBoolean(element, "$paramName.list.element") as T
|
||||||
|
else -> {
|
||||||
|
if (element is T) element
|
||||||
|
else throw ConfigError(
|
||||||
|
ConfigError.Type.NOT_EXPECTED_VALUE,
|
||||||
|
name,
|
||||||
|
"$paramName.list",
|
||||||
|
T::class.simpleName ?: T::class.java.simpleName,
|
||||||
|
element::class.simpleName ?: element::class.java.simpleName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 特定类型的 List 方法
|
||||||
|
fun <T> list(paramName: String): List<T> = get<List<T>>(paramName)
|
||||||
|
|
||||||
|
// 特定类型的 Map 方法
|
||||||
|
fun <K, V> map(paramName: String): Map<K, V> = get<Map<K, V>>(paramName)
|
||||||
|
|
||||||
|
// 泛型 List 获取(返回 List<Any>)
|
||||||
|
fun anyList(paramName: String): List<Any> = get<List<Any>>(paramName)
|
||||||
|
|
||||||
|
// 泛型 Map 获取(返回 Map<String, Any>)
|
||||||
|
fun anyMap(paramName: String): Map<String, Any> = get<Map<String, Any>>(paramName)
|
||||||
|
|
||||||
|
// String List 的便捷方法
|
||||||
|
fun stringList(paramName: String): List<String> {
|
||||||
|
val list = anyList(paramName)
|
||||||
|
return list.map { it.toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int List 的便捷方法
|
||||||
|
fun intList(paramName: String): List<Int> {
|
||||||
|
val list = anyList(paramName)
|
||||||
|
return list.map { value ->
|
||||||
|
when (value) {
|
||||||
|
is Int -> value
|
||||||
|
is Number -> value.toInt()
|
||||||
|
is String -> value.toIntOrNull()
|
||||||
|
?: throw ConfigError(
|
||||||
|
ConfigError.Type.NOT_EXPECTED_VALUE,
|
||||||
|
name,
|
||||||
|
paramName,
|
||||||
|
"List<Int>",
|
||||||
|
"元素类型: ${value::class.simpleName}"
|
||||||
|
)
|
||||||
|
else -> throw ConfigError(
|
||||||
|
ConfigError.Type.NOT_EXPECTED_VALUE,
|
||||||
|
name,
|
||||||
|
paramName,
|
||||||
|
"List<Int>",
|
||||||
|
"元素类型: ${value::class.simpleName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取特定类型的 Map
|
||||||
|
inline fun <reified V> typedMap(paramName: String): Map<String, V> {
|
||||||
|
val map = anyMap(paramName)
|
||||||
|
return map.mapValues { (key, value) ->
|
||||||
|
when (V::class) {
|
||||||
|
String::class -> value.toString() as V
|
||||||
|
Int::class -> convertToInt(value, "$paramName.$key") as V
|
||||||
|
Long::class -> convertToLong(value, "$paramName.$key") as V
|
||||||
|
Boolean::class -> convertToBoolean(value, "$paramName.$key") as V
|
||||||
|
else -> {
|
||||||
|
if (value is V) value
|
||||||
|
else throw ConfigError(
|
||||||
|
ConfigError.Type.NOT_EXPECTED_VALUE,
|
||||||
|
name,
|
||||||
|
"$paramName.$key",
|
||||||
|
V::class.simpleName ?: V::class.java.simpleName,
|
||||||
|
value::class.simpleName ?: value::class.java.simpleName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ModuleType(val modName: String) {
|
||||||
|
GROUP_MESSAGE_POLLING_MODULE(Modules.GROUP_MESSAGE_POLLING),
|
||||||
|
GROUP_REQUEST_HANDLER_MODULE(Modules.GROUP_REQUEST_HANDLER),
|
||||||
|
MAIL_MODULE(Modules.MAIL),
|
||||||
|
BAN_MODULE(Modules.BAN),
|
||||||
|
DG_LAB_MODULE(Modules.DG_LAB),
|
||||||
|
INVITE_MODULE(Modules.INVITATION_CODE),
|
||||||
|
MC_SERVER_STATUS_MODULE(Modules.MC_SERVER_STATUS),
|
||||||
|
MOD_GROUP_HANDLER_MODULE(Modules.MOD_GROUP_HANDLER),
|
||||||
|
RCON_PLAYER_LIST_MODULE(Modules.RCON_PLAYER_LIST),
|
||||||
|
STATE_MODULE(Modules.STATE),
|
||||||
|
HELP_MODULE(Modules.HELP),
|
||||||
|
UNKNOWN_MODULE("UnknownModule");
|
||||||
|
}
|
||||||
|
// 基础获取方法
|
||||||
|
fun value(paramName: String): Any =
|
||||||
|
config[paramName] ?: throw ConfigError(
|
||||||
|
ConfigError.Type.MISSING_PARAMETER,
|
||||||
|
name,
|
||||||
|
paramName
|
||||||
|
)
|
||||||
|
|
||||||
|
// 泛型获取方法
|
||||||
|
private inline fun <reified T> get(paramName: String): T {
|
||||||
|
val value = value(paramName)
|
||||||
|
return when (T::class) {
|
||||||
|
Long::class -> convertToLong(value, paramName) as T
|
||||||
|
Int::class -> convertToInt(value, paramName) as T
|
||||||
|
String::class -> value.toString() as T
|
||||||
|
Boolean::class -> convertToBoolean(value, paramName) as T
|
||||||
|
Double::class -> convertToDouble(value, paramName) as T
|
||||||
|
Float::class -> convertToFloat(value, paramName) as T
|
||||||
|
else -> {
|
||||||
|
if (value is T) value
|
||||||
|
else throw typeMismatchError<T>(value, paramName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特定类型方法(向后兼容)
|
||||||
|
fun long(paramName: String): Long = get<Long>(paramName)
|
||||||
|
fun int(paramName: String): Int = get<Int>(paramName)
|
||||||
|
fun string(paramName: String): String = get<String>(paramName)
|
||||||
|
fun boolean(paramName: String): Boolean = get<Boolean>(paramName)
|
||||||
|
fun double(paramName: String): Double = get<Double>(paramName)
|
||||||
|
fun float(paramName: String): Float = get<Float>(paramName)
|
||||||
|
|
||||||
|
// 可选值方法
|
||||||
|
inline fun <reified T> getOrNull(paramName: String): T? =
|
||||||
|
config[paramName] as? T ?: run {
|
||||||
|
val value = config[paramName]
|
||||||
|
if (value == null) null
|
||||||
|
else if (value is T) value
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> getOrDefault(paramName: String, defaultValue: T): T =
|
||||||
|
getOrNull<T>(paramName) ?: defaultValue
|
||||||
|
|
||||||
|
// 类型转换辅助方法
|
||||||
|
fun convertToLong(value: Any, paramName: String): Long = when (value) {
|
||||||
|
is Long -> value
|
||||||
|
is Number -> value.toLong()
|
||||||
|
is String -> try {
|
||||||
|
value.toLong()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw typeMismatchError<Long>(value, paramName)
|
||||||
|
}
|
||||||
|
else -> throw typeMismatchError<Long>(value, paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertToInt(value: Any, paramName: String): Int = when (value) {
|
||||||
|
is Int -> value
|
||||||
|
is Number -> value.toInt()
|
||||||
|
is String -> try {
|
||||||
|
value.toInt()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw typeMismatchError<Int>(value, paramName)
|
||||||
|
}
|
||||||
|
else -> throw typeMismatchError<Int>(value, paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertToBoolean(value: Any, paramName: String): Boolean = when (value) {
|
||||||
|
is Boolean -> value
|
||||||
|
is String -> when (value.lowercase()) {
|
||||||
|
"true", "yes", "1" -> true
|
||||||
|
"false", "no", "0" -> false
|
||||||
|
else -> throw typeMismatchError<Boolean>(value, paramName)
|
||||||
|
}
|
||||||
|
is Number -> value.toInt() != 0
|
||||||
|
else -> throw typeMismatchError<Boolean>(value, paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToDouble(value: Any, paramName: String): Double = when (value) {
|
||||||
|
is Double -> value
|
||||||
|
is Number -> value.toDouble()
|
||||||
|
is String -> try {
|
||||||
|
value.toDouble()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw typeMismatchError<Double>(value, paramName)
|
||||||
|
}
|
||||||
|
else -> throw typeMismatchError<Double>(value, paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToFloat(value: Any, paramName: String): Float = when (value) {
|
||||||
|
is Float -> value
|
||||||
|
is Number -> value.toFloat()
|
||||||
|
is String -> try {
|
||||||
|
value.toFloat()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw typeMismatchError<Float>(value, paramName)
|
||||||
|
}
|
||||||
|
else -> throw typeMismatchError<Float>(value, paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理辅助方法
|
||||||
|
private inline fun <reified T> typeMismatchError(
|
||||||
|
actualValue: Any,
|
||||||
|
paramName: String
|
||||||
|
): Nothing {
|
||||||
|
throw ConfigError(
|
||||||
|
ConfigError.Type.NOT_EXPECTED_VALUE,
|
||||||
|
paramName,
|
||||||
|
T::class.simpleName ?: T::class.java.simpleName,
|
||||||
|
actualValue::class.simpleName ?: actualValue::class.java.simpleName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ data class ToolConfig(
|
||||||
try {
|
try {
|
||||||
rconPassword = "ENC(${CryptoUtil.encrypt(rconPassword!!)})"
|
rconPassword = "ENC(${CryptoUtil.encrypt(rconPassword!!)})"
|
||||||
YamlUpdater.updateYaml(
|
YamlUpdater.updateYaml(
|
||||||
YamlConfigLoader.configFilePath.toString(),
|
YamlConfigLoader.appConfigFilePath.toString(),
|
||||||
"tools.rcon.rcon-password",
|
"tools.rcon.rcon-password",
|
||||||
rconPassword!!
|
rconPassword!!
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ data class WebsocketConfig(
|
||||||
try {
|
try {
|
||||||
encryptedToken = "ENC(${CryptoUtil.encrypt(encryptedToken!!)})"
|
encryptedToken = "ENC(${CryptoUtil.encrypt(encryptedToken!!)})"
|
||||||
YamlUpdater.updateYaml(
|
YamlUpdater.updateYaml(
|
||||||
YamlConfigLoader.configFilePath.toString(),
|
YamlConfigLoader.appConfigFilePath.toString(),
|
||||||
"websocket.encrypted-token",
|
"websocket.encrypted-token",
|
||||||
this.encryptedToken!!
|
this.encryptedToken!!
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,42 +8,49 @@ import org.yaml.snakeyaml.introspector.PropertyUtils
|
||||||
import top.r3944realms.ltdmanager.utils.ConfigInitializer
|
import top.r3944realms.ltdmanager.utils.ConfigInitializer
|
||||||
import top.r3944realms.ltdmanager.utils.NamingConventionUtil
|
import top.r3944realms.ltdmanager.utils.NamingConventionUtil
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
||||||
object YamlConfigLoader {
|
object YamlConfigLoader {
|
||||||
val configFilePath = Paths.get("config/application.yaml") // 配置文件路径
|
val appConfigFilePath: Path = Paths.get("config/application.yaml") // 配置文件路径
|
||||||
private val _config by lazy { loadConfig() } // 延迟初始化
|
val moduleConfigFilePath: Path = Paths.get("config/module.yaml") // 配置文件路径
|
||||||
val config: ConfigWrapper get() = _config
|
private val _app_config by lazy { loadAppConfigWrapper() } // 延迟初始化
|
||||||
|
val appConfig: AppConfigWrapper get() = _app_config
|
||||||
|
private val _module_config by lazy { loadModuleConfigWrapper() } // 延迟初始化
|
||||||
|
val moduleConfig: ModuleConfigWrapper get() = _module_config
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// 第一次启动确保配置文件存在
|
// 第一次启动确保配置文件存在
|
||||||
|
ConfigInitializer.initConfig("module.yaml", "config", false)
|
||||||
ConfigInitializer.initConfig("application.yaml", "config")
|
ConfigInitializer.initConfig("application.yaml", "config")
|
||||||
|
|
||||||
// 初始化后加密(确保只执行一次)
|
// 初始化后加密(确保只执行一次)
|
||||||
runCatching {
|
runCatching {
|
||||||
ensureConfigEncrypted(_config)
|
ensureConfigEncrypted(_app_config)
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
println("初始化加密失败: ${e.message}")
|
println("初始化加密失败: ${e.message}")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun ensureConfigEncrypted(config: ConfigWrapper?) {
|
private fun ensureConfigEncrypted(config: AppConfigWrapper?) {
|
||||||
config?.database?.encryptPassword()
|
config?.database?.encryptPassword()
|
||||||
config?.websocket?.encryptToken()
|
config?.websocket?.encryptToken()
|
||||||
config?.http?.encryptToken()
|
config?.http?.encryptToken()
|
||||||
|
config?.mcsm?.encryptApi()
|
||||||
config?.mail?.encryptPassword()
|
config?.mail?.encryptPassword()
|
||||||
config?.tools?.rcon?.encryptPassword()
|
config?.tools?.rcon?.encryptPassword()
|
||||||
config?.blessingSkinServer?.invitationApi?.encryptToken()
|
config?.blessingSkinServer?.invitationApi?.encryptToken()
|
||||||
|
config?.dgLab?.wsServer?.encryptPassword()
|
||||||
|
config?.imgTu?.encryptPassword()
|
||||||
}
|
}
|
||||||
private fun loadConfig(): ConfigWrapper {
|
private fun loadAppConfigWrapper(): AppConfigWrapper {
|
||||||
if (!Files.exists(configFilePath)) {
|
if (!Files.exists(appConfigFilePath)) {
|
||||||
throw RuntimeException("配置文件未找到: $configFilePath")
|
throw RuntimeException("应用配置文件未找到: $appConfigFilePath")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val yamlContent = Files.readString(configFilePath)
|
val yamlContent = Files.readString(appConfigFilePath)
|
||||||
|
|
||||||
return Yaml(getConstructor()).load(yamlContent)
|
return Yaml(getConstructor(AppConfigWrapper::class.java)).load(yamlContent)
|
||||||
?: throw RuntimeException("YAML解析返回null")
|
?: throw RuntimeException("YAML解析返回null")
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -51,7 +58,23 @@ object YamlConfigLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
private fun getConstructor(): Constructor {
|
private fun loadModuleConfigWrapper(): ModuleConfigWrapper {
|
||||||
|
if (!Files.exists(moduleConfigFilePath)) {
|
||||||
|
throw RuntimeException("模块配置文件未找到: $moduleConfigFilePath")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val yamlContent = Files.readString(moduleConfigFilePath)
|
||||||
|
|
||||||
|
return Yaml(getConstructor(ModuleConfigWrapper::class.java)).load(yamlContent)
|
||||||
|
?: throw RuntimeException("YAML解析返回null")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw RuntimeException("YAML解析失败: ${e.message}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
private fun getConstructor(clazz: Class<*>): Constructor {
|
||||||
val propertyUtils = object : PropertyUtils() {
|
val propertyUtils = object : PropertyUtils() {
|
||||||
override fun getProperty(type: Class<*>, name: String): Property {
|
override fun getProperty(type: Class<*>, name: String): Property {
|
||||||
val processedName = if (name.contains("-")) {
|
val processedName = if (name.contains("-")) {
|
||||||
|
|
@ -63,20 +86,24 @@ object YamlConfigLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Constructor(ConfigWrapper::class.java, LoaderOptions()).apply {
|
return Constructor(clazz, LoaderOptions()).apply {
|
||||||
setPropertyUtils(propertyUtils)
|
setPropertyUtils(propertyUtils)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadDatabaseConfig(): DatabaseConfig = config.database
|
fun loadDatabaseConfig(): DatabaseConfig = appConfig.database
|
||||||
fun loadCryptoConfig(): CryptoConfig = config.crypto
|
fun loadCryptoConfig(): CryptoConfig = appConfig.crypto
|
||||||
fun loadWebsocketConfig(): WebsocketConfig = config.websocket
|
fun loadMcsmConfig(): McsmConfig = appConfig.mcsm
|
||||||
fun loadHttpConfig(): HttpConfig = config.http
|
fun loadWebsocketConfig(): WebsocketConfig = appConfig.websocket
|
||||||
fun loadModeConfig(): ModeConfig = config.mode
|
fun loadHttpConfig(): HttpConfig = appConfig.http
|
||||||
fun loadToolConfig(): ToolConfig = config.tools
|
fun loadModeConfig(): ModeConfig = appConfig.mode
|
||||||
fun loadMailConfig(): MailConfig = config.mail
|
fun loadToolConfig(): ToolConfig = appConfig.tools
|
||||||
fun loadBlessingSkinServerConfig(): BlessingSkinServerConfig = config.blessingSkinServer
|
fun loadMailConfig(): MailConfig = appConfig.mail
|
||||||
data class ConfigWrapper(
|
fun loadBlessingSkinServerConfig(): BlessingSkinServerConfig = appConfig.blessingSkinServer
|
||||||
|
fun loadDgLabConfig(): DgLabConfig = appConfig.dgLab
|
||||||
|
fun loadTuImgConfig(): ImgTuConfig = appConfig.imgTu
|
||||||
|
fun loadModuleConfig(): ModuleConfig = moduleConfig.module
|
||||||
|
data class AppConfigWrapper(
|
||||||
var database: DatabaseConfig = DatabaseConfig(),
|
var database: DatabaseConfig = DatabaseConfig(),
|
||||||
var crypto: CryptoConfig = CryptoConfig(),
|
var crypto: CryptoConfig = CryptoConfig(),
|
||||||
var mode: ModeConfig = ModeConfig(),
|
var mode: ModeConfig = ModeConfig(),
|
||||||
|
|
@ -84,7 +111,13 @@ object YamlConfigLoader {
|
||||||
var http: HttpConfig = HttpConfig(),
|
var http: HttpConfig = HttpConfig(),
|
||||||
var tools: ToolConfig = ToolConfig(),
|
var tools: ToolConfig = ToolConfig(),
|
||||||
var mail: MailConfig = MailConfig(),
|
var mail: MailConfig = MailConfig(),
|
||||||
|
var mcsm: McsmConfig = McsmConfig(),
|
||||||
var blessingSkinServer: BlessingSkinServerConfig = BlessingSkinServerConfig(),
|
var blessingSkinServer: BlessingSkinServerConfig = BlessingSkinServerConfig(),
|
||||||
|
var dgLab: DgLabConfig = DgLabConfig(),
|
||||||
|
var imgTu: ImgTuConfig = ImgTuConfig(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ModuleConfigWrapper(
|
||||||
|
var module: ModuleConfig = ModuleConfig(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
151
src/main/kotlin/top/r3944realms/ltdmanager/dglab/DgLab.kt
Normal file
151
src/main/kotlin/top/r3944realms/ltdmanager/dglab/DgLab.kt
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
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
|
||||||
|
import com.r3944realms.dg_lab.api.websocket.message.role.WebSocketServerRole
|
||||||
|
import com.r3944realms.dg_lab.manager.DGPBClientManager
|
||||||
|
import com.r3944realms.dg_lab.manager.DGPBServerManager
|
||||||
|
import com.r3944realms.dg_lab.websocket.PowerBoxWSClient
|
||||||
|
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管理器
|
||||||
|
*/
|
||||||
|
class DgLab {
|
||||||
|
// 可空,延迟初始化
|
||||||
|
internal var serverManager: ServerManager? = null
|
||||||
|
get() = field
|
||||||
|
|
||||||
|
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<Long, String>) {
|
||||||
|
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(SERVER_ROLE_NAME))
|
||||||
|
.operation(operation)
|
||||||
|
.sharedData(ServerPowerBoxSharedData())
|
||||||
|
.build()
|
||||||
|
if (loadDgLabConfig.wsServer.localServerSecure) {
|
||||||
|
boxWSServer.enableSSL(Path(loadDgLabConfig.wsServer.localServerSslCert).toFile(), Path(loadDgLabConfig.wsServer.localServerSslKey).toFile(), loadDgLabConfig.wsServer.decryptedLocalServerSslPassword)
|
||||||
|
}
|
||||||
|
val dgpbServerManager = DGPBServerManager(boxWSServer)
|
||||||
|
return dgpbServerManager
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 服务器管理类
|
||||||
|
*/
|
||||||
|
fun initServerManager(server: DGPBServerManager) {
|
||||||
|
serverManager = ServerManager(server)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 初始化 客户端管理类
|
||||||
|
*/
|
||||||
|
fun initClientManager() {
|
||||||
|
clientManager = ClientManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加 客户端管理类
|
||||||
|
*/
|
||||||
|
fun addClient(key: String, client: DGPBClientManager) {
|
||||||
|
clientManager?.addClient(key, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除 客户端管理类
|
||||||
|
*/
|
||||||
|
fun removeClient(key: String) {
|
||||||
|
clientManager?.removeClient(key)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取 服务器管理类
|
||||||
|
*/
|
||||||
|
@Throws(IllegalStateException::class)
|
||||||
|
fun getServer(): DGPBServerManager {
|
||||||
|
return serverManager?.getInstance() ?: throw IllegalStateException("Server is not initialized")
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取 客户端管理类
|
||||||
|
*/
|
||||||
|
fun getClient(key: String): DGPBClientManager? {
|
||||||
|
return clientManager?.getClient(key)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取 & 创建 客户端管理类
|
||||||
|
*/
|
||||||
|
fun getClientOrCreate(key: String, operation: ClientOperation): DGPBClientManager {
|
||||||
|
val client = getClient(key)
|
||||||
|
if (client == null) {
|
||||||
|
val loadDgLabConfig = YamlConfigLoader.loadDgLabConfig()
|
||||||
|
val boxWSClient = PowerBoxWSClient.Builder.getBuilder()
|
||||||
|
.address(loadDgLabConfig.wsServer.localServerUrl)
|
||||||
|
.port(loadDgLabConfig.wsServer.localServerPort)
|
||||||
|
.role(WebSocketClientRole("QQ-$key"))
|
||||||
|
.operation(operation)
|
||||||
|
.sharedData(ClientPowerBoxSharedData())
|
||||||
|
.useRoleMsgMode(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
if (loadDgLabConfig.wsServer.localServerSecure) {
|
||||||
|
boxWSClient.enableSSL()
|
||||||
|
}
|
||||||
|
val clientManager = DGPBClientManager(
|
||||||
|
boxWSClient
|
||||||
|
)
|
||||||
|
this.clientManager?.addClient(key, clientManager)
|
||||||
|
return clientManager
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package top.r3944realms.ltdmanager.dglab.manager
|
||||||
|
|
||||||
|
import com.r3944realms.dg_lab.manager.DGPBClientManager
|
||||||
|
|
||||||
|
class ClientManager(
|
||||||
|
private val clients: MutableMap<String, DGPBClientManager> = mutableMapOf(),
|
||||||
|
) : IManager<MutableMap<String, DGPBClientManager>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加单例客户端管理示例
|
||||||
|
* @param key 唯一标识客户端管理的 key,比如 ID 或 name
|
||||||
|
*/
|
||||||
|
fun addClient(key: String, client: DGPBClientManager) {
|
||||||
|
clients[key] = client
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除单例客户端管理实例
|
||||||
|
*/
|
||||||
|
fun removeClient(key: String) {
|
||||||
|
clients.remove(key)?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 key 获取客户端
|
||||||
|
*/
|
||||||
|
fun getClient(key: String): DGPBClientManager? {
|
||||||
|
return clients[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动所有客户端
|
||||||
|
*/
|
||||||
|
override fun startAll() {
|
||||||
|
clients.values.forEach { it.start() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止所有客户端
|
||||||
|
*/
|
||||||
|
override fun stopAll() {
|
||||||
|
clients.values.forEach { it.stop() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取内部 Map 实例
|
||||||
|
*/
|
||||||
|
override fun getInstance(): MutableMap<String, DGPBClientManager> {
|
||||||
|
return clients
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package top.r3944realms.ltdmanager.dglab.manager
|
||||||
|
|
||||||
|
interface IManager<T> {
|
||||||
|
fun startAll()
|
||||||
|
fun stopAll()
|
||||||
|
fun getInstance(): T?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package top.r3944realms.ltdmanager.dglab.manager
|
||||||
|
|
||||||
|
import com.r3944realms.dg_lab.api.manager.IDGLabManager
|
||||||
|
import com.r3944realms.dg_lab.api.manager.Status
|
||||||
|
import com.r3944realms.dg_lab.api.websocket.sharedData.ISharedData
|
||||||
|
import com.r3944realms.dg_lab.manager.DGPBServerManager
|
||||||
|
|
||||||
|
class ServerManager(
|
||||||
|
private val server: DGPBServerManager
|
||||||
|
) : IManager<DGPBServerManager>, IDGLabManager {
|
||||||
|
|
||||||
|
override fun startAll() {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopAll() {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
server.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
server.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSharedData(): ISharedData {
|
||||||
|
return server.sharedData
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(): Status {
|
||||||
|
return server.status
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setStatus(p0: Status?) {
|
||||||
|
server.status = p0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInstance(): DGPBServerManager {
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
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.chevereto.response.FailedCheveretoResponse
|
||||||
|
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||||
|
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 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() {
|
||||||
|
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() {
|
||||||
|
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(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() {
|
||||||
|
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(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() {
|
||||||
|
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?) {
|
||||||
|
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() {
|
||||||
|
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 is CheveretoUploadResponse){
|
||||||
|
napCatClient.sendUnit(
|
||||||
|
SendPrivateMsgRequest(
|
||||||
|
listOf(
|
||||||
|
MessageElement.text("请在60s内绑定APP,否则将自动断开连接"),
|
||||||
|
MessageElement.image(response.image.url, "二维码")
|
||||||
|
),
|
||||||
|
ID.long(playerId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (response is FailedCheveretoResponse.Default){
|
||||||
|
napCatClient.sendUnit(
|
||||||
|
SendPrivateMsgRequest(
|
||||||
|
listOf(
|
||||||
|
MessageElement.text("无法上传图片,请联系管理员:${response.httpStatusCode} , ${response.failedMessage}"),
|
||||||
|
),
|
||||||
|
ID.long(playerId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 启动 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() {
|
||||||
|
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?) {
|
||||||
|
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?) {
|
||||||
|
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?) {
|
||||||
|
// 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?) {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +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(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")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package top.r3944realms.ltdmanager.dglab.model.game
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩家类
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class Player(
|
||||||
|
val id: Long,
|
||||||
|
var name: String,
|
||||||
|
var active: Boolean,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package top.r3944realms.ltdmanager.dglab.model.game
|
||||||
|
|
||||||
|
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<PlayerManager.PlayerState> {
|
||||||
|
@Contextual
|
||||||
|
private val map = ConcurrentHashMap<Long, Player>()
|
||||||
|
@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<Long, Player> = 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<Player> = 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<PlayerState>(fileToRead.readText())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.warn("[dglab] 读取玩家数据&状态失败", e)
|
||||||
|
PlayerState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
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<String, List<Array<IntArray>>>
|
||||||
|
* 每个 int[][] 包含两个长度为 4 的 int 数组,第一个是 frequencies,第二个是 strengths
|
||||||
|
* @return Map<String, PulseWaveList>
|
||||||
|
*/
|
||||||
|
fun convert(customPulseData: Map<String, List<Array<IntArray>>>): Map<String, PulseWaveList> {
|
||||||
|
val pulseWaveLists: MutableMap<String, PulseWaveList> = HashMap()
|
||||||
|
|
||||||
|
for ((name, operations) in customPulseData) {
|
||||||
|
val waveList = PulseWaveList()
|
||||||
|
waveList.name = name
|
||||||
|
|
||||||
|
for (op in operations) {
|
||||||
|
val freqs = op[0]
|
||||||
|
val strengths = op[1]
|
||||||
|
|
||||||
|
// 确保每个数组长度为4
|
||||||
|
require(!(freqs.size != 4 || strengths.size != 4)) { "每个波形段必须包含 4 个频率和 4 个强度值" }
|
||||||
|
|
||||||
|
// 转换频率为 Dg-Lab 格式
|
||||||
|
val convertedFreqs = convertFrequencies(freqs)
|
||||||
|
|
||||||
|
val wave = PulseWave.fromArrays(convertedFreqs, strengths)
|
||||||
|
waveList.add(wave)
|
||||||
|
}
|
||||||
|
|
||||||
|
pulseWaveLists[name] = waveList
|
||||||
|
}
|
||||||
|
|
||||||
|
return pulseWaveLists
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换单个 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(convertFrequency(f1), convertFrequency(f2), convertFrequency(f3), convertFrequency(f4)),
|
||||||
|
intArrayOf(s1, s2, s3, s4)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun PulseWaveList.toSerializable(): PulseWaveListSerializable =
|
||||||
|
PulseWaveListSerializable(name, list.map { it.toSerializable() }.toMutableList())
|
||||||
|
|
||||||
|
fun PulseWaveListSerializable.toPulseWaveList(): PulseWaveList {
|
||||||
|
val listObj = PulseWaveList()
|
||||||
|
listObj.setName(name)
|
||||||
|
list.forEach { listObj.add(it.toPulseWave()) }
|
||||||
|
return listObj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,345 @@
|
||||||
|
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 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<String, PulseWaveList> {
|
||||||
|
return mapOf(
|
||||||
|
"呼吸" to Breath,
|
||||||
|
"潮汐" to Tide,
|
||||||
|
"连击" to Combo,
|
||||||
|
"快速按捏" to FastPinch,
|
||||||
|
"按捏渐强" to PinchGradual,
|
||||||
|
"心跳节奏" to Heartbeat,
|
||||||
|
"压缩" to Compress,
|
||||||
|
"节奏步伐" to RhythmStep,
|
||||||
|
"颗粒摩擦" to GranularFriction,
|
||||||
|
"渐变弹跳" to GradualBounce,
|
||||||
|
"波浪涟漪" to WaveRipple,
|
||||||
|
"雨水冲刷" to RainWash,
|
||||||
|
"变速敲击" to SpeedHit,
|
||||||
|
"信号灯" to SignalLight,
|
||||||
|
"挑逗1" to Tease1,
|
||||||
|
"挑逗2" to Tease2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val Breath: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "呼吸"
|
||||||
|
|
||||||
|
// 每段频率和强度
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 5, 10, 20)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(20, 25, 30, 40)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(40, 45, 50, 60)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(60, 65, 70, 80)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(0, 0, 0, 0), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(0, 0, 0, 0), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(0, 0, 0, 0), intArrayOf(0, 0, 0, 0))
|
||||||
|
)
|
||||||
|
|
||||||
|
// 转成 PulseWave 并加入列表
|
||||||
|
for (seg in segments) {
|
||||||
|
list.add(createWaveSegment(seg[0], seg[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
list
|
||||||
|
}
|
||||||
|
val Tide: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "潮汐"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 4, 8, 17)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(17, 21, 25, 33)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(50, 50, 50, 50)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(50, 54, 58, 67)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(67, 71, 75, 83)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 98, 96, 92)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(92, 90, 88, 84)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
val Combo: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "连击"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 92, 84, 67)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(67, 58, 50, 33)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
val FastPinch: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "快速按捏"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
val PinchGradual: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "按捏渐强"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(29, 29, 29, 29)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(52, 52, 52, 52)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(2, 2, 2, 2)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(73, 73, 73, 73)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(87, 87, 87, 87)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
val Heartbeat: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "心跳节奏"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(110, 110, 110, 110), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(110, 110, 110, 110), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(75, 75, 75, 75)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(75, 77, 79, 83)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(83, 85, 88, 92)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
val Compress: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "压缩"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(25, 25, 24, 24), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(24, 23, 23, 23), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(22, 22, 22, 21), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(21, 21, 20, 20), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(20, 19, 19, 19), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(18, 18, 18, 17), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(17, 16, 16, 16), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(15, 15, 15, 14), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(14, 14, 13, 13), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(13, 12, 12, 12), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(11, 11, 11, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
val RhythmStep: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "节奏步伐"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 5, 10, 20)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(20, 25, 30, 40)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(40, 45, 50, 60)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(60, 65, 70, 80)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 6, 12, 25)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(25, 31, 38, 50)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(50, 56, 62, 75)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 8, 16, 33)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(33, 42, 50, 67)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 12, 25, 50)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
val GranularFriction: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "颗粒摩擦"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
val GradualBounce: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "渐变弹跳"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(1, 1, 1, 1)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(1, 9, 18, 34)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(34, 42, 50, 67)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
val WaveRipple: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "波浪涟漪"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(1, 1, 1, 1)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(1, 3, 7, 13)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(13, 25, 40, 60)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(60, 75, 90, 100)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
val RainWash: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "雨水冲刷"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 5, 15, 30)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(40, 50, 60, 70)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
val SpeedHit: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "变速敲击"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(15, 15, 15, 15), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(20, 20, 20, 20), intArrayOf(50, 50, 50, 50)),
|
||||||
|
arrayOf(intArrayOf(25, 25, 25, 25), intArrayOf(100, 100, 100, 100)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
val SignalLight: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "信号灯"
|
||||||
|
val segments = listOf(
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
|
||||||
|
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
val Tease1: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "挑逗1"
|
||||||
|
val segments = listOf(
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
val Tease2: PulseWaveList by lazy {
|
||||||
|
val list = PulseWaveList()
|
||||||
|
list.name = "挑逗2"
|
||||||
|
val segments = listOf(
|
||||||
|
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(createWaveSegment(it[0], it[1])) }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
package top.r3944realms.ltdmanager.dglab.model.pulseware
|
||||||
|
|
||||||
|
class PulseWaveClassTransform {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package top.r3944realms.ltdmanager.dglab.model.pulseware
|
||||||
|
|
||||||
|
import com.r3944realms.dg_lab.api.message.data.PulseWaveList
|
||||||
|
import kotlinx.serialization.builtins.MapSerializer
|
||||||
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.r3944realms.ltdmanager.dglab.model.pulseware.CustomPulseDataConverter.toPulseWaveList
|
||||||
|
import top.r3944realms.ltdmanager.dglab.model.pulseware.CustomPulseDataConverter.toSerializable
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object PulseWaveJsonIO {
|
||||||
|
private val json = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveToFile(map: Map<String, PulseWaveList>, file: File) {
|
||||||
|
val serializableMap = map.mapValues { it.value.toSerializable() }
|
||||||
|
file.writeText(json.encodeToString(serializableMap))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadFromFile(file: File): Map<String, PulseWaveList> {
|
||||||
|
if (!file.exists()) return emptyMap()
|
||||||
|
val type = MapSerializer(String.serializer(), PulseWaveListSerializable.serializer())
|
||||||
|
val data: Map<String, PulseWaveListSerializable> = json.decodeFromString(type, file.readText())
|
||||||
|
return data.mapValues { it.value.toPulseWaveList() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package top.r3944realms.ltdmanager.dglab.model.pulseware
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PulseWaveListSerializable(
|
||||||
|
var name: String = "",
|
||||||
|
val list: MutableList<PulseWaveSerializable> = mutableListOf()
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package top.r3944realms.ltdmanager.dglab.model.pulseware
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PulseWaveSerializable(
|
||||||
|
val f1: Int, val f2: Int, val f3: Int, val f4: Int,
|
||||||
|
val s1: Int, val s2: Int, val s3: Int, val s4: Int
|
||||||
|
)
|
||||||
|
|
@ -1,86 +1,165 @@
|
||||||
package top.r3944realms.ltdmanager
|
package top.r3944realms.ltdmanager
|
||||||
|
|
||||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
import top.r3944realms.ltdmanager.module.McServerStatusModule
|
|
||||||
import top.r3944realms.ltdmanager.module.*
|
import top.r3944realms.ltdmanager.module.*
|
||||||
|
|
||||||
|
// DSL
|
||||||
fun main() = GlobalManager.runBlockingMain {
|
fun main() = GlobalManager.runBlockingMain {
|
||||||
val groupId:Long = 538751386
|
GlobalManager.initApplication()
|
||||||
val selfQQId = 3327379836
|
// val commonGroupId:Long = 538751386
|
||||||
val selfNickName = "闲趣老土豆"
|
// val whitelistGroupId:Long = 920719236
|
||||||
// 创建模块实例
|
// val selfQQId = 3327379836
|
||||||
val groupModule = GroupRequestHandlerModule(
|
// val selfNickName = "闲趣老土豆"
|
||||||
client = GlobalManager.napCatClient,
|
// // 创建模块实例
|
||||||
targetGroupId = groupId
|
// val groupModule = GroupRequestHandlerModule(
|
||||||
)
|
// moduleName = "WhiteListGroup",
|
||||||
val groupMsgPollingModule = GroupMessagePollingModule(
|
// client = GlobalManager.napCatClient,
|
||||||
targetGroupId = groupId,
|
// targetGroupId = whitelistGroupId
|
||||||
pollIntervalMillis = 5_000L,
|
// )
|
||||||
msgHistoryCheck = 15
|
// val commonGroupMsgPollingModule = GroupMessagePollingModule(
|
||||||
)
|
// moduleName = "CommonGroupMsgPolling",
|
||||||
val toolConfig = YamlConfigLoader.loadToolConfig()
|
// targetGroupId = commonGroupId,
|
||||||
val rconModule = RconPlayerListModule(
|
// pollIntervalMillis = 5_000L,
|
||||||
groupMessagePollingModule = groupMsgPollingModule,
|
// msgHistoryCheck = 15
|
||||||
rconTimeOut = 2_000L,
|
// )
|
||||||
cooldownMillis = 10_000L,
|
// val whiteListGroupMsgPollingModule = GroupMessagePollingModule(
|
||||||
selfId = selfQQId,
|
// moduleName = "WhiteListGroup",
|
||||||
selfNickName = selfNickName,
|
// targetGroupId = whitelistGroupId,
|
||||||
rconPath = toolConfig.rcon.mcRconToolPath.toString(),
|
// pollIntervalMillis = 5_000L,
|
||||||
rconConfigPath = toolConfig.rcon.mcRconToolConfigPath.toString(),
|
// msgHistoryCheck = 15
|
||||||
keywords = setOf(
|
// )
|
||||||
//形容
|
// val commonHelpModule = HelpModule(
|
||||||
"土豆", "马铃薯", "Potato", "potato", "POTATO",
|
// moduleName = "CommonGroup",
|
||||||
"Potatoes", "potatoes", "POTATOES", "🥔",
|
// keywords = listOf("help", "帮助"),
|
||||||
//正经
|
// groupMessagePollingModule = commonGroupMsgPollingModule,
|
||||||
"列表","服务器状态", "TPS", "tps", "list", "List"
|
// selfId = selfQQId,
|
||||||
)
|
// selfNickName = selfNickName,
|
||||||
)
|
// )
|
||||||
val mailConfig = YamlConfigLoader.loadMailConfig()
|
// val whitelistHelpModule = HelpModule(
|
||||||
val mailModule = MailModule(
|
// moduleName = "WhiteListGroup",
|
||||||
host = mailConfig.host.toString(),
|
// keywords = listOf("help", "帮助"),
|
||||||
authToken = mailConfig.decryptedPassword.toString(),
|
// groupMessagePollingModule = whiteListGroupMsgPollingModule,
|
||||||
port = mailConfig.port!!,
|
// selfId = selfQQId,
|
||||||
senderEmailAddress = mailConfig.mailAddress!!,
|
// selfNickName = selfNickName,
|
||||||
)
|
// )
|
||||||
val blessingSkinConfig = YamlConfigLoader.loadBlessingSkinServerConfig()
|
// val toolConfig = YamlConfigLoader.loadToolConfig()
|
||||||
val invitationCodesModule = InvitationCodesModule(
|
// val corconModule = RconPlayerListModule(
|
||||||
groupMessagePollingModule = groupMsgPollingModule,
|
// moduleName = "CommonGroup",
|
||||||
mailModule = mailModule,
|
// groupMessagePollingModule = commonGroupMsgPollingModule,
|
||||||
apiToken = blessingSkinConfig.invitationApi?.decryptedToken!!,
|
// rconTimeOut = 2_000L,
|
||||||
selfId = selfQQId,
|
// cooldownMillis = 10_000L,
|
||||||
keywords = setOf(
|
// selfId = selfQQId,
|
||||||
"申请皮肤站注册邀请码",
|
// selfNickName = selfNickName,
|
||||||
"申请土豆服务器注册邀请码",
|
// rconPath = toolConfig.rcon.mcRconToolPath.toString(),
|
||||||
"申请LTD邀请码",
|
// rconConfigPath = toolConfig.rcon.mcRconToolConfigPath.toString(),
|
||||||
"Apply for an invitation code"
|
// keywords = setOf(
|
||||||
)
|
// //形容
|
||||||
)
|
// "土豆", "马铃薯", "Potato", "potato", "POTATO",
|
||||||
val mcServerStatusModule = McServerStatusModule(
|
// "Potatoes", "potatoes", "POTATOES", "🥔",
|
||||||
groupMessagePollingModule = groupMsgPollingModule,
|
// //正经
|
||||||
selfId = selfQQId,
|
// "列表","服务器状态", "TPS", "tps", "list", "List"
|
||||||
cooldownSeconds = 20,
|
// )
|
||||||
selfNickName = selfNickName,
|
// )
|
||||||
commands = listOf("/m", "/mcs", "seek", "s"),
|
// val rconModule = RconPlayerListModule(
|
||||||
presetServer = mapOf(
|
// moduleName = "WhiteListGroup",
|
||||||
setOf("先行土豆", "先行", "pre", "Pre", "BF", "bf", "p", "P") to "n2.akiracloud.net:10599",
|
// groupMessagePollingModule = whiteListGroupMsgPollingModule,
|
||||||
setOf("土豆", "老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
// rconTimeOut = 2_000L,
|
||||||
)
|
// cooldownMillis = 10_000L,
|
||||||
)
|
// selfId = selfQQId,
|
||||||
|
// selfNickName = selfNickName,
|
||||||
// 注册模块到全局模块管理器
|
// rconPath = toolConfig.rcon.mcRconToolPath.toString(),
|
||||||
GlobalManager.moduleManager.registerModule(groupModule)
|
// rconConfigPath = toolConfig.rcon.mcRconToolConfigPath.toString(),
|
||||||
GlobalManager.moduleManager.registerModule(groupMsgPollingModule)
|
// keywords = setOf(
|
||||||
GlobalManager.moduleManager.registerModule(mcServerStatusModule)
|
// //形容
|
||||||
GlobalManager.moduleManager.registerModule(rconModule)
|
// "土豆", "马铃薯", "Potato", "potato", "POTATO",
|
||||||
GlobalManager.moduleManager.registerModule(mailModule)
|
// "Potatoes", "potatoes", "POTATOES", "🥔",
|
||||||
GlobalManager.moduleManager.registerModule(invitationCodesModule)
|
// //正经
|
||||||
|
// "列表","服务器状态", "TPS", "tps", "list", "List"
|
||||||
// 加载模块
|
// )
|
||||||
GlobalManager.moduleManager.loadModule(groupModule.name)
|
// )
|
||||||
GlobalManager.moduleManager.loadModule(groupMsgPollingModule.name)
|
//// val mailConfig = YamlConfigLoader.loadMailConfig()
|
||||||
GlobalManager.moduleManager.loadModule(mcServerStatusModule.name)
|
//// val mailModule = MailModule(
|
||||||
GlobalManager.moduleManager.loadModule(rconModule.name)
|
//// moduleName = "WhiteListGroup",
|
||||||
GlobalManager.moduleManager.loadModule(mailModule.name)
|
//// host = mailConfig.host.toString(),
|
||||||
GlobalManager.moduleManager.loadModule(invitationCodesModule.name)
|
//// 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", "test"),
|
||||||
|
//// presetServer = mapOf(
|
||||||
|
//// setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
||||||
|
//// setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
|
||||||
|
//// )
|
||||||
|
//// )
|
||||||
|
//// val whitelistMcServerStatusModule = McServerStatusModule(
|
||||||
|
//// moduleName = "WhiteListGroup",
|
||||||
|
//// groupMessagePollingModule = whiteListGroupMsgPollingModule,
|
||||||
|
//// selfId = selfQQId,
|
||||||
|
//// 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")
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // 注册模块到全局模块管理器
|
||||||
|
// GlobalManager.moduleManager.register(groupModule)
|
||||||
|
// GlobalManager.moduleManager.register(commonGroupMsgPollingModule)
|
||||||
|
// GlobalManager.moduleManager.register(whiteListGroupMsgPollingModule)
|
||||||
|
//// GlobalManager.moduleManager.registerModule(commonMcServerStatusModule)
|
||||||
|
// GlobalManager.moduleManager.register(rconModule)
|
||||||
|
// GlobalManager.moduleManager.register(corconModule)
|
||||||
|
//// GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule)
|
||||||
|
//// GlobalManager.moduleManager.registerModule(mailModule)
|
||||||
|
//// GlobalManager.moduleManager.registerModule(invitationCodesModule)
|
||||||
|
// GlobalManager.moduleManager.register(whitelistHelpModule)
|
||||||
|
// GlobalManager.moduleManager.register(commonHelpModule)
|
||||||
|
// GlobalManager.moduleManager.register(dgLabModule)
|
||||||
|
//// GlobalManager.moduleManager.registerModule(banModule)
|
||||||
|
//// GlobalManager.moduleManager.registerModule(modGroupHandlerModule)
|
||||||
|
//
|
||||||
|
// // 加载模块
|
||||||
|
// GlobalManager.moduleManager.load(groupModule.name)
|
||||||
|
// GlobalManager.moduleManager.load(commonGroupMsgPollingModule.name)
|
||||||
|
// GlobalManager.moduleManager.load(whiteListGroupMsgPollingModule.name)
|
||||||
|
//// GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name)
|
||||||
|
// GlobalManager.moduleManager.load(corconModule.name)
|
||||||
|
// GlobalManager.moduleManager.load(rconModule.name)
|
||||||
|
//// GlobalManager.moduleManager.loadModule(mailModule.name)
|
||||||
|
//// GlobalManager.moduleManager.loadModule(invitationCodesModule.name)
|
||||||
|
// GlobalManager.moduleManager.load(commonHelpModule.name)
|
||||||
|
//// GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name)
|
||||||
|
// GlobalManager.moduleManager.load(whitelistHelpModule.name)
|
||||||
|
// GlobalManager.moduleManager.load(dgLabModule.name)
|
||||||
|
//// GlobalManager.moduleManager.loadModule(banModule.name)
|
||||||
|
//// GlobalManager.moduleManager.loadModule(modGroupHandlerModule.name)
|
||||||
}
|
}
|
||||||
211
src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt
Normal file
211
src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
package top.r3944realms.ltdmanager.mcms
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.engine.cio.*
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
|
import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.ResponseResult
|
||||||
|
import top.r3944realms.ltdmanager.utils.Environment
|
||||||
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class MCSMClient private constructor() : AutoCloseable {
|
||||||
|
private val client = HttpClient(CIO) {
|
||||||
|
expectSuccess = false
|
||||||
|
|
||||||
|
// 安装 HttpTimeout 插件
|
||||||
|
install(HttpTimeout) {
|
||||||
|
// 默认超时配置,会被具体请求的配置覆盖
|
||||||
|
requestTimeoutMillis = 30000
|
||||||
|
connectTimeoutMillis = 10000
|
||||||
|
socketTimeoutMillis = 15000
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private val blessingSkinServerConfig = YamlConfigLoader.loadBlessingSkinServerConfig()
|
||||||
|
|
||||||
|
// 限流控制
|
||||||
|
private val semaphore = Semaphore(5)
|
||||||
|
private val requestMutex = Mutex()
|
||||||
|
private val requestQueue = PriorityQueue<MCSMSkinQueueItem<MCSMResponse, FailedMCSMResponse>>(compareBy { it.priority })
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
|
init {
|
||||||
|
startQueueProcessor()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交请求
|
||||||
|
*/
|
||||||
|
suspend fun <T : MCSMResponse, F : FailedMCSMResponse> submitRequest(
|
||||||
|
request: MCSMRequest<T, F>,
|
||||||
|
priority: Int = 5,
|
||||||
|
maxRetries: Int = 3
|
||||||
|
): ResponseResult<T, F> {
|
||||||
|
val deferred = CompletableDeferred<ResponseResult<T, F>>()
|
||||||
|
requestMutex.withLock {
|
||||||
|
requestQueue.add(MCSMSkinQueueItem(request, deferred, priority, maxRetries, true))
|
||||||
|
}
|
||||||
|
return deferred.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动队列处理器
|
||||||
|
*/
|
||||||
|
private fun startQueueProcessor() {
|
||||||
|
scope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
val item = requestMutex.withLock {
|
||||||
|
requestQueue.poll()
|
||||||
|
}
|
||||||
|
if (item == null) {
|
||||||
|
delay(50)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processQueueItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理队列项
|
||||||
|
*/
|
||||||
|
private suspend fun processQueueItem(item: MCSMSkinQueueItem<MCSMResponse, FailedMCSMResponse>) {
|
||||||
|
semaphore.withPermit {
|
||||||
|
val (request, deferred, _, maxRetries, _) = item
|
||||||
|
var attempt = 0
|
||||||
|
var lastError: Exception? = null
|
||||||
|
|
||||||
|
while (attempt < maxRetries) {
|
||||||
|
try {
|
||||||
|
// 构建完整的URL,包括查询参数
|
||||||
|
val fullUrl = buildFullUrlWithQueryParams(request)
|
||||||
|
|
||||||
|
if (!Environment.isProduction()) {
|
||||||
|
LoggerUtil.logger.debug("发送请求到: $fullUrl")
|
||||||
|
LoggerUtil.logger.debug("请求方法: {}", request.method())
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = client.request(fullUrl) {
|
||||||
|
method = request.method()
|
||||||
|
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
headers {
|
||||||
|
request.headers().invoke(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于非GET请求,设置请求体
|
||||||
|
if (request.method() != HttpMethod.Get) {
|
||||||
|
setBody(request.toJSON())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseText: String = response.body()
|
||||||
|
|
||||||
|
if (!Environment.isProduction()) {
|
||||||
|
LoggerUtil.logger.debug("响应状态: {}", response.status)
|
||||||
|
LoggerUtil.logger.debug("响应内容: $responseText")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是HTML响应(重定向)
|
||||||
|
if (isHtmlResponse(responseText)) {
|
||||||
|
throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
val result = request.getResponse(responseText, response.status)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(deferred as CompletableDeferred<ResponseResult<MCSMResponse, FailedMCSMResponse>>).complete(result)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
lastError = e
|
||||||
|
attempt++
|
||||||
|
|
||||||
|
if (!request.shouldRetryOnFailure() || attempt >= maxRetries) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerUtil.logger.warn("MCSM请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
||||||
|
delay((attempt * 1000L)) // 指数退避
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有重试都失败或不应重试
|
||||||
|
val errorResponse = createFailureResponse(lastError)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(deferred as CompletableDeferred<ResponseResult<MCSMResponse, FailedMCSMResponse>>).complete(
|
||||||
|
ResponseResult.Failure(errorResponse)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建完整的URL,包含查询参数
|
||||||
|
*/
|
||||||
|
private fun buildFullUrlWithQueryParams(request: MCSMRequest<*, *>): String {
|
||||||
|
val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/")
|
||||||
|
val path = request.path().removePrefix("/")
|
||||||
|
|
||||||
|
// 构建基础URL
|
||||||
|
val urlBuilder = StringBuilder("$baseUrl/$path")
|
||||||
|
|
||||||
|
// 添加查询参数
|
||||||
|
val queryParams = request.queryParameters().entries.joinToString("&") { (key, value) ->
|
||||||
|
"${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryParams.isNotEmpty()) {
|
||||||
|
urlBuilder.append("?").append(queryParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlBuilder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否是HTML响应
|
||||||
|
*/
|
||||||
|
private fun isHtmlResponse(text: String): Boolean {
|
||||||
|
return text.contains("<!DOCTYPE html>", ignoreCase = true) ||
|
||||||
|
text.contains("<html>", ignoreCase = true) ||
|
||||||
|
text.contains("Redirecting", ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建失败响应
|
||||||
|
*/
|
||||||
|
private fun createFailureResponse(
|
||||||
|
exception: Exception?
|
||||||
|
): FailedMCSMResponse {
|
||||||
|
return FailedMCSMResponse.ExceptionFailedMCSMResponse(
|
||||||
|
result = exception?.message ?: "未知错误",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
scope.cancel()
|
||||||
|
runBlocking {
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(): MCSMClient = MCSMClient()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package top.r3944realms.ltdmanager.mcms
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||||
|
|
||||||
|
data class MCSMSkinQueueItem<out T:MCSMResponse,out F:FailedMCSMResponse>(
|
||||||
|
val request: MCSMRequest<T,F>,
|
||||||
|
val deferred: CompletableDeferred<*>,
|
||||||
|
var retries: Int,
|
||||||
|
val priority: Int,
|
||||||
|
val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit
|
||||||
|
) : Comparable<MCSMSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
|
||||||
|
override fun compareTo(other: MCSMSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package top.r3944realms.ltdmanager.mcms.request
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.ResponseResult
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class MCSMRequest<out T : MCSMResponse, out F : FailedMCSMResponse>(
|
||||||
|
@Transient
|
||||||
|
open val createTime: Long = System.currentTimeMillis()
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* 转换为JSON字符串
|
||||||
|
*/
|
||||||
|
abstract fun toJSON(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取API路径(不包含基础URL)
|
||||||
|
* 例如: "invitation-codes/generate"
|
||||||
|
*/
|
||||||
|
abstract fun path(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取HTTP方法,默认为GET(因为大多数API使用GET+查询参数)
|
||||||
|
*/
|
||||||
|
open fun method(): HttpMethod = HttpMethod.Get
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义请求头
|
||||||
|
*/
|
||||||
|
open fun headers(): HeadersBuilder.() -> Unit = {
|
||||||
|
// 默认添加Content-Type
|
||||||
|
append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
// 添加Accept头
|
||||||
|
append(HttpHeaders.Accept, "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取查询参数(用于URL参数)
|
||||||
|
* 例如: mapOf("token" to "abc123", "amount" to "1")
|
||||||
|
*/
|
||||||
|
open fun queryParameters(): Map<String, String> = emptyMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求体参数(用于POST请求的JSON body)
|
||||||
|
* 例如: mapOf("token" to "abc123", "amount" to 1)
|
||||||
|
*/
|
||||||
|
open fun bodyParameters(): Map<String, Any> = emptyMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求体内容类型,默认为Application.Json
|
||||||
|
*/
|
||||||
|
open fun contentType(): ContentType = ContentType.Application.Json
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析响应JSON字符串
|
||||||
|
* @param responseJson 响应JSON字符串
|
||||||
|
* @param httpStatusCode HTTP状态码
|
||||||
|
*/
|
||||||
|
abstract fun getResponse(responseJson: String, httpStatusCode: HttpStatusCode): ResponseResult<T, F>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预期的成功响应类型名称(用于日志和调试)
|
||||||
|
*/
|
||||||
|
abstract fun expectedResponseType(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预期的失败响应类型名称(用于日志和调试)
|
||||||
|
*/
|
||||||
|
abstract fun expectedFailureType(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要在失败时重试(默认重试)
|
||||||
|
*/
|
||||||
|
open fun shouldRetryOnFailure(): Boolean = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package top.r3944realms.ltdmanager.mcms.request.instance
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.ResponseResult
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.instance.GetInstanceListResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class GetInstanceListRequest(
|
||||||
|
private val daemonId: String,
|
||||||
|
private val page: Int,
|
||||||
|
private val pageSize: Int,
|
||||||
|
private val status: String,
|
||||||
|
private val instanceName: String? = null
|
||||||
|
) : MCSMRequest<GetInstanceListResponse, FailedMCSMResponse>() {
|
||||||
|
|
||||||
|
override fun toJSON(): String = "{}" // GET 无请求体
|
||||||
|
|
||||||
|
override fun path(): String = "api/service/remote_service_instances"
|
||||||
|
|
||||||
|
override fun queryParameters(): Map<String, String> =
|
||||||
|
buildMap {
|
||||||
|
put("daemonId", daemonId)
|
||||||
|
put("page", page.toString())
|
||||||
|
put("page_size", pageSize.toString())
|
||||||
|
put("status", status)
|
||||||
|
instanceName?.let { put("instance_name", it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getResponse(
|
||||||
|
responseJson: String,
|
||||||
|
httpStatusCode: HttpStatusCode
|
||||||
|
): ResponseResult<GetInstanceListResponse, FailedMCSMResponse> {
|
||||||
|
return if (httpStatusCode.value in 200..299) {
|
||||||
|
ResponseResult.Success(
|
||||||
|
kotlinx.serialization.json.Json.decodeFromString<GetInstanceListResponse>(responseJson)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ResponseResult.Failure(
|
||||||
|
kotlinx.serialization.json.Json.decodeFromString<FailedMCSMResponse>(responseJson)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun expectedResponseType(): String = GetInstanceListResponse::class.simpleName!!
|
||||||
|
|
||||||
|
override fun expectedFailureType(): String = FailedMCSMResponse::class.simpleName!!
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package top.r3944realms.ltdmanager.mcms.request.instance
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.ResponseResult
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.instance.StartInstanceResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动实例请求
|
||||||
|
* GET /api/protected_instance/open
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class StartInstanceRequest(
|
||||||
|
val uuid: String,
|
||||||
|
val daemonId: String
|
||||||
|
) : MCSMRequest<StartInstanceResponse, FailedMCSMResponse>() {
|
||||||
|
|
||||||
|
override fun toJSON(): String =
|
||||||
|
Json.encodeToString(this)
|
||||||
|
|
||||||
|
override fun path(): String =
|
||||||
|
"protected_instance/open"
|
||||||
|
|
||||||
|
override fun queryParameters(): Map<String, String> =
|
||||||
|
mapOf(
|
||||||
|
"uuid" to uuid,
|
||||||
|
"daemonId" to daemonId
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun method(): HttpMethod = HttpMethod.Get
|
||||||
|
|
||||||
|
override fun getResponse(
|
||||||
|
responseJson: String,
|
||||||
|
httpStatusCode: HttpStatusCode
|
||||||
|
): ResponseResult<StartInstanceResponse, FailedMCSMResponse> {
|
||||||
|
|
||||||
|
return if (httpStatusCode.value == 200) {
|
||||||
|
val obj = Json.decodeFromString(StartInstanceResponse.serializer(), responseJson)
|
||||||
|
ResponseResult.Success(obj)
|
||||||
|
} else {
|
||||||
|
val fail = Json.decodeFromString(FailedMCSMResponse.serializer(), responseJson)
|
||||||
|
ResponseResult.Failure(fail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun expectedResponseType(): String = "StartInstanceResponse"
|
||||||
|
|
||||||
|
override fun expectedFailureType(): String = "FailedMCSMResponse"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package top.r3944realms.ltdmanager.mcms.response
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
open class FailedMCSMResponse(
|
||||||
|
@Transient
|
||||||
|
val status0: Status = Status.Ok,
|
||||||
|
val data: JsonObject? = null,
|
||||||
|
@Transient
|
||||||
|
val time0: Long = -1,
|
||||||
|
): MCSMResponse(
|
||||||
|
status0, time0
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class ExceptionFailedMCSMResponse(
|
||||||
|
@Transient
|
||||||
|
val status1: Status = Status.Ok,
|
||||||
|
val data0: String? = null,
|
||||||
|
@Transient
|
||||||
|
val time1: Long = -1,
|
||||||
|
@Transient
|
||||||
|
val result: String? = null,
|
||||||
|
): FailedMCSMResponse(
|
||||||
|
status1, null, time1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package top.r3944realms.ltdmanager.mcms.response
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.polymorphic
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.instance.GetInstanceListResponse
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.instance.StartInstanceResponse
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class MCSMResponse (
|
||||||
|
open val status: Status,
|
||||||
|
open val time: Long,
|
||||||
|
@Transient
|
||||||
|
open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||||
|
@Transient
|
||||||
|
open val createTime: Long = System.currentTimeMillis()
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
// 通用的反序列化方法
|
||||||
|
inline fun <reified T : MCSMResponse> decode(jsonString: String): T {
|
||||||
|
return json.decodeFromString(jsonString)
|
||||||
|
}
|
||||||
|
val json: Json by lazy {
|
||||||
|
Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
serializersModule = SerializersModule {
|
||||||
|
polymorphic(MCSMResponse::class) {
|
||||||
|
subclass(GetInstanceListResponse::class, GetInstanceListResponse.serializer())
|
||||||
|
subclass(StartInstanceResponse::class, StartInstanceResponse.serializer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Serializable
|
||||||
|
enum class Status(val value: String) {
|
||||||
|
@SerialName("200") Ok("200"),
|
||||||
|
@SerialName("400") ParamsNotRight("400"),
|
||||||
|
@SerialName("403") PermissionDenied("403"),
|
||||||
|
@SerialName("500") InternalServerError("500");
|
||||||
|
companion object {
|
||||||
|
fun isOk(value: Status): Boolean = value == Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package top.r3944realms.ltdmanager.mcms.response
|
||||||
|
|
||||||
|
// 响应结果封装
|
||||||
|
sealed class ResponseResult<out T : MCSMResponse, out F : FailedMCSMResponse> {
|
||||||
|
data class Success<T : MCSMResponse>(val response: T) : ResponseResult<T, Nothing>()
|
||||||
|
data class Failure<F : FailedMCSMResponse>(val failure: F) : ResponseResult<Nothing, F>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否成功
|
||||||
|
*/
|
||||||
|
fun isSuccess(): Boolean = this is Success
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取成功响应(如果存在)
|
||||||
|
*/
|
||||||
|
fun getSuccessResponse(): T? = (this as? Success)?.response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取失败响应(如果存在)
|
||||||
|
*/
|
||||||
|
fun getFailureResponse(): F? = (this as? Failure)?.failure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功时执行操作
|
||||||
|
*/
|
||||||
|
inline fun onSuccess(action: (T) -> Unit): ResponseResult<T, F> {
|
||||||
|
if (this is Success) action(response)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败时执行操作
|
||||||
|
*/
|
||||||
|
inline fun onFailure(action: (F) -> Unit): ResponseResult<T, F> {
|
||||||
|
if (this is Failure) action(failure)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package top.r3944realms.ltdmanager.mcms.response.instance
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GetInstanceListResponse(
|
||||||
|
@Transient
|
||||||
|
val status0: Status = Status.Ok,
|
||||||
|
val data: InstanceListData? = null,
|
||||||
|
@Transient
|
||||||
|
val time0: Long = -1,
|
||||||
|
) : MCSMResponse(status0, time0) {
|
||||||
|
@Serializable
|
||||||
|
data class InstanceListData(
|
||||||
|
val maxPage: Int,
|
||||||
|
val pageSize: Int,
|
||||||
|
val data: List<InstanceDetail>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InstanceDetail(
|
||||||
|
val config: JsonObject? = null, //TODO: 不清楚是干什么的,需验证
|
||||||
|
val info: InstanceInfo,
|
||||||
|
val instanceUuid: String,
|
||||||
|
val processInfo: ProcessInfo,
|
||||||
|
val space: Long,
|
||||||
|
val started: Int,
|
||||||
|
val status: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InstanceInfo(
|
||||||
|
val currentPlayers: Int,
|
||||||
|
val fileLock: Int,
|
||||||
|
val maxPlayers: Int,
|
||||||
|
val openFrpStatus: Boolean,
|
||||||
|
val playersChart: List<PlayerChartItem>,
|
||||||
|
val version: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerChartItem(
|
||||||
|
val time: Long? = null,
|
||||||
|
val players: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ProcessInfo(
|
||||||
|
val cpu: Double,
|
||||||
|
val memory: Long,
|
||||||
|
val ppid: Long,
|
||||||
|
val pid: Long,
|
||||||
|
val ctime: Long,
|
||||||
|
val elapsed: Long,
|
||||||
|
val timestamp: Long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package top.r3944realms.ltdmanager.mcms.response.instance
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StartInstanceResponse(
|
||||||
|
@Transient
|
||||||
|
val status0: Status = Status.Ok,
|
||||||
|
val data: StartInstanceData,
|
||||||
|
@Transient
|
||||||
|
val time0: Long = -1
|
||||||
|
) : MCSMResponse(status0, time0){
|
||||||
|
@Serializable
|
||||||
|
data class StartInstanceData(
|
||||||
|
val instanceUuid: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
class ApplyWhitelistModule(
|
||||||
|
moduleName: String,
|
||||||
|
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||||
|
private val cooldownMillis: Long = 120_000,
|
||||||
|
private val keywords: Set<String> = setOf("申请白名单")
|
||||||
|
):
|
||||||
|
BaseModule(Modules.APPLY_WHITELIST,moduleName) {
|
||||||
|
override fun onLoad() {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onUnload() {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
344
src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt
Normal file
344
src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.r3944realms.ltdmanager.module.common.CommandParser
|
||||||
|
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.MultiCommandFilter
|
||||||
|
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.request.group.GetGroupShutListRequest
|
||||||
|
import top.r3944realms.ltdmanager.napcat.request.group.SetGroupBanRequest
|
||||||
|
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||||
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指令触发禁言模块
|
||||||
|
*/
|
||||||
|
class BanModule(
|
||||||
|
moduleName: String,
|
||||||
|
private val groupMessagePollingModule : GroupMessagePollingModule,
|
||||||
|
private val selfId: Long,
|
||||||
|
private val adminsId: List<Long> = listOf(),
|
||||||
|
muteCommandPrefixList: List<String> = listOf("mute"), // 默认命令前缀
|
||||||
|
unmuteCommandPrefixList: List<String> = listOf("unmute"),
|
||||||
|
private val minBanMinutes: Int = 1,
|
||||||
|
private val maxBanMinutes: Int = 15,
|
||||||
|
private val factorX: Int = 2, // 系数 x,禁言倍数
|
||||||
|
|
||||||
|
) : BaseModule(Modules.BAN, moduleName), PersistentState<BanModule.BanState> {
|
||||||
|
|
||||||
|
private val banCommandParse = CommandParser(muteCommandPrefixList)
|
||||||
|
private val pardonCommandParse = CommandParser(unmuteCommandPrefixList)
|
||||||
|
private val multiCommandFilter = MultiCommandFilter(listOf(banCommandParse, pardonCommandParse))
|
||||||
|
private val stateFile: File = getStateFileInternal("command_ban_state.json", name)
|
||||||
|
private val stateBackupFile: File = getStateFileInternal("command_ban_state.json.bak", name)
|
||||||
|
private var banState = loadState()
|
||||||
|
override fun getState(): BanState = banState
|
||||||
|
|
||||||
|
private val triggerFilter by lazy {
|
||||||
|
TriggerMessageFilter(
|
||||||
|
listOf(
|
||||||
|
IgnoreSelfFilter(selfId),
|
||||||
|
NewMessageFilter { userId ->
|
||||||
|
banState.getLastTriggerTime(userId) to banState.getLastTriggerRealId(userId)
|
||||||
|
},
|
||||||
|
multiCommandFilter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var scope: CoroutineScope? = null
|
||||||
|
|
||||||
|
|
||||||
|
override fun getStateFileInternal(): File = stateFile
|
||||||
|
|
||||||
|
override fun onLoad() {
|
||||||
|
LoggerUtil.logger.info("[$name] 模块已装载,监听群组: ${groupMessagePollingModule.targetGroupId}")
|
||||||
|
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
scope!!.launch {
|
||||||
|
LoggerUtil.logger.info("[$name] 启动消息监听协程")
|
||||||
|
groupMessagePollingModule.messagesFlow.collect { messages ->
|
||||||
|
handleMessages(messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onUnload() {
|
||||||
|
LoggerUtil.logger.info("[$name] 模块卸载,取消协程")
|
||||||
|
scope?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
|
||||||
|
// 先过一遍过滤器,只有符合条件的才进入后续处理
|
||||||
|
val filtered = triggerFilter.filter(messages)
|
||||||
|
for (msg in filtered) {
|
||||||
|
processBanCommand(msg)
|
||||||
|
processUnBanCommand(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 将 SpecificMsg 中的 message 段拼成一条“可解析文本”。
|
||||||
|
* - text 段直接拼接
|
||||||
|
* - 如果消息段里包含 @(在 MessageData 中为 qq 字段),则拼成 "@{qq}",方便 parseMentionToUserId 解析
|
||||||
|
*/
|
||||||
|
private fun MsgHistorySpecificMsg.plainText(): String {
|
||||||
|
return this.message.joinToString(" ") { seg ->
|
||||||
|
// 如果 message element 包含 qq 字段(即@用户),优先使用它
|
||||||
|
seg.data.qq?.let { "@${it}" } ?: (seg.data.text ?: "")
|
||||||
|
}.trim()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 从消息段中提取所有被 @ 的用户 ID
|
||||||
|
*/
|
||||||
|
private fun MsgHistorySpecificMsg.getMentionedUserIds(): List<ID> {
|
||||||
|
return this.message
|
||||||
|
.filter { it.type == MessageType.At && it.data.qq != null }
|
||||||
|
.mapNotNull { it.data.qq }
|
||||||
|
.distinctBy {
|
||||||
|
when (it) {
|
||||||
|
is ID.StringValue -> it.value
|
||||||
|
is ID.LongValue -> it.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private suspend fun processUnBanCommand(msg: MsgHistorySpecificMsg) {
|
||||||
|
try {
|
||||||
|
pardonCommandParse.parseCommand(msg.plainText()) ?: return
|
||||||
|
// 获取所有被 @ 的用户
|
||||||
|
val mentionedUserIds = msg.getMentionedUserIds().map {
|
||||||
|
when (it) {
|
||||||
|
is ID.StringValue -> it.value.toLong()
|
||||||
|
is ID.LongValue -> it.value
|
||||||
|
}
|
||||||
|
} // List<Long>
|
||||||
|
val send =
|
||||||
|
napCatClient.send<GetGroupShutListEvent>(GetGroupShutListRequest(ID.long(groupMessagePollingModule.targetGroupId)))
|
||||||
|
val muteList = send.data.map { it.uin.toLong() }
|
||||||
|
for (target in mentionedUserIds) {
|
||||||
|
if(target !in muteList) {
|
||||||
|
sendGroupMessage("❌ 目标用户未被禁言",
|
||||||
|
msg.realId
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
banUser(ID.long(target), groupMessagePollingModule.targetGroupId, 0)
|
||||||
|
sendGroupMessage(
|
||||||
|
"✅ 已解禁对方@(${target})",
|
||||||
|
msg.realId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
banState = banState.updateLastTrigger(msg.userId, msg.realId, msg.time)
|
||||||
|
saveState(banState)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.error("[$name] 执行解禁言指令失败", e)
|
||||||
|
sendGroupMessage("❌ 执行解禁言失败,请检查解指令格式或权限", msg.realId)
|
||||||
|
banState = banState.updateLastTrigger(msg.sender.userId, msg.realId, msg.time)
|
||||||
|
saveState(banState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private suspend fun processBanCommand(msg: MsgHistorySpecificMsg) {
|
||||||
|
try {
|
||||||
|
val parsed = banCommandParse.parseCommand(msg.plainText()) ?: return
|
||||||
|
val (_, argument) = parsed
|
||||||
|
|
||||||
|
val parts = argument.split(" ").filter { it.isNotBlank() }
|
||||||
|
|
||||||
|
// 解析禁言时间
|
||||||
|
val durationMinutes = parts.getOrNull(0)?.toIntOrNull()
|
||||||
|
?: Random.nextInt(minBanMinutes, maxBanMinutes + 1)
|
||||||
|
val durationSeconds = durationMinutes.coerceIn(minBanMinutes, maxBanMinutes) * 60
|
||||||
|
|
||||||
|
// 获取所有被 @ 的用户
|
||||||
|
val mentionedUserIds = msg.getMentionedUserIds() // List<ID>
|
||||||
|
val targets = mentionedUserIds.ifEmpty { listOf(ID.long(msg.sender.userId)) }
|
||||||
|
|
||||||
|
for (target in targets) {
|
||||||
|
val targetLongId = when (target) {
|
||||||
|
is ID.StringValue -> target.value.toLong()
|
||||||
|
is ID.LongValue -> target.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限检查:非管理员不能禁言多个他人
|
||||||
|
if (mentionedUserIds.isNotEmpty() && mentionedUserIds.size != 1 && msg.sender.userId !in adminsId) {
|
||||||
|
sendGroupMessage("❌ 你没有权限使用禁言多用户功能", msg.realId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁言机器人跳过
|
||||||
|
if (targetLongId == selfId) {
|
||||||
|
sendGroupMessage("❌ 你没有权限禁言机器人", msg.realId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (targetLongId in adminsId) {
|
||||||
|
sendGroupMessage("❌ 不支持禁言管理员", msg.realId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单 @ 且非自己,可能触发反禁自己
|
||||||
|
if (mentionedUserIds.size == 1 && targetLongId != msg.sender.userId && msg.sender.userId !in adminsId) {
|
||||||
|
val dice = Random.nextInt(1, 7) // 1~6
|
||||||
|
val chance = when (dice) {
|
||||||
|
6 -> 100
|
||||||
|
5 -> 80
|
||||||
|
4 -> 60
|
||||||
|
3 -> 50
|
||||||
|
2 -> 20
|
||||||
|
1 -> 0
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val selfDuration = durationSeconds * factorX
|
||||||
|
if (Random.nextInt(0,100) > chance) {
|
||||||
|
// 触发反禁自己
|
||||||
|
banUser(ID.long(msg.sender.userId), groupMessagePollingModule.targetGroupId, selfDuration)
|
||||||
|
sendGroupMessage(
|
||||||
|
"⚠️ 骰子点数: $dice, 成功概率: ${chance}% → 失败,你触发了反禁,禁言 ${selfDuration / 60} 分钟",
|
||||||
|
msg.realId
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 未触发反禁自己,禁言目标
|
||||||
|
banUser(target, groupMessagePollingModule.targetGroupId, durationSeconds)
|
||||||
|
sendGroupMessage(
|
||||||
|
"✅ 骰子点数: $dice, 成功概率: ${chance}% → 成功禁言 <@${targetLongId}>",
|
||||||
|
msg.realId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 多 @ 或管理员操作,直接禁言目标
|
||||||
|
banUser(target, groupMessagePollingModule.targetGroupId, durationSeconds)
|
||||||
|
sendGroupMessage(
|
||||||
|
if (targetLongId == msg.sender.userId) {
|
||||||
|
"✅ 你已被禁言 ${durationSeconds/ 60} 分钟"
|
||||||
|
} else {
|
||||||
|
"✅ 已禁言 <@${targetLongId}> ${durationSeconds/ 60} 分钟"
|
||||||
|
},
|
||||||
|
msg.realId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
// 更新状态
|
||||||
|
banState = banState.updateLastTrigger(msg.userId, msg.realId, msg.time)
|
||||||
|
saveState(banState)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.error("[$name] 执行禁言指令失败", e)
|
||||||
|
sendGroupMessage("❌ 执行禁言失败,请检查指令格式或权限", msg.realId)
|
||||||
|
banState = banState.updateLastTrigger(msg.sender.userId, msg.realId, msg.time)
|
||||||
|
saveState(banState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun banUser(userId: ID, groupId: Long, seconds: Int) {
|
||||||
|
val request = SetGroupBanRequest(
|
||||||
|
duration = seconds.toDouble(),
|
||||||
|
groupId = ID.long(groupId),
|
||||||
|
userId = userId
|
||||||
|
)
|
||||||
|
napCatClient.sendUnit(request)
|
||||||
|
LoggerUtil.logger.info("[$name] 已对用户 $userId 执行 $seconds 秒禁言")
|
||||||
|
}
|
||||||
|
private suspend fun sendGroupMessage(text: String, replyTo: Long? = null) {
|
||||||
|
val request = SendGroupMsgRequest(
|
||||||
|
MessageElement.reply(ID.long(replyTo ?: 0), text),
|
||||||
|
ID.long(groupMessagePollingModule.targetGroupId)
|
||||||
|
)
|
||||||
|
napCatClient.sendUnit(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun info(): String {
|
||||||
|
return buildString {
|
||||||
|
append("[$name] 指令禁言模块:\n")
|
||||||
|
append(" 管理员用户ID: ${adminsId}\n")
|
||||||
|
append(" - 用户发送 ${banCommandParse.getCommands().joinToString("、")} 来禁言自己或指定其他用户(需管理员权限)。\n")
|
||||||
|
append(" - 支持指定禁言分钟数或随机分钟数,范围 $minBanMinutes-$maxBanMinutes 分钟。\n")
|
||||||
|
append(" - 支持对单个 @ 用户禁言,有概率反禁自己(骰子点数决定概率)。\n")
|
||||||
|
append(" - 管理员可以禁言其他用户;非管理员尝试多个禁言对象会收到无权限提示。\n")
|
||||||
|
append(" - 用户发送 ${pardonCommandParse.getCommands().joinToString("、")} 来解禁指定用户。\n")
|
||||||
|
append(" - 仅支持对单个 @ 用户解禁言。\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun help(): String {
|
||||||
|
return buildString {
|
||||||
|
appendLine("📖 [$name] 使用帮助:")
|
||||||
|
appendLine("指令格式:${banCommandParse.getCommands().joinToString("、")} [分钟] [@用户...]")
|
||||||
|
appendLine("示例:")
|
||||||
|
appendLine(" - <指令> → 随机禁言自己")
|
||||||
|
appendLine(" - <指令> 5 → 禁言自己 5 分钟")
|
||||||
|
appendLine(" - <指令> 4 @User123 → 禁言指定用户 4 分钟(可能失败)")
|
||||||
|
appendLine(" - <指令> 4 @User123 @User22 → 禁言指定多用户 4 分钟(需在程序管理员列表中)")
|
||||||
|
appendLine()
|
||||||
|
appendLine("⚠️ 特殊说明:")
|
||||||
|
appendLine(" - 如果 @ 单个用户且执行者非需在程序管理员,有 y% 概率触发反禁自己,")
|
||||||
|
appendLine(" 骰子点数决定概率:6 → 100%, 5 → 80%, 4 → 60%, 3 → 50%, 2 → 20%, 1 → 0%")
|
||||||
|
appendLine(" - 禁言机器人自身不会生效")
|
||||||
|
appendLine(" - 禁言状态会自动保存以便下次使用")
|
||||||
|
appendLine()
|
||||||
|
appendLine("指令格式:${pardonCommandParse.getCommands().joinToString("、")} [@用户]")
|
||||||
|
appendLine("示例:")
|
||||||
|
appendLine(" - <指令> @User123 → 解禁指定用户")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 持久化 ----------------
|
||||||
|
@Serializable
|
||||||
|
data class UserBanDetail(
|
||||||
|
val realId: Long,
|
||||||
|
val time: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BanState(
|
||||||
|
val map: Map<Long, UserBanDetail> = emptyMap()
|
||||||
|
) {
|
||||||
|
fun getLastTriggerTime(userId: Long): Long = map[userId]?.time ?: -1
|
||||||
|
fun getLastTriggerRealId(userId: Long): Long = map[userId]?.realId ?: -1
|
||||||
|
|
||||||
|
fun updateLastTrigger(userId: Long, realId: Long, time: Long = -1): BanState {
|
||||||
|
val old = map[userId]
|
||||||
|
val newTime = if (time != -1L) time else old?.time ?: -1
|
||||||
|
val newMap = map.toMutableMap().apply {
|
||||||
|
put(userId, UserBanDetail(realId, newTime))
|
||||||
|
}
|
||||||
|
return copy(map = newMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveState(state: BanState) {
|
||||||
|
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(): BanState {
|
||||||
|
return try {
|
||||||
|
val fileToRead = when {
|
||||||
|
stateFile.exists() -> stateFile
|
||||||
|
stateBackupFile.exists() -> stateBackupFile
|
||||||
|
else -> null
|
||||||
|
} ?: return BanState()
|
||||||
|
|
||||||
|
Json.decodeFromString<BanState>(fileToRead.readText())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.warn("[$name] 读取状态失败", e)
|
||||||
|
BanState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package top.r3944realms.ltdmanager.module
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import org.intellij.lang.annotations.MagicConstant
|
||||||
import top.r3944realms.ltdmanager.GlobalManager
|
import top.r3944realms.ltdmanager.GlobalManager
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
@ -9,12 +10,12 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||||
* 模块抽象基类
|
* 模块抽象基类
|
||||||
* 所有功能模块都继承该类
|
* 所有功能模块都继承该类
|
||||||
*/
|
*/
|
||||||
abstract class BaseModule {
|
abstract class BaseModule(baseName : String = "BaseModule", idName : String = "") {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模块名称
|
* 模块名称
|
||||||
*/
|
*/
|
||||||
abstract val name: String
|
val name: String = "$baseName-#$idName";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止信号
|
* 停止信号
|
||||||
|
|
@ -73,21 +74,57 @@ abstract class BaseModule {
|
||||||
} catch (_: CancellationException) {}
|
} catch (_: CancellationException) {}
|
||||||
LoggerUtil.syncInfo("[$name] 模块已安全停止")
|
LoggerUtil.syncInfo("[$name] 模块已安全停止")
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 模块说明 / 帮助信息
|
||||||
|
* 默认返回空字符串,子类可重写提供具体帮助文本
|
||||||
|
*/
|
||||||
|
open fun help(): String = ""
|
||||||
|
/** 模块基础信息,用于 HelpModule 显示 */
|
||||||
|
open fun info(): String = "模块 $name 未提供详细信息"
|
||||||
/**
|
/**
|
||||||
* 提供访问全局 NapCatClient 的快捷方式
|
* 提供访问全局 NapCatClient 的快捷方式
|
||||||
*/
|
*/
|
||||||
protected val napCatClient get() = GlobalManager.napCatClient
|
protected val napCatClient get() = GlobalManager.napCatClient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提供访问全局 blessingSkinClient 的快捷方式
|
* 提供访问全局 blessingSkinClient 的快捷方式
|
||||||
*/
|
*/
|
||||||
protected val blessingSkinClient get() = GlobalManager.blessingSkinClient
|
protected val blessingSkinClient get() = GlobalManager.blessingSkinClient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提供访问全局 mcSrvStatusClient 的快捷方式
|
* 提供访问全局 mcSrvStatusClient 的快捷方式
|
||||||
*/
|
*/
|
||||||
protected val mcSrvStatusClient get() = GlobalManager.mcSrvStatusClient
|
protected val mcSrvStatusClient get() = GlobalManager.mcSrvStatusClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提供访问全局 加载模块 的快捷方式
|
||||||
|
*/
|
||||||
|
protected val moduleMap get() = GlobalManager.moduleManager.getModules()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取数据库连接
|
* 获取数据库连接
|
||||||
* 使用 try-with-resources 时会自动关闭
|
* 使用 try-with-resources 时会自动关闭
|
||||||
*/
|
*/
|
||||||
protected fun getConnection() = GlobalManager.getConnection()
|
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初始化")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
489
src/main/kotlin/top/r3944realms/ltdmanager/module/DGLabModule.kt
Normal file
489
src/main/kotlin/top/r3944realms/ltdmanager/module/DGLabModule.kt
Normal file
|
|
@ -0,0 +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,
|
||||||
|
private val groupMessagePollingModule : GroupMessagePollingModule,
|
||||||
|
private val selfId: Long,
|
||||||
|
val adminIds: List<Long> = listOf(),
|
||||||
|
val maxClientNumber: Int = 10,
|
||||||
|
val commandHead: List<String> = listOf("dglab"),
|
||||||
|
) : BaseModule(Modules.DG_LAB, moduleName), PersistentState<DGLabModule.DgLabState> {
|
||||||
|
|
||||||
|
var dgLabManager: DgLab? = null
|
||||||
|
private var scope: CoroutineScope? = null
|
||||||
|
private var dglabCommandDispatcher: CommandDispatcher<Player> = CommandDispatcher<Player>().apply {
|
||||||
|
for (command in commandHead) register(
|
||||||
|
literal<Player>(command)
|
||||||
|
.then(literal<Player?>("server").requires { adminIds.contains(it.id) }
|
||||||
|
.then(literal<Player?>("start").executes { startDgLab() })
|
||||||
|
.then(literal<Player?>("stop").executes { stopDgLab() })
|
||||||
|
.then(literal<Player?>("stopAllClient").executes { stopAllDgLabClient() })
|
||||||
|
)
|
||||||
|
.then(literal<Player?>("client")
|
||||||
|
.then(literal<Player?>("start").executes { startClient(it.source.id) })
|
||||||
|
.then(literal<Player?>("stop").executes { stopClient(it.source.id) })
|
||||||
|
)
|
||||||
|
.then(literal<Player?>("strength")
|
||||||
|
.then(argument<Player?, String>("channel", StringArgumentType.string())
|
||||||
|
.then(literal<Player?>("add")
|
||||||
|
.then(argument<Player?, Int>("value", IntegerArgumentType.integer(-200, 200))
|
||||||
|
.executes { strengthAdd(it.source.id, StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(literal<Player?>("set")
|
||||||
|
.then(argument<Player?, Int>("value", IntegerArgumentType.integer(0, 200))
|
||||||
|
.executes { strengthSet(it.source.id, StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(argument<Player?, Long>("player", LongArgumentType.longArg())
|
||||||
|
.then(argument<Player?, String>("channel", StringArgumentType.string())
|
||||||
|
.then(literal<Player?>("add")
|
||||||
|
.then(argument<Player?, Int>("value", IntegerArgumentType.integer(-200, 200))
|
||||||
|
.executes { strengthAdd(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(literal<Player?>("set")
|
||||||
|
.then(argument<Player?, Int>("value", IntegerArgumentType.integer(0, 200))
|
||||||
|
.executes { strengthSet(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(literal<Player?>("pulse")
|
||||||
|
.then(argument<Player?, String>("channel", StringArgumentType.string())
|
||||||
|
.then(literal<Player?>("clear").executes { pulseClear(it.source.id, StringArgumentType.getString(it, "channel")) })
|
||||||
|
.then(literal<Player?>("set")
|
||||||
|
.then(argument<Player?, String>("pulseName", StringArgumentType.string())
|
||||||
|
.then(argument<Player?, Int>("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?, Long>("player", LongArgumentType.longArg())
|
||||||
|
.then(argument<Player?, String>("channel", StringArgumentType.string())
|
||||||
|
.then(literal<Player?>("clear").executes { pulseClear(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel")) })
|
||||||
|
.then(literal<Player?>("set")
|
||||||
|
.then(argument<Player?, String>("pulseName", StringArgumentType.string())
|
||||||
|
.then(argument<Player?, Int>("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<Player?>("info").executes {}
|
||||||
|
// .then(argument<Player?, String>("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<MsgHistorySpecificMsg>) {
|
||||||
|
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<MsgHistorySpecificMsg>
|
||||||
|
): List<MsgHistorySpecificMsg> = triggerFilter.filter(messages)
|
||||||
|
private suspend fun init() {
|
||||||
|
val getGroupMemberListEvent = napCatClient.send<GetGroupMemberListEvent>(
|
||||||
|
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<Long, DgLabDetail> = 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<DgLabState>(fileToRead.readText())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.warn("[$name] 读取状态失败", e)
|
||||||
|
DgLabState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,26 +5,25 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
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.event.message.GetGroupMsgHistoryEvent
|
||||||
import top.r3944realms.ltdmanager.napcat.request.message.GetGroupMsgHistoryRequest
|
import top.r3944realms.ltdmanager.napcat.request.message.GetGroupMsgHistoryRequest
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
|
||||||
class GroupMessagePollingModule(
|
class GroupMessagePollingModule(
|
||||||
|
moduleName: String,
|
||||||
val targetGroupId: Long,
|
val targetGroupId: Long,
|
||||||
private val pollIntervalMillis: Long = 5_000L,
|
private val pollIntervalMillis: Long = 5_000L,
|
||||||
private val msgHistoryCheck: Int = 15
|
private val msgHistoryCheck: Int = 15,
|
||||||
) : BaseModule() {
|
) : BaseModule(Modules.GROUP_MESSAGE_POLLING, moduleName) {
|
||||||
|
|
||||||
override val name: String = "MessagePollingModule"
|
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
|
|
||||||
// 用 Flow 存消息,其他模块可以订阅
|
// 用 Flow 存消息,其他模块可以订阅
|
||||||
private val _messagesFlow = MutableSharedFlow<List<GetFriendMsgHistoryEvent.SpecificMsg>>(
|
private val _messagesFlow = MutableSharedFlow<List<MsgHistorySpecificMsg>>(
|
||||||
replay = 1, // 保留最近一份消息
|
replay = 1, // 保留最近一份消息
|
||||||
extraBufferCapacity = 1
|
extraBufferCapacity = 1
|
||||||
)
|
)
|
||||||
val messagesFlow: SharedFlow<List<GetFriendMsgHistoryEvent.SpecificMsg>> = _messagesFlow.asSharedFlow()
|
val messagesFlow: SharedFlow<List<MsgHistorySpecificMsg>> = _messagesFlow.asSharedFlow()
|
||||||
|
|
||||||
override fun onLoad() {
|
override fun onLoad() {
|
||||||
LoggerUtil.logger.info("[$name] 启动消息轮询 (群: $targetGroupId)")
|
LoggerUtil.logger.info("[$name] 启动消息轮询 (群: $targetGroupId)")
|
||||||
|
|
@ -32,12 +31,12 @@ class GroupMessagePollingModule(
|
||||||
scope!!.launch {
|
scope!!.launch {
|
||||||
while (isActive && loaded) {
|
while (isActive && loaded) {
|
||||||
try {
|
try {
|
||||||
val event = napCatClient.send(
|
val event = getNapCatClientOrNull()?.send<GetGroupMsgHistoryEvent>(
|
||||||
GetGroupMsgHistoryRequest(
|
GetGroupMsgHistoryRequest(
|
||||||
count = msgHistoryCheck,
|
count = msgHistoryCheck,
|
||||||
groupId = ID.long(targetGroupId)
|
groupId = ID.long(targetGroupId)
|
||||||
)
|
)
|
||||||
) as? GetGroupMsgHistoryEvent
|
)
|
||||||
|
|
||||||
val messages = event?.data?.messages ?: emptyList()
|
val messages = event?.data?.messages ?: emptyList()
|
||||||
LoggerUtil.logger.debug("[$name] 拉取到 ${messages.size} 条消息")
|
LoggerUtil.logger.debug("[$name] 拉取到 ${messages.size} 条消息")
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,11 @@ import top.r3944realms.ltdmanager.napcat.request.group.SetGroupAddRequestRequest
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
|
||||||
class GroupRequestHandlerModule(
|
class GroupRequestHandlerModule(
|
||||||
|
moduleName: String,
|
||||||
private val client: NapCatClient,
|
private val client: NapCatClient,
|
||||||
private val targetGroupId: Long,
|
private val targetGroupId: Long,
|
||||||
private val pollIntervalMillis: Long = 30_000L,
|
private val pollIntervalMillis: Long = 30_000L,
|
||||||
) : BaseModule() {
|
) : BaseModule(Modules.GROUP_REQUEST_HANDLER, moduleName) {
|
||||||
|
|
||||||
override val name: String = "GroupRequestHandlerModule"
|
|
||||||
|
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
|
|
||||||
|
|
@ -138,7 +137,7 @@ class GroupRequestHandlerModule(
|
||||||
try {
|
try {
|
||||||
getConnection().use { conn ->
|
getConnection().use { conn ->
|
||||||
val stmt = conn.prepareStatement(
|
val stmt = conn.prepareStatement(
|
||||||
"SELECT status FROM minecraft_manager_ltd.players WHERE qq=?"
|
"SELECT status FROM minecraft_manager_ltd_9.players WHERE qq=?"
|
||||||
)
|
)
|
||||||
stmt.setLong(1, actor)
|
stmt.setLong(1, actor)
|
||||||
val rs = stmt.executeQuery()
|
val rs = stmt.executeQuery()
|
||||||
|
|
@ -176,4 +175,7 @@ class GroupRequestHandlerModule(
|
||||||
return data.invitedRequest + data.joinRequests
|
return data.invitedRequest + data.joinRequests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun info(): String = "模块: $name\n功能: 自动处理群组加群请求\n版本: 1.0"
|
||||||
|
|
||||||
|
override fun help(): String = "本模块会轮询群组加群请求并根据数据库白名单自动同意或拒绝"
|
||||||
}
|
}
|
||||||
245
src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt
Normal file
245
src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.r3944realms.ltdmanager.module.common.CommandParser
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownStateProvider
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.type.CooldownFilter
|
||||||
|
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.request.message.SendForwardMsgRequest
|
||||||
|
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||||
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HelpModule 提供全局模块帮助信息
|
||||||
|
*/
|
||||||
|
class HelpModule(
|
||||||
|
moduleName: String,
|
||||||
|
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||||
|
private val selfId: Long,
|
||||||
|
private val selfNickName: String,
|
||||||
|
private val keywords: List<String> = listOf("help", "帮助"),
|
||||||
|
private val cooldownMillis: Long = 30_000L
|
||||||
|
) : BaseModule(Modules.HELP, moduleName), PersistentState<HelpModule.HelpState> {
|
||||||
|
|
||||||
|
// 命令解析器
|
||||||
|
private val commandParser = CommandParser(keywords)
|
||||||
|
private val MsgHistorySpecificMsg.textContent: String
|
||||||
|
get() = message.joinToString("") { it.data.text ?: "" }
|
||||||
|
|
||||||
|
// 持久化文件
|
||||||
|
private val stateFile: File = getStateFileInternal("help_module_state.json", name)
|
||||||
|
private val stateBackupFile: File = getStateFileInternal("help_module_state.json.bak", name)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HelpState(var lastTriggeredRealId: Long = -1L, var lastTriggerTime: Long = 0L)
|
||||||
|
|
||||||
|
private var lastTriggerState: HelpState = loadState()
|
||||||
|
|
||||||
|
// 冷却管理器
|
||||||
|
private val cooldownManager by lazy {
|
||||||
|
CooldownManager(
|
||||||
|
cooldownMillis = cooldownMillis,
|
||||||
|
scope = CooldownScope.Global,
|
||||||
|
stateProvider = object : CooldownStateProvider<HelpState> {
|
||||||
|
override fun load() = loadState()
|
||||||
|
override fun save(state: HelpState) = saveState(state)
|
||||||
|
},
|
||||||
|
getLastTrigger = { state, _ -> state.lastTriggerTime to state.lastTriggeredRealId },
|
||||||
|
updateTrigger = { state, _, realId, time -> state.copy(lastTriggeredRealId = realId, lastTriggerTime = time) },
|
||||||
|
updateCooldownRealId = { state, _, realId -> state.copy(lastTriggeredRealId = realId) },
|
||||||
|
groupId = groupMessagePollingModule.targetGroupId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发过滤器
|
||||||
|
private val triggerFilter by lazy {
|
||||||
|
TriggerMessageFilter(
|
||||||
|
listOf(
|
||||||
|
IgnoreSelfFilter(selfId),
|
||||||
|
NewMessageFilter { _ -> lastTriggerState.lastTriggerTime to lastTriggerState.lastTriggeredRealId },
|
||||||
|
KeywordFilter(keywords.toSet()),
|
||||||
|
CooldownFilter(cooldownManager) { msg, remain -> sendCooldownMessage(napCatClient, msg.realId, remain) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var scope: CoroutineScope? = null
|
||||||
|
|
||||||
|
override fun getStateFileInternal(): File = stateFile
|
||||||
|
|
||||||
|
override fun getState(): HelpState = lastTriggerState
|
||||||
|
|
||||||
|
override fun onLoad() {
|
||||||
|
LoggerUtil.logger.info("[$name] 模块已加载,监听 help 指令")
|
||||||
|
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
scope!!.launch {
|
||||||
|
groupMessagePollingModule.messagesFlow.collect { messages ->
|
||||||
|
if (loaded) handleMessages(messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onUnload() {
|
||||||
|
LoggerUtil.logger.info("[$name] 模块卸载,取消协程...")
|
||||||
|
scope?.cancel()
|
||||||
|
saveState(lastTriggerState)
|
||||||
|
LoggerUtil.logger.info("[$name] 模块已卸载完成")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
|
||||||
|
val filtered = triggerFilter.filter(messages)
|
||||||
|
val triggerMsg = filtered.maxByOrNull { it.time } ?: return
|
||||||
|
|
||||||
|
val cmdPair = commandParser.parseCommand(triggerMsg.textContent)
|
||||||
|
if (cmdPair != null) {
|
||||||
|
val (_, arg) = cmdPair
|
||||||
|
if (arg.isNotEmpty()) {
|
||||||
|
val module = moduleMap[arg]
|
||||||
|
if (module != null) sendModuleHelp(triggerMsg, arg, module)
|
||||||
|
else sendText(triggerMsg, "未找到模块: $arg")
|
||||||
|
} else {
|
||||||
|
sendAllModulesHelp(triggerMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun sendAllModulesHelp(msg: MsgHistorySpecificMsg) {
|
||||||
|
val messages = moduleMap.map { (name, module) ->
|
||||||
|
val textBuilder = StringBuilder()
|
||||||
|
textBuilder.appendLine("===== $name =====")
|
||||||
|
textBuilder.appendLine(module.info())
|
||||||
|
val helpText = module.help()
|
||||||
|
if (helpText.isNotEmpty()) textBuilder.appendLine(helpText)
|
||||||
|
textBuilder.appendLine().appendLine()
|
||||||
|
SendForwardMsgRequest.Message(
|
||||||
|
data = SendForwardMsgRequest.PurpleData(textBuilder.toString()),
|
||||||
|
type = MessageType.Text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val topMessage = SendForwardMsgRequest.TopForwardMsg(
|
||||||
|
data = SendForwardMsgRequest.MessageData(
|
||||||
|
content = messages,
|
||||||
|
nickname = selfNickName,
|
||||||
|
userId = ID.long(selfId)
|
||||||
|
),
|
||||||
|
type = MessageType.Node
|
||||||
|
)
|
||||||
|
|
||||||
|
val request = SendForwardMsgRequest(
|
||||||
|
groupId = ID.long(groupMessagePollingModule.targetGroupId),
|
||||||
|
messages = listOf(topMessage),
|
||||||
|
news = listOf(SendForwardMsgRequest.ForwardModelNews("点击查看所有模块信息")),
|
||||||
|
prompt = "全局模块信息",
|
||||||
|
source = "📚 HelpModule",
|
||||||
|
summary = "信息,共 ${messages.size} 个模块"
|
||||||
|
)
|
||||||
|
|
||||||
|
napCatClient.sendUnit(request)
|
||||||
|
updateTriggerState(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun sendModuleHelp(msg: MsgHistorySpecificMsg, moduleName: String, module: BaseModule) {
|
||||||
|
val textBuilder = StringBuilder()
|
||||||
|
textBuilder.appendLine("===== $moduleName =====")
|
||||||
|
textBuilder.appendLine(module.info())
|
||||||
|
val helpText = module.help()
|
||||||
|
if (helpText.isNotEmpty()) textBuilder.appendLine(helpText)
|
||||||
|
|
||||||
|
val message = SendForwardMsgRequest.Message(
|
||||||
|
data = SendForwardMsgRequest.PurpleData(textBuilder.toString()),
|
||||||
|
type = MessageType.Text
|
||||||
|
)
|
||||||
|
|
||||||
|
val topMessage = SendForwardMsgRequest.TopForwardMsg(
|
||||||
|
data = SendForwardMsgRequest.MessageData(
|
||||||
|
content = listOf(message),
|
||||||
|
nickname = selfNickName,
|
||||||
|
userId = ID.long(selfId)
|
||||||
|
),
|
||||||
|
type = MessageType.Node
|
||||||
|
)
|
||||||
|
|
||||||
|
val request = SendForwardMsgRequest(
|
||||||
|
groupId = ID.long(groupMessagePollingModule.targetGroupId),
|
||||||
|
messages = listOf(topMessage),
|
||||||
|
news = listOf(SendForwardMsgRequest.ForwardModelNews("点击查看模块 $moduleName 帮助")),
|
||||||
|
prompt = "模块 $moduleName 帮助",
|
||||||
|
source = "📚 HelpModule",
|
||||||
|
summary = "模块 $moduleName 帮助信息"
|
||||||
|
)
|
||||||
|
|
||||||
|
napCatClient.sendUnit(request)
|
||||||
|
updateTriggerState(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun sendText(msg: MsgHistorySpecificMsg, text: String) {
|
||||||
|
val request = SendGroupMsgRequest(
|
||||||
|
MessageElement.reply(ID.long(msg.realId), text),
|
||||||
|
ID.long(groupMessagePollingModule.targetGroupId)
|
||||||
|
)
|
||||||
|
napCatClient.sendUnit(request)
|
||||||
|
updateTriggerState(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTriggerState(msg: MsgHistorySpecificMsg) {
|
||||||
|
lastTriggerState.lastTriggeredRealId = msg.realId
|
||||||
|
lastTriggerState.lastTriggerTime = msg.time
|
||||||
|
saveState(lastTriggerState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, remaining: Long) {
|
||||||
|
val msg = "⏳ Help 查询过于频繁,请稍后再试(剩余 $remaining 秒)"
|
||||||
|
LoggerUtil.logger.info("[$name] 发送冷却提示: $msg")
|
||||||
|
client.sendUnit(
|
||||||
|
SendGroupMsgRequest(
|
||||||
|
MessageElement.reply(ID.long(realId), msg),
|
||||||
|
ID.long(groupMessagePollingModule.targetGroupId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- 持久化 ----------------
|
||||||
|
override fun saveState(state: HelpState) {
|
||||||
|
try {
|
||||||
|
if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true)
|
||||||
|
stateFile.writeText(Json.encodeToString(state))
|
||||||
|
LoggerUtil.logger.info("[$name] 已保存状态: lastTriggeredRealId=${state.lastTriggeredRealId}, lastTriggerTime=${state.lastTriggerTime}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.error("[$name] 保存状态失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadState(): HelpState {
|
||||||
|
return try {
|
||||||
|
val fileToRead = when {
|
||||||
|
stateFile.exists() -> stateFile
|
||||||
|
stateBackupFile.exists() -> stateBackupFile
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (fileToRead == null) return HelpState()
|
||||||
|
Json.decodeFromString<HelpState>(fileToRead.readText())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.warn("[$name] 读取状态失败,使用默认值", e)
|
||||||
|
HelpState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun help(): String = "发送 'help' 获取所有模块帮助信息"
|
||||||
|
|
||||||
|
override fun info(): String = "模块: $name\n功能: 提供全局模块帮助信息\n版本: 1.0"
|
||||||
|
}
|
||||||
|
|
@ -4,16 +4,24 @@ import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.r3944realms.ltdmanager.blessingskin.data.InvitationCode
|
||||||
import top.r3944realms.ltdmanager.blessingskin.request.invitecode.GenerateInvitationCodeRequest
|
import top.r3944realms.ltdmanager.blessingskin.request.invitecode.GenerateInvitationCodeRequest
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||||
|
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||||
import top.r3944realms.ltdmanager.core.mail.mail
|
import top.r3944realms.ltdmanager.core.mail.mail
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownStateProvider
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.type.CooldownFilter
|
||||||
|
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.module.exception.InvitationCodeException
|
import top.r3944realms.ltdmanager.module.exception.InvitationCodeException
|
||||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
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.message.GetFriendMsgHistoryEvent
|
|
||||||
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||||
import top.r3944realms.ltdmanager.utils.HtmlTemplateUtil
|
import top.r3944realms.ltdmanager.utils.HtmlTemplateUtil
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
|
@ -63,24 +71,67 @@ api格式 https://skins.r3944realms.top/api/invitation-codes/generate?token=XXXX
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class InvitationCodesModule(
|
class InvitationCodesModule(
|
||||||
|
moduleName: String,
|
||||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||||
private val mailModule: MailModule,
|
private val mailModule: MailModule,
|
||||||
private val apiToken: String,
|
private val apiToken: String,
|
||||||
private val selfId: Long,
|
selfId: Long,
|
||||||
private val cooldownMillis: Long = 120_000,
|
private val cooldownMillis: Long = 120_000,
|
||||||
private val keywords: Set<String> = setOf("申请邀请码")
|
private val keywords: Set<String> = setOf("申请邀请码")
|
||||||
) : BaseModule(), PersistentState<InvitationCodesModule.LastTriggerMapState> {
|
) : BaseModule(Modules.INVITATION_CODE, moduleName), PersistentState<InvitationCodesModule.LastTriggerMapState> {
|
||||||
|
|
||||||
override val name: String = "InvitationCodesModule"
|
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
|
private val stateFile: File = getStateFileInternal("invitation_codes_quarry_state.json", name)
|
||||||
|
private val stateBackupFile: File = getStateFileInternal("invitation_codes_quarry_state.json.bak", name)
|
||||||
|
private val cooldownManager by lazy{ CooldownManager(
|
||||||
|
cooldownMillis = cooldownMillis,
|
||||||
|
scope = CooldownScope.PerUser,
|
||||||
|
stateProvider = object : CooldownStateProvider<LastTriggerMapState> {
|
||||||
|
override fun load() = loadState()
|
||||||
|
override fun save(state: LastTriggerMapState) = saveState(state)
|
||||||
|
},
|
||||||
|
getLastTrigger = { state, qq ->
|
||||||
|
val detail = state.map[qq]
|
||||||
|
(detail?.time ?: -1L) to (detail?.lastCooldownRealId ?: -1L)
|
||||||
|
},
|
||||||
|
updateTrigger = { state, qq, realId, time ->
|
||||||
|
val id = requireNotNull(qq)
|
||||||
|
state.updateLastTrigger(id, realId, time)
|
||||||
|
},
|
||||||
|
updateCooldownRealId = { state, qq, realId ->
|
||||||
|
val id = requireNotNull(qq)
|
||||||
|
state.updateLastCooldownRealId(id, realId)
|
||||||
|
},
|
||||||
|
groupId = groupMessagePollingModule.targetGroupId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 在 InvitationCodesModule 类里添加:
|
||||||
|
private val triggerFilter = TriggerMessageFilter(
|
||||||
|
listOf(
|
||||||
|
IgnoreSelfFilter(selfId),
|
||||||
|
NewMessageFilter { qq ->
|
||||||
|
lastTriggerMapState.getLastTriggerTime(qq) to lastTriggerMapState.getLastTriggerRealId(qq)
|
||||||
|
},
|
||||||
|
KeywordFilter(keywords),
|
||||||
|
CooldownFilter(
|
||||||
|
cooldownManager = cooldownManager,
|
||||||
|
sendCooldown = { msg, remain ->
|
||||||
|
sendCooldownMessage(
|
||||||
|
napCatClient,
|
||||||
|
msg.userId,
|
||||||
|
msg.realId,
|
||||||
|
"⏳ 申请邀请码过于频繁(剩余 $remain 秒后自动申请)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
// 持久化文件(带锁 + 备份)
|
|
||||||
private val stateFile = getStateFile("mc_server_status_state.json")
|
|
||||||
private val stateBackupFile = getStateFile("invitation_codes_quarry_state.json.bak")
|
|
||||||
private val fileLock = ReentrantLock()
|
private val fileLock = ReentrantLock()
|
||||||
|
|
||||||
private var lastTriggerMapState = loadState()
|
private var lastTriggerMapState = loadState()
|
||||||
override fun getStateFile(): File = stateFile
|
override fun getStateFileInternal(): File = stateFile
|
||||||
override fun getState(): LastTriggerMapState = lastTriggerMapState
|
override fun getState(): LastTriggerMapState = lastTriggerMapState
|
||||||
override fun onLoad() {
|
override fun onLoad() {
|
||||||
LoggerUtil.logger.info("[$name] 模块已装载,目标群组: ${groupMessagePollingModule.targetGroupId}")
|
LoggerUtil.logger.info("[$name] 模块已装载,目标群组: ${groupMessagePollingModule.targetGroupId}")
|
||||||
|
|
@ -115,14 +166,14 @@ class InvitationCodesModule(
|
||||||
// =========================
|
// =========================
|
||||||
// 消息处理主流程
|
// 消息处理主流程
|
||||||
// =========================
|
// =========================
|
||||||
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
|
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
|
||||||
if (messages.isEmpty()) return
|
if (messages.isEmpty()) return
|
||||||
val triggerMsgs = filterTriggerMessages(messages)
|
val triggerMsgs = filterTriggerMessages(messages)
|
||||||
if (triggerMsgs.isEmpty()) return
|
if (triggerMsgs.isEmpty()) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val hadValidCodeButNotUsed = mutableListOf<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>()
|
val hadValidCodeButNotUsed = mutableListOf<Pair<Long, MsgHistorySpecificMsg>>()
|
||||||
val needNewCode = mutableListOf<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>()
|
val needNewCode = mutableListOf<Pair<Long, MsgHistorySpecificMsg>>()
|
||||||
|
|
||||||
getIdAndSelectSituation(triggerMsgs, hadValidCodeButNotUsed, needNewCode)
|
getIdAndSelectSituation(triggerMsgs, hadValidCodeButNotUsed, needNewCode)
|
||||||
createAndSearchInvitationCodeIdsThenUpdateDate(needNewCode)
|
createAndSearchInvitationCodeIdsThenUpdateDate(needNewCode)
|
||||||
|
|
@ -135,34 +186,22 @@ class InvitationCodesModule(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 过滤出符合条件的触发消息 */
|
/** 过滤出符合条件的触发消息 */
|
||||||
private fun filterTriggerMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>)
|
private suspend fun filterTriggerMessages(
|
||||||
: List<GetFriendMsgHistoryEvent.SpecificMsg> {
|
messages: List<MsgHistorySpecificMsg>
|
||||||
|
): List<MsgHistorySpecificMsg> {
|
||||||
|
|
||||||
val filtered = messages.asSequence()
|
// 先应用通用过滤器
|
||||||
.filter { msg ->
|
val filtered = triggerFilter.filter(messages)
|
||||||
msg.userId != selfId &&
|
|
||||||
(msg.time > lastTriggerMapState.getLastTriggerTime(msg.userId) ||
|
// 再做 groupBy -> 只保留每个用户最新一条
|
||||||
(msg.time == lastTriggerMapState.getLastTriggerTime(msg.userId)
|
return filtered
|
||||||
&& msg.realId > lastTriggerMapState.getLastTriggerRealId(msg.userId))) &&
|
|
||||||
msg.message.any { seg ->
|
|
||||||
seg.type == MessageType.Text &&
|
|
||||||
seg.data.text?.let { text -> keywords.any { keyword -> text == keyword } } == true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.groupBy { it.userId }
|
.groupBy { it.userId }
|
||||||
.mapNotNull { (_, msgs) -> msgs.maxByOrNull { it.time } }
|
.mapNotNull { (_, msgs) -> msgs.maxByOrNull { it.time } }
|
||||||
.filter { runBlocking { filterCoolDownMessage(it) } }
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
if (filtered.isNotEmpty()) {
|
|
||||||
LoggerUtil.logger.info("[$name] 待处理消息队列: $filtered")
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getIdAndSelectSituation(msgs: List<GetFriendMsgHistoryEvent.SpecificMsg>,
|
private suspend fun getIdAndSelectSituation(msgs: List<MsgHistorySpecificMsg>,
|
||||||
hadVaildCodeButNotUseList : MutableList<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>,
|
hadVaildCodeButNotUseList : MutableList<Pair<Long, MsgHistorySpecificMsg>>,
|
||||||
needNewCodeList: MutableList<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>) {
|
needNewCodeList: MutableList<Pair<Long, MsgHistorySpecificMsg>>) {
|
||||||
if (msgs.isEmpty()) return
|
if (msgs.isEmpty()) return
|
||||||
|
|
||||||
val qqIds = msgs.map { it.userId }
|
val qqIds = msgs.map { it.userId }
|
||||||
|
|
@ -235,7 +274,7 @@ class InvitationCodesModule(
|
||||||
sendFailedMessage(napCatClient, text = "批量查询用户资格信息失败,请联系管理员: ${e.message}")
|
sendFailedMessage(napCatClient, text = "批量查询用户资格信息失败,请联系管理员: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private suspend fun hadVaildCodeButNotUseListHandler(list: List<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>) {
|
private suspend fun hadVaildCodeButNotUseListHandler(list: List<Pair<Long, MsgHistorySpecificMsg>>) {
|
||||||
if (list.isEmpty()) return
|
if (list.isEmpty()) return
|
||||||
|
|
||||||
val whiteListIds = list.map { it.first }
|
val whiteListIds = list.map { it.first }
|
||||||
|
|
@ -358,33 +397,6 @@ class InvitationCodesModule(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
|
||||||
// 冷却逻辑
|
|
||||||
// =========================
|
|
||||||
private suspend fun filterCoolDownMessage(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
|
|
||||||
val triggerDetail = lastTriggerMapState.map[msg.userId]
|
|
||||||
val lastTriggerTime = triggerDetail?.time ?: -1L
|
|
||||||
val lastCooldownRealId = triggerDetail?.lastCooldownRealId ?: -1L
|
|
||||||
val nowSec = System.currentTimeMillis() / 1000 // 转成秒
|
|
||||||
|
|
||||||
if (lastTriggerTime == -1L || nowSec - lastTriggerTime >= cooldownMillis / 1000) {
|
|
||||||
// 正常触发
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 冷却中,如果本消息未发送过冷却提示
|
|
||||||
if (msg.realId != lastCooldownRealId) {
|
|
||||||
val remaining = ((cooldownMillis / 1000) - (nowSec - lastTriggerTime)).coerceAtLeast(1)
|
|
||||||
val msgText = "⏳ 申请邀请码过于频繁(剩余 $remaining 秒后将为你自动申请)"
|
|
||||||
sendCooldownMessage(napCatClient, msg.userId, msg.realId, msgText)
|
|
||||||
|
|
||||||
// 记录这条消息已发送过冷却提示
|
|
||||||
lastTriggerMapState = lastTriggerMapState.updateLastCooldownRealId(msg.userId, msg.realId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendCooldownMessage(client: NapCatClient, qq: Long, realId: Long, msg: String) {
|
private suspend fun sendCooldownMessage(client: NapCatClient, qq: Long, realId: Long, msg: String) {
|
||||||
val request = SendGroupMsgRequest(
|
val request = SendGroupMsgRequest(
|
||||||
MessageElement.reply(ID.long(realId), msg),
|
MessageElement.reply(ID.long(realId), msg),
|
||||||
|
|
@ -394,7 +406,7 @@ class InvitationCodesModule(
|
||||||
lastTriggerMapState = lastTriggerMapState.updateLastTrigger(qq, realId, -1)
|
lastTriggerMapState = lastTriggerMapState.updateLastTrigger(qq, realId, -1)
|
||||||
}
|
}
|
||||||
private suspend fun createAndSearchInvitationCodeIdsThenUpdateDate(
|
private suspend fun createAndSearchInvitationCodeIdsThenUpdateDate(
|
||||||
needNewTokenIdAndMsgPairs: List<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>,
|
needNewTokenIdAndMsgPairs: List<Pair<Long, MsgHistorySpecificMsg>>,
|
||||||
) {
|
) {
|
||||||
if (needNewTokenIdAndMsgPairs.isEmpty()) return
|
if (needNewTokenIdAndMsgPairs.isEmpty()) return
|
||||||
|
|
||||||
|
|
@ -419,23 +431,33 @@ class InvitationCodesModule(
|
||||||
/**
|
/**
|
||||||
* 1. 创建邀请码
|
* 1. 创建邀请码
|
||||||
*/
|
*/
|
||||||
private suspend fun createInvitationCodes(amount: Int): List<InvitationCodeGenerationResponse.InvitationCode>? {
|
private suspend fun createInvitationCodes(amount: Int): List<InvitationCode>? {
|
||||||
return try {
|
return try {
|
||||||
val response = blessingSkinClient.submitRequest(
|
val response = blessingSkinClient.submitRequest(
|
||||||
GenerateInvitationCodeRequest(amount = amount, token = apiToken)
|
GenerateInvitationCodeRequest(amount = amount, token = apiToken)
|
||||||
)
|
)
|
||||||
|
response
|
||||||
|
.onFailure {
|
||||||
|
|
||||||
|
}
|
||||||
|
.onSuccess {
|
||||||
|
|
||||||
|
}
|
||||||
when (response) {
|
when (response) {
|
||||||
is ResponseResult.Success -> {
|
is ResponseResult.Success -> {
|
||||||
if (response.response.success) {
|
if (response.response is InvitationCodeGenerationResponse) {
|
||||||
response.response.data
|
if (response.response.success) {
|
||||||
|
response.response.data
|
||||||
|
} else
|
||||||
|
LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}")
|
||||||
|
null
|
||||||
} else {
|
} else {
|
||||||
LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}")
|
LoggerUtil.logger.warn("[$name] 返回非预期对象类型: ${response.response.javaClass}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ResponseResult.Failure -> {
|
is ResponseResult.Failure -> {
|
||||||
LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedResult}")
|
LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedMessage}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -449,8 +471,8 @@ class InvitationCodesModule(
|
||||||
* 2. 验证数量匹配
|
* 2. 验证数量匹配
|
||||||
*/
|
*/
|
||||||
private fun validateCodeCountMatch(
|
private fun validateCodeCountMatch(
|
||||||
invitationCodes: List<InvitationCodeGenerationResponse.InvitationCode>?,
|
invitationCodes: List<InvitationCode>?,
|
||||||
needNewTokenIdAndMsgPairs: List<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>
|
needNewTokenIdAndMsgPairs: List<Pair<Long, MsgHistorySpecificMsg>>
|
||||||
) {
|
) {
|
||||||
if (invitationCodes == null) {
|
if (invitationCodes == null) {
|
||||||
throw InvitationCodeException.ApiFailureException("获取邀请码请求失败")
|
throw InvitationCodeException.ApiFailureException("获取邀请码请求失败")
|
||||||
|
|
@ -646,5 +668,34 @@ class InvitationCodesModule(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 在 InvitationCodesModule 类中补全:
|
||||||
|
override fun info(): String {
|
||||||
|
return """
|
||||||
|
模块: $name
|
||||||
|
功能: 自动处理群组内“申请邀请码”消息
|
||||||
|
描述:
|
||||||
|
1. 监听群消息,过滤关键词和冷却
|
||||||
|
2. 根据QQ号查询白名单状态
|
||||||
|
3. 自动创建或发送邀请码,并通过邮件发送
|
||||||
|
4. 已触发和未触发状态会持久化保存
|
||||||
|
关键词: $keywords
|
||||||
|
冷却时间: ${cooldownMillis / 1000} 秒
|
||||||
|
目标群组: ${groupMessagePollingModule.targetGroupId}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun help(): String {
|
||||||
|
return """
|
||||||
|
使用说明:
|
||||||
|
1. 在群里发送${keywords}触发本模块
|
||||||
|
2. 模块会自动判断你的白名单状态
|
||||||
|
- 若已使用过邀请码,会提醒你不要重复申请
|
||||||
|
- 若已有邀请码但未使用,会重新发送邮件提醒
|
||||||
|
- 若未生成邀请码,会调用API生成并发送邮件
|
||||||
|
3. 请求过于频繁时,会有冷却提示
|
||||||
|
4. 所有操作都有日志记录,可供管理员审计
|
||||||
|
5. 异常情况会发送失败提示消息
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import java.util.concurrent.LinkedBlockingQueue
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
class MailModule(
|
class MailModule(
|
||||||
|
moduleName: String,
|
||||||
private val protocol: String = "SMTP",
|
private val protocol: String = "SMTP",
|
||||||
private val host: String,
|
private val host: String,
|
||||||
private val port: Int,
|
private val port: Int,
|
||||||
|
|
@ -18,9 +19,7 @@ class MailModule(
|
||||||
private val enableAuth: Boolean = true,
|
private val enableAuth: Boolean = true,
|
||||||
private val enableTLS: Boolean = true,
|
private val enableTLS: Boolean = true,
|
||||||
private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s)
|
private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s)
|
||||||
) : BaseModule() {
|
) : BaseModule(Modules.MAIL, moduleName) {
|
||||||
|
|
||||||
override val name: String = "MailModule"
|
|
||||||
|
|
||||||
private lateinit var session: Session
|
private lateinit var session: Session
|
||||||
private val queue = LinkedBlockingQueue<Mail>() // 邮件队列
|
private val queue = LinkedBlockingQueue<Mail>() // 邮件队列
|
||||||
|
|
@ -142,4 +141,36 @@ class MailModule(
|
||||||
|
|
||||||
Transport.send(message)
|
Transport.send(message)
|
||||||
}
|
}
|
||||||
|
override fun info(): String {
|
||||||
|
return buildString {
|
||||||
|
appendLine("[$name] 邮件发送模块")
|
||||||
|
appendLine("功能: 异步发送邮件,支持收件人/抄送/密送,支持 HTML 或纯文本邮件。")
|
||||||
|
appendLine("SMTP 配置:")
|
||||||
|
appendLine(" - 协议: $protocol")
|
||||||
|
appendLine(" - 主机: $host")
|
||||||
|
appendLine(" - 端口: $port")
|
||||||
|
appendLine(" - 发件人邮箱: $senderEmailAddress")
|
||||||
|
appendLine(" - 身份认证: ${if (enableAuth) "启用" else "禁用"}")
|
||||||
|
appendLine(" - TLS/SSL: ${if (enableTLS) "启用" else "禁用"}")
|
||||||
|
appendLine("队列行为:")
|
||||||
|
appendLine(" - 邮件发送间隔: ${intervalMillis}ms")
|
||||||
|
appendLine(" - 队列长度: ${queue.size}")
|
||||||
|
appendLine(" - 当前发送线程状态: ${if (workerThread?.isAlive == true) "运行中" else "未运行"}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun help(): String {
|
||||||
|
return buildString {
|
||||||
|
appendLine("📖 [$name] 使用帮助:")
|
||||||
|
appendLine("1. 创建 Mail 对象,设置收件人、主题和正文")
|
||||||
|
appendLine(" 例如: Mail(to = listOf(\"example@mail.com\"), subject = \"测试\", body = \"Hello\")")
|
||||||
|
appendLine("2. 调用 enqueue(mail) 加入发送队列")
|
||||||
|
appendLine(" 邮件将异步发送,间隔 $intervalMillis ms")
|
||||||
|
appendLine("3. 模块卸载时会自动停止发送线程")
|
||||||
|
appendLine()
|
||||||
|
appendLine("注意:")
|
||||||
|
appendLine(" - 确保 SMTP 配置正确,否则发送失败")
|
||||||
|
appendLine(" - 发件人邮箱需要允许 SMTP/授权码登录")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,20 @@ import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import top.r3944realms.ltdmanager.mcserver.McServerStatus
|
import top.r3944realms.ltdmanager.mcserver.McServerStatus
|
||||||
|
import top.r3944realms.ltdmanager.module.common.CommandParser
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownStateProvider
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.type.CommandFilter
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.type.CooldownFilter
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.type.IgnoreSelfFilter
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter
|
||||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||||
import top.r3944realms.ltdmanager.napcat.data.MessageType
|
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.message.SendForwardMsgRequest
|
||||||
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
|
@ -17,30 +26,72 @@ import java.util.concurrent.locks.ReentrantLock
|
||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
class McServerStatusModule(
|
class McServerStatusModule(
|
||||||
|
moduleName: String,
|
||||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||||
private val selfId: Long,
|
private val selfId: Long,
|
||||||
private val selfNickName: String,
|
private val selfNickName: String,
|
||||||
private val cooldownSeconds: Long = 60,
|
private val cooldownMillis: Long = 60_000L,
|
||||||
private val commands: List<String> = listOf("/mcs", "/s"),
|
private val commands: List<String> = listOf("/mcs", "/s"),
|
||||||
private val presetServer: Map<Set<String>, String> = mapOf(
|
private val presetServer: Map<Set<String>, String> = mapOf(
|
||||||
setOf("hp", "hypixel") to "mc.hypixel.net",
|
setOf("hp", "hypixel") to "mc.hypixel.net",
|
||||||
setOf("pm", "mineplex") to "play.mineplex.com"
|
setOf("pm", "mineplex") to "play.mineplex.com"
|
||||||
)
|
)
|
||||||
) : BaseModule(), PersistentState<McServerStatusModule.CooldownState> {
|
) : BaseModule(Modules.MC_SERVER_STATUS, moduleName), PersistentState<McServerStatusModule.CooldownState> {
|
||||||
|
private val stateFile:File = getStateFileInternal("mc_server_status_state.json", name)
|
||||||
|
private val stateBackupFile:File = getStateFileInternal("mc_server_status_state.json.bak", name)
|
||||||
|
private val commandParser: CommandParser = CommandParser(commands)
|
||||||
|
|
||||||
|
private val cooldownManager by lazy {
|
||||||
|
CooldownManager(
|
||||||
|
cooldownMillis = cooldownMillis,
|
||||||
|
scope = CooldownScope.PerUser,
|
||||||
|
stateProvider = object : CooldownStateProvider<CooldownState> {
|
||||||
|
override fun load() = loadState()
|
||||||
|
override fun save(state: CooldownState) = saveState(state)
|
||||||
|
},
|
||||||
|
getLastTrigger = { state, qq ->
|
||||||
|
val detail = state.map[qq]
|
||||||
|
(detail?.time ?: -1L) to (detail?.lastCooldownRealId ?: -1L)
|
||||||
|
},
|
||||||
|
updateTrigger = { state, qq, realId, time ->
|
||||||
|
val id = requireNotNull(qq) { "userId required for per-user cooldown" }
|
||||||
|
state.updateLastTrigger(id, realId, time) }
|
||||||
|
,
|
||||||
|
updateCooldownRealId = { state, qq, realId ->
|
||||||
|
val id = requireNotNull(qq) { "userId required for per-user cooldown" }
|
||||||
|
state.updateLastCooldownRealId(id, realId)
|
||||||
|
},
|
||||||
|
groupId = groupMessagePollingModule.targetGroupId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val triggerFilter = TriggerMessageFilter(
|
||||||
|
listOf(
|
||||||
|
IgnoreSelfFilter(selfId),
|
||||||
|
NewMessageFilter { qq ->
|
||||||
|
cooldownState.getLastTriggerTime(qq) to cooldownState.getLastTriggerRealId(qq)
|
||||||
|
},
|
||||||
|
CommandFilter(commandParser),
|
||||||
|
CooldownFilter(
|
||||||
|
cooldownManager = cooldownManager,
|
||||||
|
sendCooldown = { msg, remaining ->
|
||||||
|
sendCooldownMessage(napCatClient, msg.realId, "⏳ 查询过于频繁, $remaining 秒后执行查询,切勿重复发送")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
private val presetServerByAlias: Map<String, String> by lazy {
|
private val presetServerByAlias: Map<String, String> by lazy {
|
||||||
presetServer.flatMap { (aliases, ip) ->
|
presetServer.flatMap { (aliases, ip) ->
|
||||||
aliases.map { it.lowercase() to ip }
|
aliases.map { it.lowercase() to ip }
|
||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
fun getServerIp(alias: String): String? = presetServerByAlias[alias.lowercase()]
|
fun getServerIp(alias: String): String? = presetServerByAlias[alias.lowercase()]
|
||||||
override val name: String = "McServerStatusModule"
|
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
private val stateFile = getStateFile("mc_server_status_state.json")
|
|
||||||
private val stateBackupFile = getStateFile("mc_server_status_state.json.bak")
|
|
||||||
private val fileLock = ReentrantLock()
|
private val fileLock = ReentrantLock()
|
||||||
private var cooldownState = loadState()
|
private var cooldownState = loadState()
|
||||||
|
|
||||||
override fun getStateFile(): File = stateFile
|
override fun getStateFileInternal(): File = stateFile
|
||||||
override fun getState(): CooldownState = cooldownState
|
override fun getState(): CooldownState = cooldownState
|
||||||
|
|
||||||
override fun onLoad() {
|
override fun onLoad() {
|
||||||
|
|
@ -61,7 +112,7 @@ class McServerStatusModule(
|
||||||
LoggerUtil.logger.info("[$name] 模块已卸载完成")
|
LoggerUtil.logger.info("[$name] 模块已卸载完成")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
|
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
|
||||||
if (messages.isEmpty()) return
|
if (messages.isEmpty()) return
|
||||||
val triggerMsgs = filterTriggerMessages(messages)
|
val triggerMsgs = filterTriggerMessages(messages)
|
||||||
if (triggerMsgs.isEmpty()) return
|
if (triggerMsgs.isEmpty()) return
|
||||||
|
|
@ -76,32 +127,11 @@ class McServerStatusModule(
|
||||||
saveState(cooldownState)
|
saveState(cooldownState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private suspend fun filterTriggerMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>)
|
|
||||||
: List<GetFriendMsgHistoryEvent.SpecificMsg> {
|
|
||||||
|
|
||||||
val filtered = messages.asSequence()
|
private suspend fun filterTriggerMessages(
|
||||||
.filter { msg ->
|
messages: List<MsgHistorySpecificMsg>
|
||||||
// 忽略自己消息
|
): List<MsgHistorySpecificMsg> = triggerFilter.filter(messages)
|
||||||
msg.userId != selfId &&
|
|
||||||
// 新消息判断
|
|
||||||
(msg.time > cooldownState.getLastTriggerTime(msg.userId) ||
|
|
||||||
(msg.time == cooldownState.getLastTriggerTime(msg.userId) &&
|
|
||||||
msg.realId > cooldownState.getLastTriggerRealId(msg.userId)))
|
|
||||||
}
|
|
||||||
.filter { msg ->
|
|
||||||
// 检查命令
|
|
||||||
msg.message.any { seg ->
|
|
||||||
seg.type == MessageType.Text &&
|
|
||||||
(
|
|
||||||
seg.data.text?.let { text -> commands.any { cmd -> text.startsWith(cmd) } } == true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter { runBlocking { handleCooldown(it) } } // 这里处理冷却
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
private suspend fun sendFailedMessage(
|
private suspend fun sendFailedMessage(
|
||||||
client: NapCatClient,
|
client: NapCatClient,
|
||||||
qq: Long? = null,
|
qq: Long? = null,
|
||||||
|
|
@ -129,31 +159,7 @@ class McServerStatusModule(
|
||||||
LoggerUtil.logger.info("[$name] 已发送 失败消息[无指定对象]")
|
LoggerUtil.logger.info("[$name] 已发送 失败消息[无指定对象]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** 冷却提示消息 */
|
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, text: String) {
|
||||||
|
|
||||||
private suspend fun handleCooldown(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
|
|
||||||
val trigger = cooldownState.map[msg.userId]
|
|
||||||
val lastTriggerTime = trigger?.time ?: -1L
|
|
||||||
val lastCooldownRealId = trigger?.lastCooldownRealId ?: -1L
|
|
||||||
val nowSec = System.currentTimeMillis() / 1000
|
|
||||||
|
|
||||||
// 未触发过或者已超过冷却
|
|
||||||
if (lastTriggerTime == -1L || nowSec - lastTriggerTime >= cooldownSeconds) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 冷却中且未发送过冷却提示
|
|
||||||
if (msg.realId != lastCooldownRealId) {
|
|
||||||
val remaining = ((cooldownSeconds - (nowSec - lastTriggerTime))).coerceAtLeast(1)
|
|
||||||
val msgText = "⏳ 查询过于频繁, $remaining 秒后执行查询,切勿重复发送"
|
|
||||||
sendCooldownMessage(napCatClient, msg.userId, msg.realId, msgText)
|
|
||||||
cooldownState = cooldownState.updateLastCooldownRealId(msg.userId, msg.realId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendCooldownMessage(client: NapCatClient, qq: Long, realId: Long, text: String) {
|
|
||||||
val request = SendGroupMsgRequest(
|
val request = SendGroupMsgRequest(
|
||||||
MessageElement.reply(ID.long(realId), text),
|
MessageElement.reply(ID.long(realId), text),
|
||||||
ID.long(groupMessagePollingModule.targetGroupId)
|
ID.long(groupMessagePollingModule.targetGroupId)
|
||||||
|
|
@ -163,7 +169,7 @@ class McServerStatusModule(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private suspend fun processCommand(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
|
private suspend fun processCommand(msg: MsgHistorySpecificMsg) {
|
||||||
// 找出文本内容
|
// 找出文本内容
|
||||||
val text = msg.message
|
val text = msg.message
|
||||||
.firstOrNull { it.type == MessageType.Text }
|
.firstOrNull { it.type == MessageType.Text }
|
||||||
|
|
@ -171,16 +177,18 @@ class McServerStatusModule(
|
||||||
?.trim()
|
?.trim()
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
// 解析命令
|
// 使用命令解析器解析命令
|
||||||
val matchedCommand = commands.firstOrNull { text.startsWith(it) } ?: return
|
val parsedCommand = commandParser.parseCommand(text) ?: return
|
||||||
var address = text.removePrefix(matchedCommand).trim()
|
val (_, address) = parsedCommand
|
||||||
|
|
||||||
// 使用预设别名替换
|
// 使用预设别名替换
|
||||||
presetServerByAlias[address.lowercase()]?.let { presetIp ->
|
val finalAddress = if (address.isNotEmpty()) {
|
||||||
address = presetIp
|
presetServerByAlias[address.lowercase()] ?: address
|
||||||
|
} else {
|
||||||
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (address.isEmpty()) {
|
if (finalAddress.isEmpty()) {
|
||||||
sendFailedMessage(
|
sendFailedMessage(
|
||||||
napCatClient,
|
napCatClient,
|
||||||
msg.userId,
|
msg.userId,
|
||||||
|
|
@ -192,9 +200,8 @@ class McServerStatusModule(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val status = mcSrvStatusClient.getServerStatus(address) // 返回 McServerStatus
|
val status = mcSrvStatusClient.getServerStatus(finalAddress)
|
||||||
|
|
||||||
// 检查是否查询失败
|
|
||||||
if (!status.online) {
|
if (!status.online) {
|
||||||
sendFailedMessage(
|
sendFailedMessage(
|
||||||
napCatClient, msg.userId, msg.realId, msg.time,
|
napCatClient, msg.userId, msg.realId, msg.time,
|
||||||
|
|
@ -203,9 +210,7 @@ class McServerStatusModule(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询成功,发送状态消息
|
sendStatusForwardMessage(napCatClient, msg, finalAddress, status, msg.realId, msg.time)
|
||||||
sendStatusForwardMessage(napCatClient, msg, address, status, msg.realId, msg.time)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LoggerUtil.logger.error("查询服务器状态失败: $address", e)
|
LoggerUtil.logger.error("查询服务器状态失败: $address", e)
|
||||||
sendFailedMessage(
|
sendFailedMessage(
|
||||||
|
|
@ -221,7 +226,7 @@ class McServerStatusModule(
|
||||||
// ---------------- 转发消息封装 ----------------
|
// ---------------- 转发消息封装 ----------------
|
||||||
private suspend fun sendStatusForwardMessage(
|
private suspend fun sendStatusForwardMessage(
|
||||||
client: NapCatClient,
|
client: NapCatClient,
|
||||||
msg: GetFriendMsgHistoryEvent.SpecificMsg,
|
msg: MsgHistorySpecificMsg,
|
||||||
address: String,
|
address: String,
|
||||||
status: McServerStatus,
|
status: McServerStatus,
|
||||||
realId: Long,
|
realId: Long,
|
||||||
|
|
@ -311,23 +316,36 @@ class McServerStatusModule(
|
||||||
data class CooldownState(
|
data class CooldownState(
|
||||||
val map: Map<Long, TriggerDetail> = emptyMap()
|
val map: Map<Long, TriggerDetail> = emptyMap()
|
||||||
) {
|
) {
|
||||||
|
// 获取上次处理时间
|
||||||
fun getLastTriggerTime(qq: Long): Long = map[qq]?.time ?: -1
|
fun getLastTriggerTime(qq: Long): Long = map[qq]?.time ?: -1
|
||||||
|
|
||||||
|
// 获取上次处理消息ID
|
||||||
fun getLastTriggerRealId(qq: Long): Long = map[qq]?.realId ?: -1
|
fun getLastTriggerRealId(qq: Long): Long = map[qq]?.realId ?: -1
|
||||||
fun updateLastTrigger(qq: Long, realId: Long, time: Long = -1): CooldownState {
|
|
||||||
|
// 获取上次冷却消息ID
|
||||||
|
fun getLastCooldownRealId(qq: Long): Long = map[qq]?.lastCooldownRealId ?: -1
|
||||||
|
|
||||||
|
// 冷却结束,允许处理消息 → 更新 time 和 realId
|
||||||
|
fun updateLastTrigger(qq: Long, realId: Long, time: Long): CooldownState {
|
||||||
val old = map[qq]
|
val old = map[qq]
|
||||||
val newTime = if (time != -1L) time else old?.time ?: -1
|
|
||||||
val newMap = map.toMutableMap().apply {
|
val newMap = map.toMutableMap().apply {
|
||||||
put(qq, TriggerDetail(realId, newTime, old?.lastCooldownRealId ?: -1))
|
put(qq, TriggerDetail(
|
||||||
|
realId = realId, // 当前允许处理消息ID
|
||||||
|
time = time, // 当前允许处理消息时间
|
||||||
|
lastCooldownRealId = old?.lastCooldownRealId ?: -1 // 保留冷却中记录的消息ID
|
||||||
|
))
|
||||||
}
|
}
|
||||||
return copy(map = newMap)
|
return copy(map = newMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 冷却中消息 → 只更新 lastCooldownRealId,保留 time 和 realId
|
||||||
fun updateLastCooldownRealId(qq: Long, realId: Long): CooldownState {
|
fun updateLastCooldownRealId(qq: Long, realId: Long): CooldownState {
|
||||||
val old = map[qq]
|
val old = map[qq]
|
||||||
val newMap = map.toMutableMap().apply {
|
val newMap = map.toMutableMap().apply {
|
||||||
put(qq, TriggerDetail(
|
put(qq, TriggerDetail(
|
||||||
realId = old?.realId ?: -1,
|
realId = old?.realId ?: -1, // 保持上次允许处理的消息ID
|
||||||
time = old?.time ?: -1,
|
time = old?.time ?: -1, // 保持上次允许处理的时间
|
||||||
lastCooldownRealId = realId
|
lastCooldownRealId = realId // 更新当前冷却拒绝的消息ID
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
return copy(map = newMap)
|
return copy(map = newMap)
|
||||||
|
|
@ -336,9 +354,9 @@ class McServerStatusModule(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TriggerDetail(
|
data class TriggerDetail(
|
||||||
val realId: Long,
|
val realId: Long, // 上次允许处理消息ID
|
||||||
val time: Long,
|
val time: Long, // 上次允许处理消息时间(秒)
|
||||||
val lastCooldownRealId: Long = -1L
|
val lastCooldownRealId: Long = -1 // 上次被冷却拒绝的消息ID
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun loadState(): CooldownState {
|
override fun loadState(): CooldownState {
|
||||||
|
|
@ -368,4 +386,33 @@ class McServerStatusModule(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun info(): String {
|
||||||
|
return buildString {
|
||||||
|
appendLine("模块名称: $name")
|
||||||
|
appendLine("模块类型: McServerStatusModule")
|
||||||
|
appendLine("目标群组: ${groupMessagePollingModule.targetGroupId}")
|
||||||
|
appendLine("机器人昵称: $selfNickName (ID: $selfId)")
|
||||||
|
appendLine("冷却时间: ${cooldownMillis / 1000} 秒")
|
||||||
|
appendLine("支持命令: ${commands.joinToString(", ")}")
|
||||||
|
appendLine("预设服务器别名:")
|
||||||
|
presetServer.forEach { (aliases, ip) ->
|
||||||
|
appendLine(" ${aliases.joinToString("/")} -> $ip")
|
||||||
|
}
|
||||||
|
appendLine("状态文件路径: ${stateFile.absolutePath}")
|
||||||
|
appendLine("状态备份文件路径: ${stateBackupFile.absolutePath}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 返回模块使用帮助
|
||||||
|
override fun help(): String = buildString {
|
||||||
|
appendLine("使用帮助 - McServerStatusModule")
|
||||||
|
appendLine("指令格式: /mcs <服务器别名或IP> 或 /s <服务器别名或IP>")
|
||||||
|
appendLine("示例:")
|
||||||
|
presetServerByAlias.forEach { (alias, ip) ->
|
||||||
|
appendLine(" /mcs $alias -> 查询服务器 $ip 状态")
|
||||||
|
}
|
||||||
|
appendLine("注意事项:")
|
||||||
|
appendLine(" - 查询冷却时间为 ${cooldownMillis / 1000} 秒")
|
||||||
|
appendLine(" - 输入服务器 IP 或别名均可")
|
||||||
|
appendLine(" - 查询结果会以转发消息形式发送到群组")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||||
|
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||||
|
import top.r3944realms.ltdmanager.napcat.event.NapCatEvent
|
||||||
|
import top.r3944realms.ltdmanager.napcat.event.account.GetStrangerInfoEvent
|
||||||
|
import top.r3944realms.ltdmanager.napcat.event.group.GetGroupIgnoredNotifiesEvent
|
||||||
|
import top.r3944realms.ltdmanager.napcat.event.group.GetGroupSystemMsgEvent
|
||||||
|
import top.r3944realms.ltdmanager.napcat.request.account.GetStrangerInfoRequest
|
||||||
|
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupIgnoredNotifiesRequest
|
||||||
|
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupSystemMsgRequest
|
||||||
|
import top.r3944realms.ltdmanager.napcat.request.group.SetGroupAddRequestRequest
|
||||||
|
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||||
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块: 入群申请自动处理
|
||||||
|
* 功能:
|
||||||
|
* 1. 监听目标群的入群申请事件
|
||||||
|
* 2. 根据 answers 列表自动同意或拒绝
|
||||||
|
*/
|
||||||
|
class ModGroupHandlerModule(
|
||||||
|
moduleName: String,
|
||||||
|
private val targetGroupId: Long,
|
||||||
|
private val answers: List<String> = listOf("正确答案"),
|
||||||
|
private val pollIntervalMillis: Long = 30_000L
|
||||||
|
) : BaseModule(Modules.MOD_GROUP_HANDLER, moduleName), PersistentState<ModGroupHandlerModule.RejectRecords> {
|
||||||
|
|
||||||
|
private var scope: CoroutineScope? = null
|
||||||
|
private val stateFile: File = getStateFileInternal("reject_records.json", name)
|
||||||
|
private val fileLock = ReentrantLock()
|
||||||
|
private var stateCache: RejectRecords? = null
|
||||||
|
private val json = Json { prettyPrint = true; encodeDefaults = true }
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RejectRecord(
|
||||||
|
val userId: Long,
|
||||||
|
var reason: MutableList<String> = mutableListOf(),
|
||||||
|
var rejectCount: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录所有被拒绝用户的Map,key = userId
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class RejectRecords(
|
||||||
|
val records: MutableMap<Long, RejectRecord> = mutableMapOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getStateFileInternal(): File = stateFile
|
||||||
|
|
||||||
|
override fun getState(): RejectRecords {
|
||||||
|
if (stateCache == null) stateCache = loadState()
|
||||||
|
return stateCache!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveState(state: RejectRecords) {
|
||||||
|
fileLock.withLock {
|
||||||
|
try {
|
||||||
|
stateFile.writeText(json.encodeToString(state))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.error("[$name] 保存拒绝记录失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadState(): RejectRecords {
|
||||||
|
return try {
|
||||||
|
if (!stateFile.exists()) return RejectRecords()
|
||||||
|
val text = stateFile.readText()
|
||||||
|
json.decodeFromString(RejectRecords.serializer(), text)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.warn("[$name] 拒绝记录加载失败,使用默认值", e)
|
||||||
|
RejectRecords()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addReject(userId: Long, reason: String) {
|
||||||
|
val state = getState()
|
||||||
|
val record = state.records[userId]
|
||||||
|
if (record != null) {
|
||||||
|
record.rejectCount += 1
|
||||||
|
record.reason.add(reason)
|
||||||
|
} else {
|
||||||
|
state.records[userId] = RejectRecord(userId, mutableListOf(reason), 1)
|
||||||
|
}
|
||||||
|
saveState(state)
|
||||||
|
}
|
||||||
|
private fun getRejectRecord(userId: Long): RejectRecord? {
|
||||||
|
return getState().records[userId]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoad() {
|
||||||
|
LoggerUtil.logger.info("[$name] 模块已装载,目标群组: $targetGroupId")
|
||||||
|
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
scope!!.launch {
|
||||||
|
LoggerUtil.logger.info("[$name] 轮询协程启动")
|
||||||
|
while (isActive && loaded) {
|
||||||
|
try {
|
||||||
|
handleEvents()
|
||||||
|
delay(pollIntervalMillis)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.error("[$name] 轮询异常", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onUnload() {
|
||||||
|
LoggerUtil.logger.info("[$name] 模块卸载")
|
||||||
|
scope?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleEvents() {
|
||||||
|
val systemEvent: GetGroupSystemMsgEvent = napCatClient.send(GetGroupSystemMsgRequest())
|
||||||
|
handleEvent(systemEvent)
|
||||||
|
|
||||||
|
val ignoredEvent: GetGroupIgnoredNotifiesEvent = napCatClient.send(GetGroupIgnoredNotifiesRequest())
|
||||||
|
handleEvent(ignoredEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleEvent(event: Any) {
|
||||||
|
if (!loaded) return
|
||||||
|
val provider: GroupRequestProvider? = when (event) {
|
||||||
|
is GetGroupSystemMsgEvent -> event.asProvider()
|
||||||
|
is GetGroupIgnoredNotifiesEvent -> event.asProvider()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
provider?.getAllRequests()?.forEach { request ->
|
||||||
|
if (!request.checked && request.groupId == targetGroupId) {
|
||||||
|
LoggerUtil.logger.info("[$name] 处理请求: requestId=${request.requestId},requestQQ =${request.invitorUin}")
|
||||||
|
val pattern = """答案:(.*)""".toRegex()
|
||||||
|
val answer = pattern.find(request.message)?.groupValues?.get(1) ?: ""
|
||||||
|
val answerAllow = answers.contains(answer)
|
||||||
|
if (answerAllow) {
|
||||||
|
val info = napCatClient.send<GetStrangerInfoEvent>(GetStrangerInfoRequest(ID.long(request.invitorUin)))
|
||||||
|
val levelAllow = info.data.qqLevel >= 16
|
||||||
|
val setRequest = SetGroupAddRequestRequest(
|
||||||
|
levelAllow,
|
||||||
|
request.requestId.toString(),
|
||||||
|
if(!levelAllow) "QQ等级低于16级" else ""
|
||||||
|
)
|
||||||
|
napCatClient.send<NapCatEvent>(setRequest)
|
||||||
|
if (levelAllow) {
|
||||||
|
napCatClient.send<NapCatEvent>(
|
||||||
|
SendGroupMsgRequest(
|
||||||
|
listOf(
|
||||||
|
MessageElement.at(ID.long(request.invitorUin), request.requesterNick),
|
||||||
|
MessageElement.text("\n"),
|
||||||
|
MessageElement.text(
|
||||||
|
formatRejectRecordMessage(request.invitorUin)
|
||||||
|
)
|
||||||
|
), ID.long(targetGroupId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LoggerUtil.logger.info("[$name] 已${if (levelAllow) "同意" else "拒绝"} 请求${if(!levelAllow) ",等级不够,${info.data.qqLevel}" else "" }: ${request.requestId}")
|
||||||
|
if(levelAllow) stateCache?.records?.remove(request.invitorUin)
|
||||||
|
} else {
|
||||||
|
val rejectCount = (getRejectRecord(request.invitorUin)?.rejectCount ?: 0) + 1
|
||||||
|
napCatClient.sendUnit(SetGroupAddRequestRequest(false, request.requestId.toString(), "答案错误,请输入标准答案,拒绝次数:${rejectCount}"))
|
||||||
|
addReject(request.invitorUin, answer)
|
||||||
|
LoggerUtil.logger.info("[$name] 答案错误:${answer},已拒绝请求: ${request.requestId}")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun formatRejectRecordMessage(userId: Long): String {
|
||||||
|
val record = getRejectRecord(userId)
|
||||||
|
return if (record != null) {
|
||||||
|
"""
|
||||||
|
📊 用户审核记录
|
||||||
|
──────────────────
|
||||||
|
🔹 用户QQ号:${record.userId}
|
||||||
|
🔹 尝试次数:${record.rejectCount}
|
||||||
|
🔹 最终评分:${rate(record.rejectCount)}
|
||||||
|
|
||||||
|
📝 尝试答案:
|
||||||
|
${ "\n" + record.reason.joinToString("\n") { " • $it" }}
|
||||||
|
|
||||||
|
⚠️ 提示:请仔细阅读群文档后再在群里提问,否则你会失去你的大脑🧠
|
||||||
|
""".trimIndent()
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
📊 用户审核记录
|
||||||
|
──────────────────
|
||||||
|
🔹 用户QQ号:${userId}
|
||||||
|
🔹 尝试次数:0
|
||||||
|
🔹 最终评分:SSS ⭐
|
||||||
|
|
||||||
|
💡 该用户尚未有审核记录
|
||||||
|
⚠️ 提示:请仔细阅读群文档后再在群里提问,否则你会失去你的大脑🧠
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun rate(count: Int): String = when (count) {
|
||||||
|
0 -> "S"
|
||||||
|
1 -> "A"
|
||||||
|
2 -> "B"
|
||||||
|
3 -> "C"
|
||||||
|
4 -> "D"
|
||||||
|
else -> "F"
|
||||||
|
}
|
||||||
|
interface GroupRequestProvider {
|
||||||
|
fun getAllRequests(): List<GetGroupSystemMsgEvent.SystemInfo>
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GetGroupSystemMsgEvent.asProvider(): GroupRequestProvider = object : GroupRequestProvider {
|
||||||
|
override fun getAllRequests(): List<GetGroupSystemMsgEvent.SystemInfo> =
|
||||||
|
data.invitedRequest + data.joinRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GetGroupIgnoredNotifiesEvent.asProvider(): GroupRequestProvider = object : GroupRequestProvider {
|
||||||
|
override fun getAllRequests(): List<GetGroupSystemMsgEvent.SystemInfo> =
|
||||||
|
data.invitedRequest + data.joinRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun info(): String = """
|
||||||
|
模块: $name
|
||||||
|
功能: 自动处理指定群组的入群申请
|
||||||
|
1. 根据答案列表自动同意或拒绝
|
||||||
|
2. 拒绝记录会保存到本地,并可查询尝试次数和尝试答案
|
||||||
|
3. 用户通过验证且等级满足要求时,会向群里发送消息,显示用户QQ号、尝试次数、评分和尝试答案
|
||||||
|
版本: 1.0
|
||||||
|
""".trimIndent()
|
||||||
|
override fun help(): String = "轮询群组入群申请,根据答案列表自动同意或拒绝,并记录拒绝用户信息"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.GlobalManager
|
||||||
|
import top.r3944realms.ltdmanager.core.config.ModuleConfig
|
||||||
|
import top.r3944realms.ltdmanager.core.config.ModuleConfig.Module.ModuleType.*
|
||||||
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
|
import top.r3944realms.ltdmanager.module.exception.ConfigError
|
||||||
|
|
||||||
|
object ModuleFactory {
|
||||||
|
fun createModule(config: ModuleConfig.Module): BaseModule {
|
||||||
|
return when(config.type) {
|
||||||
|
GROUP_MESSAGE_POLLING_MODULE -> createGroupMessagePolling(config)
|
||||||
|
GROUP_REQUEST_HANDLER_MODULE -> createGroupRequestHandler(config)
|
||||||
|
MAIL_MODULE -> createMail(config)
|
||||||
|
BAN_MODULE -> createBan(config)
|
||||||
|
DG_LAB_MODULE -> createDgLab(config)
|
||||||
|
INVITE_MODULE -> createInvite(config)
|
||||||
|
MC_SERVER_STATUS_MODULE -> createMcServerStatus(config)
|
||||||
|
RCON_PLAYER_LIST_MODULE -> createRconPlayerList(config)
|
||||||
|
STATE_MODULE -> createState(config)
|
||||||
|
MOD_GROUP_HANDLER_MODULE -> createModGroupHandler(config)
|
||||||
|
HELP_MODULE -> createHelpModule(config)
|
||||||
|
UNKNOWN_MODULE -> throw ConfigError(ConfigError.Type.INVALID_PARAMETER, "unknown module")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun resolveDependency(dep: ModuleConfig.Module.Dependency?, name: String): BaseModule? {
|
||||||
|
if (dep != null) {
|
||||||
|
return GlobalManager.moduleManager.getModule(dep.getDepName())
|
||||||
|
} else throw ConfigError (ConfigError.Type.MISSING_PARAMETER, "dependency", name)
|
||||||
|
}
|
||||||
|
private fun createGroupMessagePolling(config: ModuleConfig.Module): GroupMessagePollingModule {
|
||||||
|
val targetGroupId = config.long("target-group-id")
|
||||||
|
val pollIntervalMillis = config.getOrDefault("poll-interval-millis", 5_000L)
|
||||||
|
val msgHistoryCheck = config.getOrDefault("msg-history-check", 15)
|
||||||
|
return GroupMessagePollingModule(
|
||||||
|
config.name, targetGroupId,
|
||||||
|
pollIntervalMillis, msgHistoryCheck,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private fun createGroupRequestHandler(config: ModuleConfig.Module): GroupRequestHandlerModule {
|
||||||
|
val targetGroupId = config.long("target-group-id")
|
||||||
|
val pollIntervalMillis = config.getOrDefault("poll-interval-millis", 30_000L)
|
||||||
|
return GroupRequestHandlerModule(
|
||||||
|
config.name, GlobalManager.napCatClient,
|
||||||
|
targetGroupId, pollIntervalMillis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMail(config: ModuleConfig.Module): MailModule {
|
||||||
|
val mailConfig = YamlConfigLoader.loadMailConfig()
|
||||||
|
return MailModule(
|
||||||
|
moduleName = config.name,
|
||||||
|
host = mailConfig.host.toString(),
|
||||||
|
authToken = mailConfig.decryptedPassword.toString(),
|
||||||
|
port = mailConfig.port!!,
|
||||||
|
senderEmailAddress = mailConfig.mailAddress!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createBan(config: ModuleConfig.Module): BanModule {
|
||||||
|
val selfId = config.long("self-id")
|
||||||
|
val adminIds = config.list<Long>("admin-ids")
|
||||||
|
val muteCommandPrefixList = config.stringList("mute-command-prefix-list")
|
||||||
|
val unmuteCommandPrefixList = config.stringList("unmute-command-prefix-list")
|
||||||
|
val groupMessagePollingModule = resolveDependency(config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling") as GroupMessagePollingModule
|
||||||
|
val minBanMinutes = config.int("min-ban-minutes")
|
||||||
|
val maxBanMinutes = config.int("max-ban-minutes")
|
||||||
|
val factorX: Int = config.int("factor-x")
|
||||||
|
return BanModule(
|
||||||
|
config.name,
|
||||||
|
groupMessagePollingModule,
|
||||||
|
selfId,
|
||||||
|
adminIds,
|
||||||
|
muteCommandPrefixList,
|
||||||
|
unmuteCommandPrefixList,
|
||||||
|
minBanMinutes,
|
||||||
|
maxBanMinutes,
|
||||||
|
factorX
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDgLab(config: ModuleConfig.Module): DGLabModule {
|
||||||
|
val selfId = config.long("self-id")
|
||||||
|
val adminIds = config.list<Long>("admin-ids")
|
||||||
|
val maxClientNumber = config.int("max-client-number")
|
||||||
|
val commandHead = config.stringList("command-head")
|
||||||
|
val groupMessagePollingModule = resolveDependency(config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling") as GroupMessagePollingModule
|
||||||
|
return DGLabModule(
|
||||||
|
config.name,
|
||||||
|
groupMessagePollingModule,
|
||||||
|
selfId,
|
||||||
|
adminIds,
|
||||||
|
maxClientNumber,
|
||||||
|
commandHead
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createInvite(config: ModuleConfig.Module): InvitationCodesModule {
|
||||||
|
val selfId = config.long("self-id")
|
||||||
|
val groupMessagePollingModule = resolveDependency(config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling") as GroupMessagePollingModule
|
||||||
|
val mailModule = resolveDependency(config.findDependency(MAIL_MODULE), "mailModule") as MailModule
|
||||||
|
val blessingSkinConfig = YamlConfigLoader.loadBlessingSkinServerConfig()
|
||||||
|
val cooldownMillis = config.getOrDefault("cooldown-millis", 120_000L)
|
||||||
|
val keywords = config.stringList("keywords")
|
||||||
|
return InvitationCodesModule(
|
||||||
|
config.name,
|
||||||
|
groupMessagePollingModule,
|
||||||
|
mailModule,
|
||||||
|
blessingSkinConfig.invitationApi?.decryptedToken!!,
|
||||||
|
selfId,
|
||||||
|
cooldownMillis,
|
||||||
|
keywords.toSet()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMcServerStatus(config: ModuleConfig.Module): McServerStatusModule {
|
||||||
|
val selfId = config.long("self-id")
|
||||||
|
val cooldownMillis = config.getOrDefault("cooldown-millis", 60_000L)
|
||||||
|
val groupMessagePollingModule = resolveDependency(config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling") as GroupMessagePollingModule
|
||||||
|
val commands = config.stringList("commands")
|
||||||
|
val selfNickName = config.string("self-nick-name")
|
||||||
|
val preset = config.map<Set<String>, String>("preset-server")
|
||||||
|
return McServerStatusModule(
|
||||||
|
config.name,
|
||||||
|
groupMessagePollingModule,
|
||||||
|
selfId,
|
||||||
|
selfNickName,
|
||||||
|
cooldownMillis,
|
||||||
|
commands,
|
||||||
|
preset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRconPlayerList(config: ModuleConfig.Module): RconPlayerListModule {
|
||||||
|
val toolConfig = YamlConfigLoader.loadToolConfig()
|
||||||
|
val selfId = config.long("self-id")
|
||||||
|
val cooldownMillis = config.getOrDefault("cooldown-millis", 10_000L)
|
||||||
|
val rconTimeout = config.getOrDefault("rcon-timeout-millis", 2_000L)
|
||||||
|
val groupMessagePollingModule = resolveDependency(config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling") as GroupMessagePollingModule
|
||||||
|
val selfNickName = config.string("self-nick-name")
|
||||||
|
val keywords = config.stringList("keywords")
|
||||||
|
return RconPlayerListModule(
|
||||||
|
config.name,
|
||||||
|
groupMessagePollingModule,
|
||||||
|
rconTimeout,
|
||||||
|
cooldownMillis,
|
||||||
|
selfId,
|
||||||
|
selfNickName,
|
||||||
|
toolConfig.rcon.mcRconToolPath.toString(),
|
||||||
|
toolConfig.rcon.mcRconToolConfigPath.toString(),
|
||||||
|
keywords.toSet()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createState(config: ModuleConfig.Module): StateModule {
|
||||||
|
val onlineName = config.string("online-name")
|
||||||
|
val offlineName = config.string("offline-name")
|
||||||
|
return StateModule(
|
||||||
|
config.name,
|
||||||
|
onlineName,
|
||||||
|
offlineName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createModGroupHandler(config: ModuleConfig.Module): ModGroupHandlerModule {
|
||||||
|
val targetGroupId = config.long("target-group-id")
|
||||||
|
val answers = config.stringList("answers")
|
||||||
|
val pollingMillis = config.getOrDefault("poll-interval-millis", 30_000L)
|
||||||
|
return ModGroupHandlerModule(
|
||||||
|
config.name,
|
||||||
|
targetGroupId,
|
||||||
|
answers,
|
||||||
|
pollingMillis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createHelpModule(config: ModuleConfig.Module): HelpModule {
|
||||||
|
val selfId = config.long("self-id")
|
||||||
|
val cooldownMillis = config.getOrDefault("cooldown-millis", 10_000L)
|
||||||
|
val groupMessagePollingModule = resolveDependency(config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling") as GroupMessagePollingModule
|
||||||
|
val selfNickName = config.string("self-nick-name")
|
||||||
|
val keywords = config.stringList("keywords")
|
||||||
|
return HelpModule(
|
||||||
|
config.name,
|
||||||
|
groupMessagePollingModule,
|
||||||
|
selfId,
|
||||||
|
selfNickName,
|
||||||
|
keywords,
|
||||||
|
cooldownMillis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,43 @@
|
||||||
package top.r3944realms.ltdmanager.module
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class ModuleManager {
|
class ModuleManager {
|
||||||
|
|
||||||
private val modules = mutableMapOf<String, BaseModule>()
|
private val modules = mutableMapOf<String, BaseModule>()
|
||||||
|
private var hasLoaded = false
|
||||||
|
fun getModules(): Map<String, BaseModule> {
|
||||||
|
return (modules).toMap()
|
||||||
|
}
|
||||||
|
fun getModule(name: String): BaseModule? {
|
||||||
|
return modules[name]
|
||||||
|
}
|
||||||
|
fun loadConfig() {
|
||||||
|
if (!hasLoaded) {
|
||||||
|
hasLoaded = true
|
||||||
|
val moduleConfig = YamlConfigLoader.loadModuleConfig()
|
||||||
|
moduleConfig.modules.let {
|
||||||
|
val enableBaseModules = LinkedList<BaseModule>()
|
||||||
|
if (it != null) {
|
||||||
|
for (mod in it) {
|
||||||
|
val module = ModuleFactory.createModule(mod)
|
||||||
|
register(module)
|
||||||
|
if (mod.enabled) {
|
||||||
|
enableBaseModules.add(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (module in enableBaseModules) {
|
||||||
|
load(module.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 注册模块到管理器
|
* 注册模块到管理器
|
||||||
*/
|
*/
|
||||||
fun registerModule(module: BaseModule) {
|
fun register(module: BaseModule) {
|
||||||
if (modules.containsKey(module.name)) {
|
if (modules.containsKey(module.name)) {
|
||||||
LoggerUtil.logger.warn("模块已注册: ${module.name}")
|
LoggerUtil.logger.warn("模块已注册: ${module.name}")
|
||||||
return
|
return
|
||||||
|
|
@ -17,11 +45,19 @@ class ModuleManager {
|
||||||
modules[module.name] = module
|
modules[module.name] = module
|
||||||
LoggerUtil.logger.info("模块注册: ${module.name}")
|
LoggerUtil.logger.info("模块注册: ${module.name}")
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 注册多模块到管理器
|
||||||
|
*/
|
||||||
|
fun register(moduleList: List<BaseModule>) {
|
||||||
|
for (module in moduleList) {
|
||||||
|
register(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载指定模块
|
* 加载指定模块
|
||||||
*/
|
*/
|
||||||
fun loadModule(name: String) {
|
fun load(name: String) {
|
||||||
val module = modules[name]
|
val module = modules[name]
|
||||||
if (module == null) {
|
if (module == null) {
|
||||||
LoggerUtil.logger.warn("尝试加载不存在的模块: $name")
|
LoggerUtil.logger.warn("尝试加载不存在的模块: $name")
|
||||||
|
|
@ -41,7 +77,7 @@ class ModuleManager {
|
||||||
/**
|
/**
|
||||||
* 卸载指定模块
|
* 卸载指定模块
|
||||||
*/
|
*/
|
||||||
suspend fun unloadModule(name: String) {
|
suspend fun unload(name: String) {
|
||||||
val module = modules[name]
|
val module = modules[name]
|
||||||
if (module == null) {
|
if (module == null) {
|
||||||
LoggerUtil.logger.warn("尝试卸载不存在的模块: $name")
|
LoggerUtil.logger.warn("尝试卸载不存在的模块: $name")
|
||||||
|
|
@ -74,6 +110,13 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提供获取所有模块信息的方法
|
||||||
|
*/
|
||||||
|
fun getAllModuleInfo(): Map<String, String> {
|
||||||
|
return modules.mapValues { it.value.info() }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有模块名称
|
* 获取所有模块名称
|
||||||
*/
|
*/
|
||||||
|
|
@ -89,14 +132,14 @@ class ModuleManager {
|
||||||
* 扩展方法:批量加载模块
|
* 扩展方法:批量加载模块
|
||||||
*/
|
*/
|
||||||
fun ModuleManager.loadModules(vararg names: String) {
|
fun ModuleManager.loadModules(vararg names: String) {
|
||||||
names.forEach { loadModule(it) }
|
names.forEach { load(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扩展方法:批量卸载模块
|
* 扩展方法:批量卸载模块
|
||||||
*/
|
*/
|
||||||
suspend fun ModuleManager.unloadModules(vararg names: String) {
|
suspend fun ModuleManager.unloadModules(vararg names: String) {
|
||||||
names.forEach { unloadModule(it) }
|
names.forEach { unload(it) }
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 关闭所有模块
|
* 关闭所有模块
|
||||||
|
|
|
||||||
26
src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt
Normal file
26
src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object Modules {
|
||||||
|
private val MODULES: MutableList<String> = LinkedList();
|
||||||
|
val BAN: String = register("BanModule")
|
||||||
|
val APPLY_WHITELIST: String = register("ApplyWhitelistModule")
|
||||||
|
val DG_LAB: String = register("DGLabModule")
|
||||||
|
val GROUP_MESSAGE_POLLING: String = register("GroupMessagePollingModule")
|
||||||
|
val GROUP_REQUEST_HANDLER: String = register("GroupRequestHandlerModule")
|
||||||
|
val HELP: String = register("HelpModule")
|
||||||
|
val MAIL: String = register("MailModule")
|
||||||
|
val MC_SERVER_STATUS: String = register("MCServerStatusModule")
|
||||||
|
val MOD_GROUP_HANDLER: String = register("ModGroupHandlerModule")
|
||||||
|
val RCON_PLAYER_LIST: String = register("RconPlayerListModule")
|
||||||
|
val INVITATION_CODE: String = register("InvitationCodeModule")
|
||||||
|
val STATE: String = register("StateModule")
|
||||||
|
fun register(name: String): String {
|
||||||
|
MODULES.add(name)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
fun getModules(): Array<String> {
|
||||||
|
return MODULES.toTypedArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
package top.r3944realms.ltdmanager.module
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.utils.FileNameFilter
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface PersistentState<T> {
|
interface PersistentState<T> {
|
||||||
fun getStateFile(): File
|
fun getStateFileInternal(): File
|
||||||
fun getState(): T
|
fun getState(): T
|
||||||
fun saveState(state: T)
|
fun saveState(state: T)
|
||||||
fun loadState(): T
|
fun loadState(): T
|
||||||
// 默认实现:统一管理 data 目录下的文件
|
// 默认实现:统一管理 data 目录下的文件
|
||||||
fun getStateFile(name: String): File {
|
fun getStateFileInternal(name: String, subName: String): File {
|
||||||
val dataDir = File("data")
|
val dataDir = File("data", FileNameFilter.filterFileName(subName))
|
||||||
if (!dataDir.exists()) dataDir.mkdirs()
|
if (!dataDir.exists()) dataDir.mkdirs()
|
||||||
return File(dataDir, name)
|
return File(dataDir, name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,19 @@ import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import top.r3944realms.ltdmanager.module.RconPlayerListModule.LastTriggerState
|
import top.r3944realms.ltdmanager.module.RconPlayerListModule.LastTriggerState
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||||||
|
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownStateProvider
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter
|
||||||
|
import top.r3944realms.ltdmanager.module.common.filter.type.CooldownFilter
|
||||||
|
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.NapCatClient
|
||||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||||
import top.r3944realms.ltdmanager.napcat.data.MessageType
|
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.message.SendForwardMsgRequest
|
||||||
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||||
import top.r3944realms.ltdmanager.utils.CmdUtil
|
import top.r3944realms.ltdmanager.utils.CmdUtil
|
||||||
|
|
@ -18,26 +26,61 @@ import java.io.File
|
||||||
import java.util.concurrent.TimeoutException
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
class RconPlayerListModule(
|
class RconPlayerListModule(
|
||||||
|
moduleName: String,
|
||||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||||
private val rconTimeOut: Long = 2_000L,
|
private val rconTimeOut: Long = 2_000L,
|
||||||
private val cooldownMillis: Long = 30_000L,
|
private val cooldownMillis: Long = 30_000L,
|
||||||
private var lastSuccessTime: Long = 0L,
|
|
||||||
private val selfId: Long,
|
private val selfId: Long,
|
||||||
private val selfNickName: String,
|
private val selfNickName: String,
|
||||||
private val rconPath: String,
|
private val rconPath: String,
|
||||||
private val rconConfigPath: String,
|
private val rconConfigPath: String,
|
||||||
private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家")
|
private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家")
|
||||||
) : BaseModule(), PersistentState<LastTriggerState> {
|
) : BaseModule(Modules.RCON_PLAYER_LIST, moduleName), PersistentState<LastTriggerState> {
|
||||||
|
private val cooldownManager by lazy {
|
||||||
override val name: String = "RconPlayerListModule"
|
CooldownManager(
|
||||||
|
cooldownMillis = cooldownMillis,
|
||||||
|
scope = CooldownScope.Global,
|
||||||
|
stateProvider = object : CooldownStateProvider<LastTriggerState> {
|
||||||
|
override fun load() = loadState()
|
||||||
|
override fun save(state: LastTriggerState) = saveState(state)
|
||||||
|
},
|
||||||
|
getLastTrigger = { state, _ -> state.lastTriggerTime to state.lastTriggeredRealId },
|
||||||
|
updateTrigger = { state, _, realId, time ->
|
||||||
|
// ✅ 消息成功触发时更新状态
|
||||||
|
state.updateTrigger(realId, time)
|
||||||
|
state
|
||||||
|
},
|
||||||
|
updateCooldownRealId = { state, _, realId ->
|
||||||
|
// ✅ 消息被冷却拒绝时更新 lastCooldownRealId
|
||||||
|
state.updateCooldownRealId(realId)
|
||||||
|
state
|
||||||
|
},
|
||||||
|
groupId = groupMessagePollingModule.targetGroupId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/** 抽象过滤器组合 —— lazy 避免初始化顺序问题 */
|
||||||
|
private val triggerFilter by lazy {
|
||||||
|
TriggerMessageFilter(
|
||||||
|
listOf(
|
||||||
|
IgnoreSelfFilter(selfId),
|
||||||
|
NewMessageFilter { _ ->
|
||||||
|
lastTriggerState.lastTriggerTime to lastTriggerState.lastTriggeredRealId
|
||||||
|
},
|
||||||
|
KeywordFilter(keywords),
|
||||||
|
CooldownFilter(cooldownManager) { msg, remain ->
|
||||||
|
sendCooldownMessage(napCatClient, msg.realId, remain)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
private var scope : CoroutineScope? = null
|
private var scope : CoroutineScope? = null
|
||||||
|
|
||||||
// 持久化文件路径
|
// 持久化文件路径
|
||||||
private val stateFile = getStateFile("rcon_playerlist_state.json")
|
private val stateFile: File = getStateFileInternal("rcon_playerlist_state.json", name)
|
||||||
|
|
||||||
private val stateBackupFile = getStateFile("invitation_codes_quarry_state.json.bak")
|
private val stateBackupFile: File = getStateFileInternal("rcon_playerlist_state.json.bak", name)
|
||||||
|
|
||||||
override fun getStateFile(): File = stateFile
|
override fun getStateFileInternal(): File = stateFile
|
||||||
|
|
||||||
// 保存最新触发过的消息 realId 和 time
|
// 保存最新触发过的消息 realId 和 time
|
||||||
private var lastTriggerState: LastTriggerState = loadState()
|
private var lastTriggerState: LastTriggerState = loadState()
|
||||||
|
|
@ -67,104 +110,60 @@ class RconPlayerListModule(
|
||||||
LoggerUtil.logger.info("[$name] 模块已卸载完成")
|
LoggerUtil.logger.info("[$name] 模块已卸载完成")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
|
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
|
||||||
val triggerMessages = messages
|
val filtered = triggerFilter.filter(messages)
|
||||||
.asSequence() // 使用序列提高性能,特别是消息量大时
|
|
||||||
.filter { msg ->
|
|
||||||
((msg.time > lastTriggerState.lastTriggerTime ||
|
|
||||||
(msg.time == lastTriggerState.lastTriggerTime && msg.realId > lastTriggerState.lastTriggeredRealId))
|
|
||||||
&& msg.userId != selfId) &&
|
|
||||||
msg.message.any { seg ->
|
|
||||||
seg.type == MessageType.Text &&
|
|
||||||
seg.data.text?.let { text -> keywords.any { keyword -> text == keyword } } == true
|
|
||||||
}
|
|
||||||
}.toList()
|
|
||||||
|
|
||||||
if (triggerMessages.isNotEmpty()) {
|
// RCON 模块只取最新的一条消息
|
||||||
val triggerMsg = triggerMessages.maxBy { it.time }
|
val triggerMsg = filtered.maxByOrNull { it.time }
|
||||||
LoggerUtil.logger.info("[$name] 找到触发消息 realId=${triggerMsg.realId}, time=${triggerMsg.time}, userId=${triggerMsg.userId}")
|
if (triggerMsg != null) {
|
||||||
processTrigger(triggerMsg)
|
try {
|
||||||
|
processTrigger(triggerMsg)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoggerUtil.logger.error("[$name] 处理触发消息失败", e)
|
||||||
|
sendFailedMessage(napCatClient, triggerMsg.realId, triggerMsg.time, "处理异常: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private suspend fun processTrigger(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
|
private suspend fun processTrigger(msg: MsgHistorySpecificMsg) {
|
||||||
val now = System.currentTimeMillis()
|
LoggerUtil.logger.info("[$name] 执行 RCON 查询")
|
||||||
|
|
||||||
// ✅ 冷却检查(首次触发直接允许)
|
|
||||||
val canTrigger = (lastSuccessTime == 0L) || (now - lastSuccessTime >= cooldownMillis)
|
|
||||||
if (!canTrigger) {
|
|
||||||
val remaining = ((cooldownMillis - (now - lastSuccessTime)) / 1000).coerceAtLeast(1)
|
|
||||||
LoggerUtil.logger.info("[$name] 冷却中,拒绝执行,剩余 $remaining 秒")
|
|
||||||
sendCooldownMessage(napCatClient, msg.realId, msg.time)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 执行 RCON 命令
|
|
||||||
val commands = listOf("forge tps", "list")
|
val commands = listOf("forge tps", "list")
|
||||||
LoggerUtil.logger.info("[$name] 执行 RCON 命令: $commands")
|
LoggerUtil.logger.info("[$name] 执行 RCON 命令: $commands")
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val tpsOutput = runCatching {
|
val tpsOutput = CmdUtil.runExeCommand(
|
||||||
CmdUtil.runExeCommand(
|
rconPath, "-c", rconConfigPath,
|
||||||
rconPath,
|
"-T", (rconTimeOut / 1000).toString() + "s", "forge tps"
|
||||||
"-c", rconConfigPath,
|
)
|
||||||
"-T", (rconTimeOut / 1000).toString() + "s",
|
val listOutput = CmdUtil.runExeCommand(
|
||||||
"forge tps"
|
rconPath, "-c", rconConfigPath,
|
||||||
)
|
"-T", (rconTimeOut / 1000).toString() + "s", "list"
|
||||||
}.getOrElse { ex ->
|
)
|
||||||
LoggerUtil.logger.warn("[$name] 执行 forge tps 失败: ${ex.message}")
|
|
||||||
throw ex
|
|
||||||
}
|
|
||||||
|
|
||||||
val listOutput = runCatching {
|
|
||||||
CmdUtil.runExeCommand(
|
|
||||||
rconPath,
|
|
||||||
"-c", rconConfigPath,
|
|
||||||
"-T", (rconTimeOut / 1000).toString() + "s",
|
|
||||||
"list"
|
|
||||||
)
|
|
||||||
}.getOrElse { ex ->
|
|
||||||
LoggerUtil.logger.warn("[$name] 执行 list 失败: ${ex.message}")
|
|
||||||
throw ex
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tpsOutput.contains("i/o timeout") || listOutput.contains("i/o timeout")) {
|
if (tpsOutput.contains("i/o timeout") || listOutput.contains("i/o timeout")) {
|
||||||
throw TimeoutException()
|
throw TimeoutException()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并输出,后续一起解析
|
|
||||||
buildString {
|
buildString {
|
||||||
appendLine(tpsOutput.trim())
|
appendLine(tpsOutput.trim())
|
||||||
appendLine("--------")
|
appendLine("--------")
|
||||||
appendLine(listOutput.trim())
|
appendLine(listOutput.trim())
|
||||||
}
|
}
|
||||||
}.onFailure { ex ->
|
}.onFailure { ex ->
|
||||||
lastSuccessTime = now // ✅ 成功/失败都要刷新冷却开始时间
|
LoggerUtil.logger.error("[$name] RCON 查询失败", ex)
|
||||||
|
|
||||||
if (ex is TimeoutException) {
|
if (ex is TimeoutException) {
|
||||||
LoggerUtil.logger.warn("[$name] RCON 连接超时: ${ex.message}")
|
sendFailedMessage(napCatClient, msg.realId, msg.time, "⏳ RCON 连接超时")
|
||||||
sendFailedMessage(napCatClient, msg.realId, msg.time)
|
// ✅ 更新触发状态 & 持久化
|
||||||
} else {
|
lastTriggerState.lastTriggeredRealId = msg.realId
|
||||||
LoggerUtil.logger.error("[$name] RCON 命令执行失败", ex)
|
lastTriggerState.lastTriggerTime = msg.time
|
||||||
sendFailedMessage(
|
saveState(lastTriggerState)
|
||||||
napCatClient,
|
return
|
||||||
msg.realId,
|
|
||||||
msg.time,
|
|
||||||
"系统内部错误请联系管理员:${ex.message}"
|
|
||||||
)
|
|
||||||
throw ex
|
|
||||||
}
|
}
|
||||||
|
throw ex
|
||||||
}.onSuccess { output ->
|
}.onSuccess { output ->
|
||||||
lastSuccessTime = now
|
|
||||||
LoggerUtil.logger.info("[$name] RCON 命令执行成功,输出长度: ${output.length}")
|
|
||||||
LoggerUtil.logger.debug("[$name] RCON 输出内容: $output")
|
|
||||||
|
|
||||||
val tpsInfo = parseTPS(output)
|
val tpsInfo = parseTPS(output)
|
||||||
val playerListInfo = parsePlayerList(output)
|
val playerListInfo = parsePlayerList(output)
|
||||||
|
|
||||||
LoggerUtil.logger.info(
|
|
||||||
"[$name] 解析成功: TPS=${tpsInfo.overall.meanTPS}, 在线 ${playerListInfo.onlineCount} 人"
|
|
||||||
)
|
|
||||||
|
|
||||||
sendForwardMessage(napCatClient, tpsInfo, playerListInfo, msg.realId, msg.time)
|
sendForwardMessage(napCatClient, tpsInfo, playerListInfo, msg.realId, msg.time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,11 +174,8 @@ class RconPlayerListModule(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, time: Long) {
|
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, remaining: Long) {
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val remaining = ((cooldownMillis - (now - lastSuccessTime)) / 1000).coerceAtLeast(1) // 至少显示 1 秒
|
|
||||||
val msg = "⏳ 查询过于频繁,请稍后再试(剩余 $remaining 秒)"
|
val msg = "⏳ 查询过于频繁,请稍后再试(剩余 $remaining 秒)"
|
||||||
|
|
||||||
LoggerUtil.logger.info("[$name] 发送冷却提示: $msg")
|
LoggerUtil.logger.info("[$name] 发送冷却提示: $msg")
|
||||||
|
|
||||||
val request = SendGroupMsgRequest(
|
val request = SendGroupMsgRequest(
|
||||||
|
|
@ -187,11 +183,6 @@ class RconPlayerListModule(
|
||||||
ID.long(groupMessagePollingModule.targetGroupId)
|
ID.long(groupMessagePollingModule.targetGroupId)
|
||||||
)
|
)
|
||||||
client.sendUnit(request)
|
client.sendUnit(request)
|
||||||
|
|
||||||
// 更新触发状态,但不更新 lastSuccessTime(避免延长冷却)
|
|
||||||
lastTriggerState.lastTriggeredRealId = realId
|
|
||||||
lastTriggerState.lastTriggerTime = time
|
|
||||||
saveState(lastTriggerState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val failedMessages = listOf(
|
private val failedMessages = listOf(
|
||||||
|
|
@ -479,13 +470,30 @@ class RconPlayerListModule(
|
||||||
// ---------------- 持久化部分 ----------------
|
// ---------------- 持久化部分 ----------------
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LastTriggerState(var lastTriggeredRealId: Long, var lastTriggerTime: Long)
|
data class LastTriggerState(
|
||||||
|
var lastTriggeredRealId: Long = -1, // 上次允许处理消息ID
|
||||||
|
var lastTriggerTime: Long = 0, // 上次允许处理时间(毫秒或秒都可以,根据你的逻辑)
|
||||||
|
var lastCooldownRealId: Long = -1 // 上次冷却期间被拒绝的消息ID
|
||||||
|
) {
|
||||||
|
/** ✅ 冷却结束,更新触发状态 */
|
||||||
|
fun updateTrigger(realId: Long, time: Long) {
|
||||||
|
lastTriggeredRealId = realId
|
||||||
|
lastTriggerTime = time
|
||||||
|
// 保留 lastCooldownRealId 不变
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ⚠️ 冷却中,更新冷却消息ID */
|
||||||
|
fun updateCooldownRealId(realId: Long) {
|
||||||
|
lastCooldownRealId = realId
|
||||||
|
// 保留 lastTriggeredRealId 和 lastTriggerTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun saveState(state: LastTriggerState) {
|
override fun saveState(state: LastTriggerState) {
|
||||||
try {
|
try {
|
||||||
// 先备份现有主文件
|
// 先备份现有主文件
|
||||||
if (stateFile.exists()) {
|
if (stateFile.exists()) {
|
||||||
stateFile.copyTo(File(stateFile.parent, stateFile.name + ".bak"), overwrite = true)
|
stateFile.copyTo(stateBackupFile, overwrite = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入主文件
|
// 写入主文件
|
||||||
|
|
@ -500,7 +508,7 @@ class RconPlayerListModule(
|
||||||
return try {
|
return try {
|
||||||
val fileToRead = when {
|
val fileToRead = when {
|
||||||
stateFile.exists() -> stateFile
|
stateFile.exists() -> stateFile
|
||||||
File(stateFile.parent, stateFile.name + ".bak").exists() -> File(stateFile.parent, stateFile.name + ".bak")
|
stateBackupFile.exists() -> stateBackupFile
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -517,5 +525,36 @@ class RconPlayerListModule(
|
||||||
LastTriggerState(-1L, 0L)
|
LastTriggerState(-1L, 0L)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 返回模块基本信息
|
||||||
|
override fun info(): String = buildString {
|
||||||
|
appendLine("模块名称: $name")
|
||||||
|
appendLine("模块类型: RconPlayerListModule")
|
||||||
|
appendLine("目标群组: ${groupMessagePollingModule.targetGroupId}")
|
||||||
|
appendLine("机器人昵称: $selfNickName (ID: $selfId)")
|
||||||
|
appendLine("冷却时间: ${cooldownMillis / 1000} 秒")
|
||||||
|
appendLine("RCON 命令路径: $rconPath")
|
||||||
|
appendLine("RCON 配置文件路径: $rconConfigPath")
|
||||||
|
appendLine("RCON 超时时间: $rconTimeOut ms")
|
||||||
|
appendLine("关键词触发: ${keywords.joinToString(", ")}")
|
||||||
|
appendLine("状态文件路径: ${stateFile.absolutePath}")
|
||||||
|
appendLine("状态备份文件路径: ${stateBackupFile.absolutePath}")
|
||||||
|
appendLine("上次触发消息ID: ${lastTriggerState.lastTriggeredRealId}")
|
||||||
|
appendLine("上次触发时间: ${lastTriggerState.lastTriggerTime}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回模块使用帮助
|
||||||
|
override fun help(): String = buildString {
|
||||||
|
appendLine("使用帮助 - RconPlayerListModule")
|
||||||
|
appendLine("功能: 查询服务器 TPS 和在线玩家列表,通过关键词触发或冷却机制限制频率")
|
||||||
|
appendLine("触发关键词: ${keywords.joinToString(", ")}")
|
||||||
|
appendLine("示例:")
|
||||||
|
keywords.forEach { keyword ->
|
||||||
|
appendLine(" - 在群里发送 \"$keyword\" 将触发 RCON 查询")
|
||||||
|
}
|
||||||
|
appendLine("注意事项:")
|
||||||
|
appendLine(" - 查询冷却时间为 ${cooldownMillis / 1000} 秒")
|
||||||
|
appendLine(" - RCON 查询可能受服务器响应时间影响")
|
||||||
|
appendLine(" - 查询结果会以转发消息形式发送到群组")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
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(Modules.STATE, 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,342 @@
|
||||||
|
package top.r3944realms.ltdmanager.module.common
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高级命令解析器
|
||||||
|
* 支持自定义参数语法和参数验证
|
||||||
|
*/
|
||||||
|
class AdvancedCommandParser {
|
||||||
|
private val commands = mutableListOf<CommandDefinition>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 命令定义类
|
||||||
|
*/
|
||||||
|
data class CommandDefinition(
|
||||||
|
val name: String,
|
||||||
|
val aliases: List<String> = emptyList(),
|
||||||
|
val syntax: String = "",
|
||||||
|
val description: String = "",
|
||||||
|
val parameterPattern: Regex = DEFAULT_PARAMETER_PATTERN
|
||||||
|
) {
|
||||||
|
val allCommandForms: List<String> get() = listOf(name) + aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析结果
|
||||||
|
*/
|
||||||
|
data class ParseResult(
|
||||||
|
val command: String,
|
||||||
|
val arguments: Map<String, String> = emptyMap(),
|
||||||
|
val rawArguments: List<String> = 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+)>""") // <param>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可选参数
|
||||||
|
*/
|
||||||
|
val SQUARE_BRACKETS = Regex("""\[(\w+)]""") // [param]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义参数类型
|
||||||
|
*/
|
||||||
|
val CURLY_BRACES = Regex("""\{(\w+)}""") // {param}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册命令
|
||||||
|
*/
|
||||||
|
fun registerCommand(
|
||||||
|
name: String,
|
||||||
|
aliases: List<String> = 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<String> {
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
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<String>): Map<String, String> {
|
||||||
|
val parameters = extractParameterNames(commandDef.syntax, commandDef.parameterPattern)
|
||||||
|
val result = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
if (parameters.isEmpty()) {
|
||||||
|
args.forEachIndexed { index, value -> result["arg${index + 1}"] = value }
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
val positionals = mutableListOf<String>()
|
||||||
|
val namedParams = mutableMapOf<String, String>()
|
||||||
|
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<String> {
|
||||||
|
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<CommandDefinition> = 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package top.r3944realms.ltdmanager.module.common
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 命令解析器
|
||||||
|
* 严格模式:只支持命令后带空格的情况,避免误读
|
||||||
|
*/
|
||||||
|
class CommandParser(private val commands: List<String>) {
|
||||||
|
/**
|
||||||
|
* 获取指令
|
||||||
|
*/
|
||||||
|
fun getCommands(): List<String> = commands
|
||||||
|
/**
|
||||||
|
* 解析命令
|
||||||
|
* @param text 输入的文本
|
||||||
|
* @return Pair<命令, 参数> 或 null(如果不是有效命令)
|
||||||
|
*/
|
||||||
|
fun parseCommand(text: String): Pair<String, String>? {
|
||||||
|
val trimmedText = text.trim()
|
||||||
|
|
||||||
|
// 查找匹配的命令(必须后面跟着空格或字符串结束)
|
||||||
|
val matchedCommand = commands.firstOrNull { command ->
|
||||||
|
trimmedText.startsWith("$command ") || trimmedText == command
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
// 获取参数部分
|
||||||
|
val argument = if (trimmedText.length > matchedCommand.length) {
|
||||||
|
trimmedText.substring(matchedCommand.length).trim()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(matchedCommand, argument)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文本是否包含有效命令
|
||||||
|
*/
|
||||||
|
fun containsCommand(text: String): Boolean {
|
||||||
|
return parseCommand(text.trim()) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取命令部分(不包含参数)
|
||||||
|
*/
|
||||||
|
fun getCommandOnly(text: String): String? {
|
||||||
|
return parseCommand(text.trim())?.first
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取参数部分(不包含命令)
|
||||||
|
*/
|
||||||
|
fun getArgumentOnly(text: String): String {
|
||||||
|
return parseCommand(text.trim())?.second ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package top.r3944realms.ltdmanager.module.common.cooldown
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.GlobalManager.napCatClient
|
||||||
|
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||||
|
import top.r3944realms.ltdmanager.napcat.request.group.SetGroupBanRequest
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理冷却
|
||||||
|
* @param S 状态类型
|
||||||
|
*/
|
||||||
|
class CooldownManager<S>(
|
||||||
|
private val cooldownMillis: Long,
|
||||||
|
private val scope: CooldownScope,
|
||||||
|
private val stateProvider: CooldownStateProvider<S>,
|
||||||
|
private val getLastTrigger: (S, Long?) -> Pair<Long, Long>, // (lastTimeSec, lastRealId)
|
||||||
|
private val updateTrigger: (S, Long?, Long, Long) -> S, // 更新 lastTimeSec, lastRealId
|
||||||
|
private val updateCooldownRealId: (S, Long?, Long) -> S,
|
||||||
|
private val groupId: Long, // 所属群组 ID,用于禁言
|
||||||
|
private val banSeconds: Int = 60 // 重复发送禁言时间
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var state: S = stateProvider.load()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查冷却
|
||||||
|
* @param userId PerUser 模式必须
|
||||||
|
* @param realId 消息 realId
|
||||||
|
*/
|
||||||
|
suspend fun checkAndHandle(userId: Long?, realId: Long): CooldownResult {
|
||||||
|
require(scope != CooldownScope.PerUser || userId != null) { "userId required for per-user cooldown" }
|
||||||
|
|
||||||
|
val (lastTimeSec, lastCooldownRealId) = getLastTrigger(state, userId)
|
||||||
|
val cooldownSec = cooldownMillis / 1000
|
||||||
|
|
||||||
|
val now = System.currentTimeMillis() / 1000
|
||||||
|
val elapsed = if (lastTimeSec == -1L) Long.MAX_VALUE else now - lastTimeSec
|
||||||
|
|
||||||
|
return if (elapsed >= cooldownSec) {
|
||||||
|
// ✅ 冷却结束,允许处理消息
|
||||||
|
state = updateTrigger(state, userId, realId, now)
|
||||||
|
stateProvider.save(state)
|
||||||
|
CooldownResult(allowed = true, remaining = 0, notify = false)
|
||||||
|
} else {
|
||||||
|
val remaining = max(0, cooldownSec - elapsed)
|
||||||
|
val notify = realId != lastCooldownRealId // 第一次触发冷却提示
|
||||||
|
|
||||||
|
if (notify) {
|
||||||
|
// 第一次冷却提示,记录消息 ID
|
||||||
|
state = updateCooldownRealId(state, userId, realId)
|
||||||
|
stateProvider.save(state)
|
||||||
|
} else {
|
||||||
|
// // ⚠️ 重复发送冷却消息 -> 禁言
|
||||||
|
// if (userId != null) {
|
||||||
|
// banUser(userId, groupId, banSeconds)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
CooldownResult(allowed = false, remaining = remaining, notify = notify)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun banUser(userId: Long, groupId: Long, seconds: Int) {
|
||||||
|
val request = SetGroupBanRequest(
|
||||||
|
duration = seconds.toDouble(),
|
||||||
|
groupId = ID.long(groupId),
|
||||||
|
userId = ID.long(userId)
|
||||||
|
)
|
||||||
|
napCatClient.sendUnit(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package top.r3944realms.ltdmanager.module.common.cooldown
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冷却结果
|
||||||
|
* @param allowed 是否允许触发
|
||||||
|
* @param remaining 剩余秒数(如果未允许触发)
|
||||||
|
* @param notify 是否可以发送冷却提示
|
||||||
|
*/
|
||||||
|
data class CooldownResult(
|
||||||
|
val allowed: Boolean,
|
||||||
|
val remaining: Long = 0L,
|
||||||
|
val notify: Boolean = true
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package top.r3944realms.ltdmanager.module.common.cooldown
|
||||||
|
|
||||||
|
sealed class CooldownScope {
|
||||||
|
data object Global : CooldownScope()
|
||||||
|
data object PerUser : CooldownScope()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package top.r3944realms.ltdmanager.module.common.cooldown
|
||||||
|
|
||||||
|
interface CooldownStateProvider<S> {
|
||||||
|
fun load(): S
|
||||||
|
fun save(state: S)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package top.r3944realms.ltdmanager.module.common.filter
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
|
||||||
|
|
||||||
|
interface MessageFilter {
|
||||||
|
suspend fun test(msg: MsgHistorySpecificMsg): Boolean
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package top.r3944realms.ltdmanager.module.common.filter
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
|
||||||
|
|
||||||
|
class TriggerMessageFilter(private val filters: List<MessageFilter>) {
|
||||||
|
suspend fun filter(messages: List<MsgHistorySpecificMsg>)
|
||||||
|
: List<MsgHistorySpecificMsg> {
|
||||||
|
|
||||||
|
val result = mutableListOf<MsgHistorySpecificMsg>()
|
||||||
|
for (msg in messages) {
|
||||||
|
if (filters.all { it.test(msg) }) {
|
||||||
|
result.add(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
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.data.msghistory.MsgHistorySpecificMsg
|
||||||
|
|
||||||
|
/** 命令解析器匹配 */
|
||||||
|
class CommandFilter(private val parser: CommandParser) : MessageFilter {
|
||||||
|
override suspend fun test(msg: MsgHistorySpecificMsg): Boolean {
|
||||||
|
return msg.message.any { seg ->
|
||||||
|
seg.type == MessageType.Text && seg.data.text?.let { parser.containsCommand(it) } == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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.data.msghistory.MsgHistorySpecificMsg
|
||||||
|
|
||||||
|
class CooldownFilter(
|
||||||
|
private val cooldownManager: CooldownManager<*>,
|
||||||
|
private val sendCooldown: suspend (MsgHistorySpecificMsg, Long) -> Unit
|
||||||
|
) : MessageFilter {
|
||||||
|
|
||||||
|
override suspend fun test(msg: MsgHistorySpecificMsg): Boolean {
|
||||||
|
val result = cooldownManager.checkAndHandle(msg.userId, msg.realId)
|
||||||
|
if (!result.allowed && result.notify) {
|
||||||
|
sendCooldown(msg, result.remaining)
|
||||||
|
}
|
||||||
|
return result.allowed
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user