diff --git a/.gitignore b/.gitignore index b0acb4b8..c15beb94 100644 --- a/.gitignore +++ b/.gitignore @@ -120,4 +120,11 @@ run/ logs kbc.yml plugins -permissions.json \ No newline at end of file +permissions.json + +# AI Tools +.claude/ +.spec-workflow/ +.fastRequest/ +AGENTS.md +CLAUDE.md \ No newline at end of file diff --git a/README.md b/README.md index fbb80e3d..953acfac 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ java -jar kookbc-.jar 配置完成后,再次使用之前的命令行启动 KookBC ,当如下语句出现时,您的 KookBC 就已经准备就绪,可以使用。 ```text -[XX:XX:XX] [Main Thread/INFO] Done! Type "help" for help. +[XX:XX:XX] [Main Thread/INFO] 完成!输入 "help" 获取帮助。 ``` 其中,`X` 为任意可能的值,您可以忽视。 diff --git a/build.gradle.kts b/build.gradle.kts index 2b7525da..f293a02c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ import java.util.* plugins { `java-library` `maven-publish` - id("com.gorylenko.gradle-git-properties") version "2.4.2" + id("com.gorylenko.gradle-git-properties") version "2.5.3" id("com.gradleup.shadow") version "9.0.0-beta4" id("publish-conventions") } @@ -25,7 +25,7 @@ dependencies { shadow(dep) api(dep) } - shadowApi(libs.com.github.snwcreations.jkook) + shadowApi(libs.io.github.snwcreations.jkook) shadowApi(libs.com.github.snwcreations.terminalconsoleappender) shadowApi(libs.uk.org.lidalia.sysout.over.slf4j) shadowApi(libs.org.apache.logging.log4j.log4j.core) @@ -38,6 +38,10 @@ dependencies { shadowApi(libs.net.kyori.event.method) shadowApi(libs.net.freeutils.jlhttp) shadowApi(libs.com.google.code.gson.gson) + shadow("com.fasterxml.jackson.core:jackson-core:2.17.2"); api("com.fasterxml.jackson.core:jackson-core:2.17.2") + shadow("com.fasterxml.jackson.core:jackson-databind:2.17.2"); api("com.fasterxml.jackson.core:jackson-databind:2.17.2") + shadow("com.fasterxml.jackson.core:jackson-annotations:2.17.2"); api("com.fasterxml.jackson.core:jackson-annotations:2.17.2") + shadow("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2"); api("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2") shadowApi(libs.com.github.ben.manes.caffeine.caffeine) shadowApi(libs.net.fabricmc.sponge.mixin) shadowApi(libs.dev.rollczi.litecommands.framework) @@ -45,10 +49,10 @@ dependencies { compileOnly(libs.org.jetbrains.annotations) } group = "io.github.snwcreations" -version = "0.32.2" +version = "0.33.0" description = "KookBC" -java.sourceCompatibility = JavaVersion.VERSION_1_8 -java.targetCompatibility = JavaVersion.VERSION_1_8 +java.sourceCompatibility = JavaVersion.VERSION_21 +java.targetCompatibility = JavaVersion.VERSION_21 tasks.compileJava { options.encoding = "UTF-8" @@ -71,6 +75,7 @@ gitProperties { val skipShade = properties["skipShade"] == "true" tasks.shadowJar { enabled = !skipShade + duplicatesStrategy = DuplicatesStrategy.EXCLUDE archiveClassifier = "" transform(com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer()) exclude( @@ -81,9 +86,13 @@ tasks.shadowJar { ) } +tasks.compileJava { + options.encoding = "UTF-8" +} + tasks.processResources { filesMatching("*.json") { - expand(properties + mapOf("jkookVersion" to libs.versions.com.github.snwcreations.jkook.get())) + expand(properties + mapOf("jkookVersion" to libs.versions.io.github.snwcreations.jkook.get())) } } @@ -99,7 +108,7 @@ tasks.jar { mapOf( "Build-Time" to Date(), "Specification-Title" to "JKook", - "Specification-Version" to libs.com.github.snwcreations.jkook.get().version, + "Specification-Version" to libs.io.github.snwcreations.jkook.get().version, "Specification-Vendor" to "SNWCreations", "Implementation-Title" to "KookBC", "Implementation-Version" to version.toString(), diff --git a/buildSrc/src/main/kotlin/publish-conventions.gradle.kts b/buildSrc/src/main/kotlin/publish-conventions.gradle.kts index 057a006f..781cb832 100644 --- a/buildSrc/src/main/kotlin/publish-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/publish-conventions.gradle.kts @@ -16,8 +16,8 @@ indra { gpl3OnlyLicense() javaVersions { - target(8) - minimumToolchain(17) + target(21) + minimumToolchain(21) } configurePublications { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ac9bf4e..7728472f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] com-github-ben-manes-caffeine-caffeine = "2.9.3" -com-github-snwcreations-jkook = "0.54.1" -com-github-snwcreations-terminalconsoleappender = "1.3.5" com-google-code-gson-gson = "2.10.1" +io-github-snwcreations-jkook = "0.54.2" +com-github-snwcreations-terminalconsoleappender = "1.3.5" com-squareup-okhttp3-okhttp = "4.10.0" dev-rollczi-litecommands-framework = "3.9.5" net-bytebuddy-byte-buddy = "1.12.10" @@ -18,12 +18,19 @@ org-fusesource-jansi-jansi = "2.4.0" org-jetbrains-annotations = "23.1.0" org-jline-jline-terminal-jansi = "3.21.0" uk-org-lidalia-sysout-over-slf4j = "1.0.2" +# Test Dependencies +junit = "5.9.3" +mockito = "4.11.0" +wiremock = "2.27.2" +testcontainers = "1.17.6" +assertj = "3.24.2" +mockwebserver = "4.10.0" [libraries] com-github-ben-manes-caffeine-caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "com-github-ben-manes-caffeine-caffeine" } -com-github-snwcreations-jkook = { module = "com.github.SNWCreations:JKook", version.ref = "com-github-snwcreations-jkook" } -com-github-snwcreations-terminalconsoleappender = { module = "com.github.SNWCreations:TerminalConsoleAppender", version.ref = "com-github-snwcreations-terminalconsoleappender" } com-google-code-gson-gson = { module = "com.google.code.gson:gson", version.ref = "com-google-code-gson-gson" } +io-github-snwcreations-jkook = { module = "io.github.snwcreations:jkook", version.ref = "io-github-snwcreations-jkook" } +com-github-snwcreations-terminalconsoleappender = { module = "com.github.SNWCreations:TerminalConsoleAppender", version.ref = "com-github-snwcreations-terminalconsoleappender" } com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3-okhttp" } dev-rollczi-litecommands-framework = { module = "dev.rollczi:litecommands-framework", version.ref = "dev-rollczi-litecommands-framework" } net-bytebuddy-byte-buddy = { module = "net.bytebuddy:byte-buddy", version.ref = "net-bytebuddy-byte-buddy" } @@ -39,3 +46,16 @@ org-fusesource-jansi-jansi = { module = "org.fusesource.jansi:jansi", version.re org-jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "org-jetbrains-annotations" } org-jline-jline-terminal-jansi = { module = "org.jline:jline-terminal-jansi", version.ref = "org-jline-jline-terminal-jansi" } uk-org-lidalia-sysout-over-slf4j = { module = "uk.org.lidalia:sysout-over-slf4j", version.ref = "uk-org-lidalia-sysout-over-slf4j" } +# Test Libraries +junit_jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } +junit_jupiter_engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } +junit_jupiter_params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } +mockito_core = { module = "org.mockito:mockito-core", version.ref = "mockito" } +mockito_junit_jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } +mockito_inline = { module = "org.mockito:mockito-inline", version.ref = "mockito" } +wiremock_jre8 = { module = "com.github.tomakehurst:wiremock-jre8", version.ref = "wiremock" } +testcontainers_junit_jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } +testcontainers_core = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" } +assertj_core = { module = "org.assertj:assertj-core", version.ref = "assertj" } +mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" } + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0e..d4081da4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/snw/kookbc/CLIOptions.java b/src/main/java/snw/kookbc/CLIOptions.java index b9344981..91d904ae 100644 --- a/src/main/java/snw/kookbc/CLIOptions.java +++ b/src/main/java/snw/kookbc/CLIOptions.java @@ -11,7 +11,7 @@ public final class CLIOptions { static { NO_BUCKET = Boolean.getBoolean("kookbc.nobucket"); if (NO_BUCKET) { - logger.warn("You've used kookbc.nobucket option, we won't check if you're going to be out of rate limit!"); + logger.warn("您已启用 kookbc.nobucket 选项,我们将不会检查是否会超出速率限制!"); } } diff --git a/src/main/java/snw/kookbc/LaunchMain.java b/src/main/java/snw/kookbc/LaunchMain.java index f0dbd3ec..780acc17 100644 --- a/src/main/java/snw/kookbc/LaunchMain.java +++ b/src/main/java/snw/kookbc/LaunchMain.java @@ -1,8 +1,11 @@ /* * License: https://github.com/Mojang/LegacyLauncher */ + package snw.kookbc; +import static snw.kookbc.util.VirtualThreadUtil.startVirtualThread; + import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; @@ -40,15 +43,15 @@ public static void main(String[] args) { ClassLoader appClassLoader = LaunchMain.class.getClassLoader(); Class mixinClass = Class.forName("org.spongepowered.asm.util.JavaVersion"); if (mixinClass.getClassLoader() != appClassLoader) { - System.out.println("[KookBC/WARN] Mixin support is already enabled!"); - System.out.println("[KookBC/WARN] If you're sure you don't need Mixin support, visit the following link:"); + System.out.println("[KookBC/WARN] Mixin 支持已启用!"); + System.out.println("[KookBC/WARN] 如果您确定不需要 Mixin 支持,请访问以下链接:"); System.out.println("[KookBC/WARN] https://github.com/SNWCreations/KookBC/blob/main/docs/KookBC_CommandLine.md#%E5%90%AF%E5%8A%A8%E5%85%A5%E5%8F%A3"); // Never part, never give up! classLoader = new LaunchClassLoader(getUrls(appClassLoader).toArray(new URL[0])); AccessClassLoader loader = AccessClassLoader.of(classLoader); MixinPluginManager.instance().loadFolder(loader, new File("plugins")); String[] finalArgs = args; - Thread thread = new Thread(() -> { + Thread thread = startVirtualThread(() -> { try { Class mainClass = Class.forName(LaunchMainTweaker.CLASS_NAME); MethodHandles.lookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class)) @@ -56,9 +59,15 @@ public static void main(String[] args) { } catch (Throwable e) { throw new RuntimeException(e); } - }); + }, "Mixin-Launch-Thread"); thread.setContextClassLoader(PluginClassLoaderDelegate.INSTANCE); - thread.start(); + + // 等待虚拟线程完成 + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } return; } } catch (ClassNotFoundException ignored) { @@ -76,14 +85,6 @@ public static void main(String[] args) { } args = argsList.toArray(new String[0]); Thread.currentThread().setName(MAIN_THREAD_NAME); - // Actually here should not be warning, but the logging level of the LaunchWrapper is WARN. - // Also, I think this message can let the user know they are running KookBC under Launch mode. - LogWrapper.LOGGER.warn("Launching KookBC with Mixin support"); - LogWrapper.LOGGER.warn("The author of Mixin support: huanmeng_qwq@Github"); // thank you! --- SNWCreations - LogWrapper.LOGGER.warn("Tips: You can safely ignore this."); - LogWrapper.LOGGER.warn("But if you're really sure you don't need Mixin support, visit the following link:"); - LogWrapper.LOGGER.warn("https://github.com/SNWCreations/KookBC/blob/main/docs/KookBC_CommandLine.md"); - LogWrapper.LOGGER.warn("The documentation will tell you how can you launch KookBC without Mixin support."); launch.launch(args); } @@ -138,7 +139,7 @@ private void launch(String[] args) { final OptionParser parser = new OptionParser(); parser.allowsUnrecognizedOptions(); - final OptionSpec tweakClassOption = parser.accepts("tweakClass", "Tweak class(es) to load").withRequiredArg().defaultsTo(MIXIN_TWEAK, DEFAULT_TWEAK); + final OptionSpec tweakClassOption = parser.accepts("tweakClass", "要加载的调整类").withRequiredArg().defaultsTo(MIXIN_TWEAK, DEFAULT_TWEAK); final OptionSpec nonOption = parser.nonOptions(); final OptionSet options = parser.parse(args); @@ -175,14 +176,14 @@ private void launch(String[] args) { final String tweakName = it.next(); // Safety check - don't reprocess something we've already visited if (allTweakerNames.contains(tweakName)) { - LogWrapper.LOGGER.warn("Tweak class name {} has already been visited -- skipping", tweakName); + LogWrapper.LOGGER.warn("调整类名 {} 已经被访问过 -- 跳过", tweakName); // remove the tweaker from the stack otherwise it will create an infinite loop it.remove(); continue; } else { allTweakerNames.add(tweakName); } - LogWrapper.LOGGER.info("Loading tweak class name {}", tweakName); + LogWrapper.LOGGER.info("正在加载调整类 {}", tweakName); // Ensure we allow the tweak class to load with the parent classloader classLoader.addClassLoaderExclusion(tweakName.substring(0, tweakName.lastIndexOf('.'))); @@ -193,7 +194,7 @@ private void launch(String[] args) { it.remove(); // If we haven't visited a tweaker yet, the first will become the 'primary' tweaker if (primaryTweaker == null) { - LogWrapper.LOGGER.info("Using primary tweak class name {}", tweakName); + LogWrapper.LOGGER.info("使用主要调整类 {}", tweakName); primaryTweaker = tweaker; } } @@ -201,7 +202,7 @@ private void launch(String[] args) { // Now, iterate all the tweakers we just instantiated for (final Iterator it = tweakers.iterator(); it.hasNext(); ) { final ITweaker tweaker = it.next(); - LogWrapper.LOGGER.info("Calling tweak class {}", tweaker.getClass().getName()); + LogWrapper.LOGGER.info("调用调整类 {}", tweaker.getClass().getName()); tweaker.acceptOptions(options.valuesOf(nonOption)); tweaker.injectIntoClassLoader(classLoader); allTweakers.add(tweaker); @@ -221,7 +222,7 @@ private void launch(String[] args) { } if (primaryTweaker == null) { - throw new NullPointerException("Tweaker not found"); + throw new NullPointerException("未找到调整器"); } // Finally, we turn to the primary tweaker, and let it tell us where to go to launch @@ -231,19 +232,25 @@ private void launch(String[] args) { if (launchTarget != null && !launchTarget.isEmpty()) { final Class clazz = Class.forName(launchTarget, false, classLoader); MethodHandle mainMethodHandle = MethodHandles.lookup().findStatic(clazz, "main", MethodType.methodType(void.class, String[].class)); - Thread main = new Thread(() -> { + Thread main = startVirtualThread(() -> { try { mainMethodHandle.invoke((Object) argumentList.toArray(new String[0])); } catch (Throwable e) { e.printStackTrace(); } - }, clazz.getSimpleName()); + }, clazz.getSimpleName() + "-Main-Thread"); main.setContextClassLoader(PluginClassLoaderDelegate.INSTANCE); - main.start(); + + // 等待虚拟线程完成 + try { + main.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } } } catch (Exception e) { - LogWrapper.LOGGER.error("Unable to launch", e); + LogWrapper.LOGGER.error("无法启动", e); System.exit(1); } } diff --git a/src/main/java/snw/kookbc/Main.java b/src/main/java/snw/kookbc/Main.java index f6e014a0..ab580e85 100644 --- a/src/main/java/snw/kookbc/Main.java +++ b/src/main/java/snw/kookbc/Main.java @@ -19,6 +19,7 @@ package snw.kookbc; import static snw.kookbc.util.Util.isStartByLaunch; +import static snw.kookbc.util.VirtualThreadUtil.startVirtualThread; import java.io.File; import java.io.FileNotFoundException; @@ -54,7 +55,7 @@ public static void main(String[] args) { try { System.exit(main0(args)); } catch (Throwable e) { - logger.error("Unexpected situation happened during the execution of main method!", e); + logger.error("主方法执行过程中发生意外情况!", e); System.exit(1); } } @@ -79,14 +80,14 @@ private int start(String[] args) { // --help -- Get help and exit OptionParser parser = new OptionParser(); - OptionSpec tokenOption = parser.accepts("token", "The token that will be used. (Unsafe, write token to kbc.yml instead.)").withOptionalArg(); - OptionSpec helpOption = parser.accepts("help", "Get help and exit."); + OptionSpec tokenOption = parser.accepts("token", "将要使用的 token。(不安全,建议将 token 写入 kbc.yml)").withOptionalArg(); + OptionSpec helpOption = parser.accepts("help", "获取帮助并退出"); OptionSet options; try { options = parser.parse(args); } catch (OptionException e) { - logger.error("Unable to parse argument. Is your argument correct?", e); + logger.error("无法解析命令行参数。参数格式是否正确?", e); return 1; } @@ -98,7 +99,7 @@ private int start(String[] args) { try { parser.printHelpOn(System.out); } catch (IOException e) { - logger.error("Unable to print help."); + logger.error("无法打印帮助信息。"); return 1; } return 0; @@ -119,29 +120,29 @@ private int start(String[] args) { config.load(kbcLocal); } catch (FileNotFoundException ignored) { } catch (IOException | InvalidConfigurationException e) { - logger.error("Cannot load kbc.yml", e); + logger.error("无法加载 kbc.yml 配置文件", e); } String configToken = config.getString("token"); if (configToken != null && !configToken.isEmpty()) { - logger.debug("Got valid token in kbc.yml."); + logger.debug("在 kbc.yml 中找到有效的 token。"); if (token == null || token.isEmpty()) { - logger.debug("The value of token from command line is invalid. We will use the value from kbc.yml configuration."); + logger.debug("命令行中的 token 值无效。将使用 kbc.yml 配置文件中的值。"); token = configToken; } else { - logger.debug("The value of token from command line is OK, so we won't use the value from kbc.yml configuration."); + logger.debug("命令行中的 token 值有效,将不使用 kbc.yml 配置文件中的值。"); } } else { - logger.warn("Invalid token value in kbc.yml."); + logger.warn("在 kbc.yml 中找到无效的 token 值。"); } if (token == null) { - logger.error("No token provided. Program cannot continue."); + logger.error("未提供 token。程序无法继续运行。"); return 1; } if (!config.getBoolean("allow-help-ad", true)) { - logger.warn("Detected allow-help-ad is false! :("); // why don't you support us? + logger.warn("检测到 allow-help-ad 被设置为 false! :("); // why don't you support us? } return startClient(token, config, pluginsFolder); } @@ -149,20 +150,20 @@ private int start(String[] args) { protected int startClient(String token, YamlConfiguration config, File pluginsFolder) { if (!isStartByLaunch()) { logger.warn("***************************************"); - logger.warn("Launching KookBC WITHOUT Mixin support!"); - logger.warn("All Mixins in plugins will be ignored."); - logger.warn("Tips: You can safely ignore this if you"); - logger.warn(" don't have Mixin plugins."); + logger.warn("正在启动 KookBC,但未提供 Mixin 支持!"); + logger.warn("插件中的所有 Mixin 将被忽略。"); + logger.warn("提示:如果您没有 Mixin 插件,"); + logger.warn(" 可以安全地忽略此消息。"); logger.warn("***************************************"); } RuntimeMXBean runtimeMX = ManagementFactory.getRuntimeMXBean(); OperatingSystemMXBean osMX = ManagementFactory.getOperatingSystemMXBean(); if (runtimeMX != null && osMX != null) { - logger.debug("System information is following:"); - logger.debug("Java: {} ({} {} by {})", runtimeMX.getSpecVersion(), runtimeMX.getVmName(), runtimeMX.getVmVersion(), runtimeMX.getVmVendor()); - logger.debug("Host: {} {} (Architecture: {})", osMX.getName(), osMX.getVersion(), osMX.getArch()); + logger.debug("系统信息如下:"); + logger.debug("Java: {} ({} {} 由 {} 提供)", runtimeMX.getSpecVersion(), runtimeMX.getVmName(), runtimeMX.getVmVersion(), runtimeMX.getVmVendor()); + logger.debug("主机: {} {} (架构: {})", osMX.getName(), osMX.getVersion(), osMX.getArch()); } else { - logger.debug("Unable to read system info"); + logger.debug("无法读取系统信息"); } CoreImpl core = new CoreImpl(logger); @@ -171,12 +172,12 @@ protected int startClient(String token, YamlConfiguration config, File pluginsFo null, null, null, null, null, null); // make sure the things can stop correctly (e.g. Scheduler), but the crash makes no sense. - Runtime.getRuntime().addShutdownHook(new Thread(client::shutdown, "JVM Shutdown Hook Thread")); + Runtime.getRuntime().addShutdownHook(new Thread(client::shutdown, "JVM-Shutdown-Hook-Thread")); try { client.start(); } catch (Exception e) { - logger.error("Failed to start client", e); + logger.error("启动客户端失败", e); client.shutdown(); return 1; } @@ -210,7 +211,7 @@ private static void saveKBCConfig() { } } } catch (IOException e) { - logger.warn("Cannot save kbc.yml because an error occurred", e); + logger.warn("由于发生错误,无法保存 kbc.yml 文件", e); } } } diff --git a/src/main/java/snw/kookbc/SharedConstants.java b/src/main/java/snw/kookbc/SharedConstants.java index 6e5a704d..81fb33f1 100644 --- a/src/main/java/snw/kookbc/SharedConstants.java +++ b/src/main/java/snw/kookbc/SharedConstants.java @@ -18,13 +18,11 @@ package snw.kookbc; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import snw.kookbc.util.JacksonUtil; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; // The shared constants as the symbol for KookBC. // Want to modify them? see kookbc_version_data.json in src/main/resources folder. @@ -47,24 +45,24 @@ public final class SharedConstants { static { // region Initialize data object - JsonObject dataObject; + JsonNode dataObject; try (InputStream inputStream = SharedConstants.class.getClassLoader().getResourceAsStream("kookbc_version_data.json")) { if (inputStream == null) { throw new Error("Cannot find kookbc_version_data.json"); } - dataObject = JsonParser.parseReader(new InputStreamReader(inputStream)).getAsJsonObject(); - } catch (JsonParseException | IOException e) { + dataObject = JacksonUtil.getMapper().readTree(inputStream); + } catch (IOException e) { throw new Error("Cannot initialize KookBC data", e); // should never happen } // endregion try { - SPEC_NAME = dataObject.get("spec_name").getAsString(); - SPEC_VERSION = dataObject.get("spec_version").getAsString(); - IMPL_NAME = dataObject.get("name").getAsString(); - IMPL_VERSION = dataObject.get("version").getAsString(); - REPO_URL = dataObject.get("repo_url").getAsString(); - IS_SNAPSHOT = Boolean.parseBoolean(dataObject.get("is_snapshot").getAsString()); + SPEC_NAME = dataObject.get("spec_name").asText(); + SPEC_VERSION = dataObject.get("spec_version").asText(); + IMPL_NAME = dataObject.get("name").asText(); + IMPL_VERSION = dataObject.get("version").asText(); + REPO_URL = dataObject.get("repo_url").asText(); + IS_SNAPSHOT = Boolean.parseBoolean(dataObject.get("is_snapshot").asText()); } catch (Exception e) { throw new Error("Cannot define KookBC data", e); } diff --git a/src/main/java/snw/kookbc/impl/CoreImpl.java b/src/main/java/snw/kookbc/impl/CoreImpl.java index 23d7e834..20c3dcdb 100644 --- a/src/main/java/snw/kookbc/impl/CoreImpl.java +++ b/src/main/java/snw/kookbc/impl/CoreImpl.java @@ -37,6 +37,8 @@ import snw.kookbc.impl.plugin.SimplePluginManager; import snw.kookbc.impl.scheduler.SchedulerImpl; +import static snw.kookbc.util.VirtualThreadUtil.startVirtualThread; + import java.util.Optional; public class CoreImpl implements Core { @@ -132,7 +134,7 @@ public Unsafe getUnsafe() { public void shutdown() { // If we don't use another thread for calling the real shutdown method, // the client won't be terminated if you called this in a scheduler task. - new Thread(client::shutdown, "Shutdown Thread").start(); + startVirtualThread(client::shutdown, "Shutdown-Thread"); } // Just a friendly way to get the client instance. diff --git a/src/main/java/snw/kookbc/impl/HttpAPIImpl.java b/src/main/java/snw/kookbc/impl/HttpAPIImpl.java index 8b9f8fb8..8109fdff 100644 --- a/src/main/java/snw/kookbc/impl/HttpAPIImpl.java +++ b/src/main/java/snw/kookbc/impl/HttpAPIImpl.java @@ -19,7 +19,8 @@ package snw.kookbc.impl; import static java.util.Collections.unmodifiableCollection; -import static snw.kookbc.util.GsonUtil.get; +import static snw.kookbc.util.JacksonUtil.get; +import static snw.kookbc.util.JacksonUtil.parse; import java.io.File; import java.io.IOException; @@ -30,18 +31,22 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; import okhttp3.MediaType; import okhttp3.MultipartBody; @@ -49,6 +54,7 @@ import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; +import java.net.URL; import snw.jkook.HttpAPI; import snw.jkook.entity.Game; import snw.jkook.entity.Guild; @@ -74,8 +80,10 @@ import snw.kookbc.impl.pageiter.JoinedGuildIterator; import snw.kookbc.impl.pageiter.JoinedVoiceChannelsIterator; import snw.kookbc.util.MapBuilder; +import snw.kookbc.util.VirtualThreadUtil; +import snw.kookbc.interfaces.AsyncHttpAPI; -public class HttpAPIImpl implements HttpAPI { +public class HttpAPIImpl implements HttpAPI, AsyncHttpAPI { private static final MediaType OCTET_STREAM; private static final Collection SUPPORTED_MUSIC_SOFTWARES; // private static final long UPLOAD_FILE_LENGTH_LIMIT = 25; // in MB @@ -91,6 +99,19 @@ public class HttpAPIImpl implements HttpAPI { private final KBCClient client; + // ===== 异步 API 增强 - 智能请求合并器 ===== + + /** + * 智能请求合并器 - 避免重复请求,提升性能 + */ + private final RequestCoalescer requestCoalescer = new RequestCoalescer(); + + /** + * 异步执行器 - 专用于 HTTP API 操作 + */ + private final ExecutorService httpExecutor = VirtualThreadUtil.getHttpExecutor(); + + public HttpAPIImpl(KBCClient client) { this.client = client; } @@ -132,32 +153,14 @@ public Category getCategory(String s) { @Override public String uploadFile(File file) { - RequestBody body = new MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("file", file.getName(), RequestBody.create(file, OCTET_STREAM)) - .build(); - Request request = new Request.Builder() - .url(HttpAPIRoute.ASSET_UPLOAD.toFullURL()) - .post(body) - .addHeader("Authorization", client.getNetworkClient().getTokenWithPrefix()) - .build(); - return JsonParser.parseString(client.getNetworkClient().call(request)).getAsJsonObject().getAsJsonObject("data") - .get("url").getAsString(); + // 内部使用异步实现,但对外保持同步接口 + return uploadFileAsync(file).join(); } @Override public String uploadFile(String filename, byte[] content) { - RequestBody requestBody = new MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("file", filename, RequestBody.create(content, OCTET_STREAM)) - .build(); - Request request = new Request.Builder() - .url(HttpAPIRoute.ASSET_UPLOAD.toFullURL()) - .post(requestBody) - .addHeader("Authorization", client.getNetworkClient().getTokenWithPrefix()) - .build(); - return JsonParser.parseString(client.getNetworkClient().call(request)).getAsJsonObject().getAsJsonObject("data") - .get("url").getAsString(); + // 内部使用异步实现,但对外保持同步接口 + return uploadFileAsync(filename, content).join(); } @Override @@ -188,8 +191,8 @@ public String uploadFile(String fileName, String url) throws IllegalArgumentExce @Override public void removeInvite(String urlCode) { - client.getNetworkClient().post(HttpAPIRoute.INVITE_DELETE.toFullURL(), - Collections.singletonMap("url_code", urlCode)); + // 内部使用异步实现,但对外保持同步接口 + removeInviteAsync(urlCode).join(); } @Override @@ -213,8 +216,8 @@ public Game createGame(String name, @Nullable String icon) { } else { body = Collections.singletonMap("name", name); } - JsonObject object = client.getNetworkClient().post(HttpAPIRoute.GAME_CREATE.toFullURL(), body); - Game game = client.getEntityBuilder().buildGame(object); + JsonNode response = client.getNetworkClient().post(HttpAPIRoute.GAME_CREATE.toFullURL(), body); + Game game = client.getEntityBuilder().buildGame(response); client.getStorage().addGame(game); return game; } @@ -288,18 +291,18 @@ public FriendStateImpl(boolean lazyInit) { this.blocked = new AtomicReference<>(); this.requests = new AtomicReference<>(); if (!lazyInit) { - JsonObject object = client.getNetworkClient().get(HttpAPIRoute.FRIEND_LIST.toFullURL()); - JsonArray request = get(object, "request").getAsJsonArray(); + JsonNode object = client.getNetworkClient().get(HttpAPIRoute.FRIEND_LIST.toFullURL()); + JsonNode request = get(object, "request"); Collection requestCollection; - if (!request.isEmpty()) { + if (request.isArray() && request.size() > 0) { requestCollection = new ArrayList<>(request.size()); convertRawRequest(request, requestCollection); } else { requestCollection = Collections.emptyList(); } - JsonArray blocked = get(object, "blocked").getAsJsonArray(); + JsonNode blocked = get(object, "blocked"); Collection blockedUsers = buildUserListFromFriendStateArray(blocked); - JsonArray friend = get(object, "friend").getAsJsonArray(); + JsonNode friend = get(object, "friend"); Collection friends = buildUserListFromFriendStateArray(friend); this.friends.set(friends); @@ -308,12 +311,12 @@ public FriendStateImpl(boolean lazyInit) { } } - protected void convertRawRequest(JsonArray request, Collection requestCollection) { - for (JsonElement element : request) { - JsonObject obj = element.getAsJsonObject(); - int id = get(obj, "id").getAsInt(); - JsonObject userObj = get(element.getAsJsonObject(), "friend_info").getAsJsonObject(); - User user = client.getStorage().getUser(get(userObj, "id").getAsString(), userObj); + protected void convertRawRequest(JsonNode request, Collection requestCollection) { + for (JsonNode element : request) { + JsonNode obj = element; + int id = get(obj, "id").asInt(); + JsonNode userObj = get(element, "friend_info"); + User user = client.getStorage().getUser(get(userObj, "id").asText(), userObj); FriendRequestImpl requestObj = new FriendRequestImpl(id, user); requestCollection.add(requestObj); } @@ -323,9 +326,8 @@ protected void convertRawRequest(JsonArray request, Collection re public Collection getBlockedUsers() { return blocked.updateAndGet(i -> { if (i == null) { - JsonObject object = client.getNetworkClient() - .get(HttpAPIRoute.FRIEND_LIST.toFullURL() + "?type=block"); - JsonArray friend = get(object, "block").getAsJsonArray(); + JsonNode object = client.getNetworkClient().get(HttpAPIRoute.FRIEND_LIST.toFullURL() + "?type=block"); + JsonNode friend = get(object, "block"); return buildUserListFromFriendStateArray(friend); } return i; @@ -336,9 +338,8 @@ public Collection getBlockedUsers() { public Collection getFriends() { return friends.updateAndGet(i -> { if (i == null) { - JsonObject object = client.getNetworkClient() - .get(HttpAPIRoute.FRIEND_LIST.toFullURL() + "?type=friend"); - JsonArray friend = get(object, "friend").getAsJsonArray(); + JsonNode object = client.getNetworkClient().get(HttpAPIRoute.FRIEND_LIST.toFullURL() + "?type=friend"); + JsonNode friend = get(object, "friend"); return buildUserListFromFriendStateArray(friend); } return i; @@ -349,10 +350,10 @@ public Collection getFriends() { public Collection getPendingFriendRequests() { return requests.updateAndGet(i -> { if (i == null) { - JsonObject object = client.getNetworkClient().get(HttpAPIRoute.FRIEND_LIST.toFullURL()); - JsonArray request = get(object, "request").getAsJsonArray(); + JsonNode object = client.getNetworkClient().get(HttpAPIRoute.FRIEND_LIST.toFullURL()); + JsonNode request = get(object, "request"); Collection requestCollection; - if (!request.isEmpty()) { + if (request.isArray() && request.size() > 0) { requestCollection = new HashSet<>(request.size()); convertRawRequest(request, requestCollection); } else { @@ -364,12 +365,12 @@ public Collection getPendingFriendRequests() { }); } - protected Collection buildUserListFromFriendStateArray(JsonArray array) { - if (!array.isEmpty()) { + protected Collection buildUserListFromFriendStateArray(JsonNode array) { + if (array.isArray() && array.size() > 0) { Collection c = new ArrayList<>(array.size()); - for (JsonElement element : array) { - JsonObject userObj = get(element.getAsJsonObject(), "friend_info").getAsJsonObject(); - User user = client.getStorage().getUser(get(userObj, "id").getAsString(), userObj); + for (JsonNode element : array) { + JsonNode userObj = get(element, "friend_info"); + User user = client.getStorage().getUser(get(userObj, "id").asText(), userObj); c.add(user); } return Collections.unmodifiableCollection(c); @@ -441,4 +442,251 @@ public void handleFriendRequest(int id, boolean accept) { .build(); HttpAPIImpl.this.client.getNetworkClient().postContent(HttpAPIRoute.FRIEND_HANDLE_REQUEST.toFullURL(), body); } + + // ===== 异步 API 实现 - 内部使用,提供给插件的异步版本 ===== + + /** + * 异步文件上传 - File 版本 + * + * @param file 要上传的文件 + * @return 异步上传结果,包含文件 URL + */ + public CompletableFuture uploadFileAsync(File file) { + return requestCoalescer.coalesce("upload_file_" + file.getName() + "_" + file.length(), () -> + CompletableFuture.supplyAsync(() -> { + RequestBody body = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", file.getName(), RequestBody.create(file, OCTET_STREAM)) + .build(); + Request request = new Request.Builder() + .url(HttpAPIRoute.ASSET_UPLOAD.toFullURL()) + .post(body) + .addHeader("Authorization", client.getNetworkClient().getTokenWithPrefix()) + .build(); + return get(parse(client.getNetworkClient().call(request)).get("data"), "url").asText(); + }, httpExecutor) + ); + } + + /** + * 异步文件上传 - 字节数组版本 + * + * @param filename 文件名 + * @param content 文件内容字节数组 + * @return 异步上传结果,包含文件 URL + */ + public CompletableFuture uploadFileAsync(String filename, byte[] content) { + return requestCoalescer.coalesce("upload_file_" + filename + "_" + content.length, () -> + CompletableFuture.supplyAsync(() -> { + RequestBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", filename, RequestBody.create(content, OCTET_STREAM)) + .build(); + Request request = new Request.Builder() + .url(HttpAPIRoute.ASSET_UPLOAD.toFullURL()) + .post(requestBody) + .addHeader("Authorization", client.getNetworkClient().getTokenWithPrefix()) + .build(); + return get(parse(client.getNetworkClient().call(request)).get("data"), "url").asText(); + }, httpExecutor) + ); + } + + /** + * 异步文件上传 - URL 版本 + * + * @param fileName 文件名 + * @param url 文件 URL + * @return 异步上传结果,包含文件 URL + */ + public CompletableFuture uploadFileAsync(String fileName, String url) { + return requestCoalescer.coalesce("upload_file_url_" + url, () -> + CompletableFuture.supplyAsync(() -> { + try { + new URL(url); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Cannot upload file: Malformed URL", e); + } + try (Response response = client.getNetworkClient().getOkHttpClient().newCall( + new Request.Builder() + .get() + .url(url) + .build() + ).execute()) { + final String bodyErr = "Cannot upload file at " + url + ": Response body should not be null"; + final ResponseBody body = Objects.requireNonNull(response.body(), bodyErr); + byte[] bytes = body.bytes(); + return uploadFileAsync(fileName, bytes).join(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }, httpExecutor) + ); + } + + /** + * 异步删除邀请链接 + * + * @param urlCode 邀请链接代码 + * @return 异步删除结果 + */ + public CompletableFuture removeInviteAsync(String urlCode) { + return requestCoalescer.coalesce("remove_invite_" + urlCode, () -> + CompletableFuture.runAsync(() -> { + client.getNetworkClient().post(HttpAPIRoute.INVITE_DELETE.toFullURL(), + Collections.singletonMap("url_code", urlCode)); + }, httpExecutor) + ); + } + + /** + * 异步获取用户信息 + * + * @param id 用户 ID + * @return 异步用户信息 + */ + public CompletableFuture getUserAsync(String id) { + return requestCoalescer.coalesce("get_user_" + id, () -> + CompletableFuture.supplyAsync(() -> client.getStorage().getUser(id), httpExecutor) + ); + } + + /** + * 异步获取服务器信息 + * + * @param id 服务器 ID + * @return 异步服务器信息 + */ + public CompletableFuture getGuildAsync(String id) { + return requestCoalescer.coalesce("get_guild_" + id, () -> + CompletableFuture.supplyAsync(() -> client.getStorage().getGuild(id), httpExecutor) + ); + } + + /** + * 异步获取频道信息 + * + * @param id 频道 ID + * @return 异步频道信息 + */ + public CompletableFuture getChannelAsync(String id) { + return requestCoalescer.coalesce("get_channel_" + id, () -> + CompletableFuture.supplyAsync(() -> client.getStorage().getChannel(id), httpExecutor) + ); + } + + /** + * 批量异步获取用户信息 + * + * @param userIds 用户 ID 列表 + * @return 异步用户信息列表 + */ + public CompletableFuture> getBatchUsersAsync(List userIds) { + return CompletableFuture.supplyAsync(() -> { + List> futures = userIds.stream() + .map(this::getUserAsync) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())) + .join(); + }, httpExecutor); + } + + /** + * 批量异步获取服务器信息 + * + * @param guildIds 服务器 ID 列表 + * @return 异步服务器信息列表 + */ + public CompletableFuture> getBatchGuildsAsync(List guildIds) { + return CompletableFuture.supplyAsync(() -> { + List> futures = guildIds.stream() + .map(this::getGuildAsync) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())) + .join(); + }, httpExecutor); + } + + /** + * 批量异步获取频道信息 + * + * @param channelIds 频道 ID 列表 + * @return 异步频道信息列表 + */ + public CompletableFuture> getBatchChannelsAsync(List channelIds) { + return CompletableFuture.supplyAsync(() -> { + List> futures = channelIds.stream() + .map(this::getChannelAsync) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())) + .join(); + }, httpExecutor); + } + + // ===== 智能请求合并器实现 ===== + + /** + * 智能请求合并器 - 避免重复的并发请求,提升性能 + */ + private static class RequestCoalescer { + private final Map> ongoingRequests = new ConcurrentHashMap<>(); + + /** + * 合并相同的请求,避免重复执行 + * + * @param key 请求的唯一标识 + * @param supplier 请求的执行逻辑 + * @param 请求结果类型 + * @return 合并后的请求结果 + */ + @SuppressWarnings("unchecked") + public CompletableFuture coalesce(String key, Supplier> supplier) { + return (CompletableFuture) ongoingRequests.computeIfAbsent(key, k -> { + CompletableFuture future = supplier.get(); + // 请求完成后清理缓存 + future.whenComplete((result, exception) -> ongoingRequests.remove(k)); + return future; + }); + } + + /** + * 获取当前正在进行的请求数量 + * + * @return 正在进行的请求数量 + */ + public int getOngoingRequestCount() { + return ongoingRequests.size(); + } + + /** + * 清理所有请求缓存(用于测试或特殊情况) + */ + public void clearCache() { + ongoingRequests.clear(); + } + } + + // ===== AsyncHttpAPI 接口实现 ===== + + @Override + public int getOngoingRequestCount() { + return requestCoalescer.getOngoingRequestCount(); + } + + @Override + public void clearRequestCache() { + requestCoalescer.clearCache(); + } } diff --git a/src/main/java/snw/kookbc/impl/KBCClient.java b/src/main/java/snw/kookbc/impl/KBCClient.java index 16affc33..e0aa9ada 100644 --- a/src/main/java/snw/kookbc/impl/KBCClient.java +++ b/src/main/java/snw/kookbc/impl/KBCClient.java @@ -72,6 +72,7 @@ import java.util.function.Consumer; import static snw.kookbc.util.Util.closeLoaderIfPossible; +import static snw.kookbc.util.VirtualThreadUtil.newVirtualThreadExecutor; // The client representation. public class KBCClient { @@ -133,7 +134,7 @@ public KBCClient(CoreImpl core, ConfigurationSection config, File pluginsFolder, this.storage = Optional.ofNullable(storage).orElseGet(() -> EntityStorage::new).apply(this); this.entityBuilder = Optional.ofNullable(entityBuilder).orElseGet(() -> EntityBuilder::new).apply(this); this.msgBuilder = Optional.ofNullable(msgBuilder).orElseGet(() -> MessageBuilder::new).apply(this); - this.eventExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "Event Executor")); + this.eventExecutor = newVirtualThreadExecutor("Event-Executor"); this.shutdownLock = new ReentrantLock(); this.shutdownCondition = this.shutdownLock.newCondition(); this.eventFactory = Optional.ofNullable(eventFactory).orElseGet(() -> EventFactory::new).apply(this); @@ -145,8 +146,8 @@ public KBCClient(CoreImpl core, ConfigurationSection config, File pluginsFolder, this.networkSystem = new JLHttpWebhookNetworkSystem(this, null); } else { getCore().getLogger().warn("***********************************"); - getCore().getLogger().warn("Unrecognized network mode: " + mode); - getCore().getLogger().warn("Switch to default mode: websocket"); + getCore().getLogger().warn("无法识别的网络模式: " + mode); + getCore().getLogger().warn("切换到默认模式: websocket"); getCore().getLogger().warn("***********************************"); this.networkSystem = new OkhttpWebSocketNetworkSystem(this); } @@ -171,7 +172,7 @@ protected void loadPermissions() { this.userPermissions.put(userPermissionSaved.getUid(), userPermissionSaved); } } catch (IOException e) { - getCore().getLogger().error("Failed to load permissions", e); + getCore().getLogger().error("加载权限失败", e); } } @@ -234,40 +235,40 @@ public boolean isRunning() { // or you will get NullPointerException. public synchronized void start() { // Print version information - getCore().getLogger().info("Starting {} version {}", getCore().getImplementationName(), getCore().getImplementationVersion()); - getCore().getLogger().info("This VM is running {} version {} (Implementing API version {})", getCore().getImplementationName(), getCore().getImplementationVersion(), getCore().getAPIVersion()); - getCore().getLogger().info("Working directory: {}", new File(".").getAbsolutePath()); + getCore().getLogger().info("正在启动 {} 版本 {}", getCore().getImplementationName(), getCore().getImplementationVersion()); + getCore().getLogger().info("当前运行 {} 版本 {} (实现 API 版本 {})", getCore().getImplementationName(), getCore().getImplementationVersion(), getCore().getAPIVersion()); + getCore().getLogger().info("工作目录: {}", new File(".").getAbsolutePath()); Properties gitProperties = new Properties(); try { gitProperties.load(getClass().getClassLoader().getResourceAsStream("kookbc_git_data.properties")); - getCore().getLogger().info("Compiled from Git commit {}, build at {}", gitProperties.get("git.commit.id"), gitProperties.get("git.commit.time")); + getCore().getLogger().info("编译信息: Git 提交 {}, 构建时间 {}", gitProperties.get("git.commit.id"), gitProperties.get("git.commit.time")); } catch (NullPointerException | IOException e) { - getCore().getLogger().warn("Unable to read Git commit information", e); + getCore().getLogger().warn("无法读取 Git 提交信息", e); } if (SharedConstants.IS_SNAPSHOT) { getCore().getLogger().warn("***********************************"); - getCore().getLogger().warn("YOU ARE RUNNING A SNAPSHOT BUILD."); - getCore().getLogger().warn("DO NOT USE SNAPSHOT BUILDS IN YOUR"); - getCore().getLogger().warn(" PRODUCTION ENVIRONMENT!"); - getCore().getLogger().warn("If you don't know why you're seeing"); - getCore().getLogger().warn(" this, download the stable version."); + getCore().getLogger().warn("您正在运行快照构建版本。"); + getCore().getLogger().warn("请勿在生产环境中使用"); + getCore().getLogger().warn(" 快照构建版本!"); + getCore().getLogger().warn("如果您不知道为什么看到"); + getCore().getLogger().warn(" 这个消息,请下载稳定版本。"); getCore().getLogger().warn("***********************************"); } - core.getLogger().debug("Fetching Bot user object"); + core.getLogger().debug("正在获取 Bot 用户对象"); User botUser = getEntityBuilder().buildUser(getNetworkClient().get(HttpAPIRoute.USER_ME.toFullURL())); getStorage().addUser(botUser); core.setUser(botUser); registerInternal(); - getCore().getLogger().debug("Enabling plugins"); + getCore().getLogger().debug("正在启用插件"); enablePlugins(); - getCore().getLogger().info("Running delayed init tasks"); + getCore().getLogger().info("正在运行延迟初始化任务"); ((SchedulerImpl) core.getScheduler()).runAfterPluginInitTasks(); - getCore().getLogger().debug("Starting Network"); + getCore().getLogger().debug("正在启动网络"); startNetwork(); finishStart(); - getCore().getLogger().info("Done! Type \"help\" for help."); + getCore().getLogger().info("完成!输入 \"help\" 获取帮助。"); if (getConfig().getBoolean("check-update", true)) { new UpdateChecker(this).start(); // check update. Added since 2022/7/24 @@ -294,18 +295,18 @@ private void enablePlugins() { } @SuppressWarnings("DataFlowIssue") List newIncomingFiles = new ArrayList<>(Arrays.asList(getPluginsFolder().listFiles(File::isFile))); - getCore().getLogger().debug("Before filtering: {}", newIncomingFiles); - getCore().getLogger().debug("Current known plugins: {}", this.plugins); + getCore().getLogger().debug("过滤前: {}", newIncomingFiles); + getCore().getLogger().debug("当前已知插件: {}", this.plugins); for (Plugin plugin : this.plugins) { - getCore().getLogger().debug("Checking file: {}", plugin.getFile()); + getCore().getLogger().debug("正在检查文件: {}", plugin.getFile()); newIncomingFiles.removeIf(i -> i.getAbsolutePath().equals(plugin.getFile().getAbsolutePath())); // remove already loaded file } - getCore().getLogger().debug("After filtering: {}", newIncomingFiles); + getCore().getLogger().debug("过滤后: {}", newIncomingFiles); int before = ((SimplePluginManager) getCore().getPluginManager()).getLoaderProviders().size(); List pluginsToEnable = this.plugins; - getCore().getLogger().debug("Plugins to be enabled: {}", pluginsToEnable); + getCore().getLogger().debug("待启用的插件: {}", pluginsToEnable); boolean shouldContinue; @@ -314,9 +315,9 @@ private void enablePlugins() { enablePlugins(pluginsToEnable); int after = ((SimplePluginManager) getCore().getPluginManager()).getLoaderProviders().size(); if (after > before) { // new loader providers added - getCore().getLogger().debug("Found new plugin loader providers, trying to load more plugins"); + getCore().getLogger().debug("发现新的插件加载器提供者,尝试加载更多插件"); if (!newIncomingFiles.isEmpty()) { - getCore().getLogger().debug("Files to be loaded: {}", newIncomingFiles); + getCore().getLogger().debug("待加载的文件: {}", newIncomingFiles); List newPlugins = new ArrayList<>(); for (Iterator iterator = newIncomingFiles.iterator(); iterator.hasNext(); ) { File fileToLoad = iterator.next(); @@ -324,14 +325,14 @@ private void enablePlugins() { try { plugin = getCore().getPluginManager().loadPlugin(fileToLoad); } catch (InvalidPluginException e) { - getCore().getLogger().debug("Exception appeared", e); + getCore().getLogger().debug("出现异常", e); continue; // don't remove, maybe it will be loaded in next loop? } - getCore().getLogger().debug("Successfully loaded {} from file {}", plugin, fileToLoad); + getCore().getLogger().debug("成功从文件 {} 加载插件 {}", fileToLoad, plugin); newPlugins.add(plugin); iterator.remove(); // prevent next loop load this again } - getCore().getLogger().debug("New plugins to be enabled in next round: {}", newPlugins); + getCore().getLogger().debug("下一轮待启用的新插件: {}", newPlugins); if (!newPlugins.isEmpty()) { newPlugins.sort(DependencyListBasedPluginComparator.INSTANCE); pluginsToEnable = newPlugins; @@ -356,11 +357,11 @@ private void enablePlugins(@Nullable List plugins) { // onLoad PluginDescription description = plugin.getDescription(); - plugin.getLogger().info("Loading {} version {}", description.getName(), description.getVersion()); + plugin.getLogger().info("正在加载 {} 版本 {}", description.getName(), description.getVersion()); try { plugin.onLoad(); } catch (Throwable e) { - plugin.getLogger().error("Unable to load this plugin", e); + plugin.getLogger().error("无法加载此插件", e); iterator.remove(); } // end onLoad @@ -372,14 +373,14 @@ private void enablePlugins(@Nullable List plugins) { try { plugin.reloadConfig(); // ensure the default configuration will be loaded } catch (Exception e) { - plugin.getLogger().error("Unable to load configuration", e); + plugin.getLogger().error("无法加载配置", e); } // onEnable try { getCore().getPluginManager().enablePlugin(plugin); } catch (UnknownDependencyException e) { - getCore().getLogger().error("Unable to enable plugin {} because unknown dependency detected.", plugin.getDescription().getName(), e); + getCore().getLogger().error("无法启用插件 {},检测到未知的依赖项", plugin.getDescription().getName(), e); closeLoaderIfPossible(plugin); iterator.remove(); continue; @@ -414,10 +415,10 @@ protected void finishStart() { UUID.fromString(rawBotMarketUUID); new BotMarketPingThread(this, rawBotMarketUUID, () -> getNetworkSystem().isConnected()).start(); } catch (IllegalArgumentException e) { - getCore().getLogger().warn("Invalid UUID of BotMarket. We won't schedule the PING task for BotMarket."); + getCore().getLogger().warn("BotMarket 的 UUID 无效,不会为 BotMarket 安排 PING 任务"); } */ - getCore().getLogger().warn("BotMarket Ping is currently deprecated, as they are upgrading their system."); + getCore().getLogger().warn("BotMarket Ping 目前已弃用,他们正在升级系统"); } } // endregion @@ -427,41 +428,41 @@ protected void finishStart() { // Note that this method won't return until the client stopped, // so call it in a single thread. public void loop() { - getCore().getLogger().debug("Starting console"); + getCore().getLogger().debug("正在启动控制台"); try { new Console(this).start(); } catch (IOException e) { - getCore().getLogger().error("Failed to read input from console"); - getCore().getLogger().error("Running WITHOUT console!"); - getCore().getLogger().error("You can stop this process by creating a new file named"); - getCore().getLogger().error("KOOKBC_STOP in the working directory of this process."); - getCore().getLogger().error("Stacktrace is following:"); + getCore().getLogger().error("从控制台读取输入失败"); + getCore().getLogger().error("在没有控制台的情况下运行!"); + getCore().getLogger().error("您可以通过创建一个名为"); + getCore().getLogger().error("KOOKBC_STOP 的新文件来停止此进程,文件位于此进程的工作目录中"); + getCore().getLogger().error("堆栈跟踪如下:"); e.printStackTrace(); new StopSignalListener(this).start(); } catch (Exception e) { - getCore().getLogger().error("Unexpected situation happened during the main loop.", e); + getCore().getLogger().error("主循环执行过程中发生意外情况", e); } - getCore().getLogger().debug("REPL end"); + getCore().getLogger().debug("REPL 结束"); } // Shutdown this client, and loop() method will return after this method completes. public synchronized void shutdown() { - getCore().getLogger().debug("Client shutdown request received"); + getCore().getLogger().debug("收到客户端关闭请求"); if (!isRunning()) { - getCore().getLogger().debug("The client has already stopped"); + getCore().getLogger().debug("客户端已经停止"); return; } running = false; // make sure the client will shut down if Bot wish the client stop. - getCore().getLogger().info("Stopping client"); + getCore().getLogger().info("正在停止客户端"); getCore().getPluginManager().clearPlugins(); shutdownNetwork(); eventExecutor.shutdown(); - getCore().getLogger().info("Stopping core"); - getCore().getLogger().info("Stopping scheduler (If the application got into infinite loop, please kill this process!)"); + getCore().getLogger().info("正在停止核心"); + getCore().getLogger().info("正在停止调度器(如果应用程序陷入无限循环,请终止此进程!)"); ((SchedulerImpl) getCore().getScheduler()).shutdown(); - getCore().getLogger().info("Client stopped"); + getCore().getLogger().info("客户端已停止"); // region Emit shutdown signal shutdownLock.lock(); @@ -560,7 +561,7 @@ private void registerCommands(List> commands) { ResultTypes resultTypes = ResultTypes.valueOf(getConfig().getString("internal-commands-reply-result-type")); k.defaultResultType(resultTypes); } catch (Exception e) { - getCore().getLogger().error("`internal-commands-reply-result-type` is not a valid result-type"); + getCore().getLogger().error("`internal-commands-reply-result-type` 不是有效的 result-type"); } return k; }).commands(commands.toArray()).build(); diff --git a/src/main/java/snw/kookbc/impl/command/CommandManagerImpl.java b/src/main/java/snw/kookbc/impl/command/CommandManagerImpl.java index 0bdd4a8f..1dec0d71 100644 --- a/src/main/java/snw/kookbc/impl/command/CommandManagerImpl.java +++ b/src/main/java/snw/kookbc/impl/command/CommandManagerImpl.java @@ -145,7 +145,7 @@ public boolean executeCommand0(CommandSender sender, String cmdLine, Message msg @Override public boolean executeCommand(CommandSender sender, String cmdLine, Message msg) throws CommandException { if (cmdLine.isEmpty()) { - client.getCore().getLogger().debug("Received empty command!"); + client.getCore().getLogger().debug("收到空命令!"); return false; } @@ -176,18 +176,18 @@ public boolean executeCommand(CommandSender sender, String cmdLine, Message msg) // we will use the "/hello a b" as the example JKookCommand actualCommand = null; // "a" is an actual subcommand, so we expect it is not null if (!sub.isEmpty()) { // if the command have subcommand, expect true - client.getCore().getLogger().debug("The subcommand does exists. Attempting to search the final command."); + client.getCore().getLogger().debug("子命令存在,正在尝试搜索最终命令"); while (!args.isEmpty()) { // get the first argument, so we got "a" String subName = args.get(0); - client.getCore().getLogger().debug("Got temp subcommand root name: {}", subName); + client.getCore().getLogger().debug("获取临时子命令根名称: {}", subName); boolean found = false; // expect true // then get the command for (JKookCommand s : sub) { // if the root name equals to the sub name if (Objects.equals(s.getRootName(), subName)) { // expect true - client.getCore().getLogger().debug("Got valid subcommand: {}", subName); // debug + client.getCore().getLogger().debug("获取到有效的子命令: {}", subName); // debug // then remove the first argument args.remove(0); // "a" was removed, so we have "b" in next round actualCommand = s; // got "a" subcommand @@ -197,27 +197,27 @@ public boolean executeCommand(CommandSender sender, String cmdLine, Message msg) } } if (!found) { // if the subcommand is not found - client.getCore().getLogger().debug("No subcommand matching current command root name. We will attempt to execute the command currently found."); // debug + client.getCore().getLogger().debug("没有与当前命令根名称匹配的子命令,将尝试执行当前找到的命令"); // debug // then we can regard the actualCommand as the final result to be executed break; // exit the while loop } } } - client.getCore().getLogger().debug("The final command has been found. Time elasped: {}ms", System.currentTimeMillis() - startTimeStamp); + client.getCore().getLogger().debug("已找到最终命令,用时: {}ms", System.currentTimeMillis() - startTimeStamp); if (sender instanceof User) { if (msg == null) { - client.getCore().getLogger().warn("A user issued command but the message object is null. Is the plugin calling a command as the user?"); + client.getCore().getLogger().warn("用户执行命令但消息对象为空,是插件以用户身份调用命令吗?"); } client.getCore().getLogger().info( - "{}(User ID: {}) issued command: {}", + "{}(用户 ID: {}) 执行命令: {}", ((User) sender).getName(), ((User) sender).getId(), cmdLine ); if (sender == client.getCore().getUser()) { - client.getCore().getLogger().warn("Running a command as the bot in this client instance. It is impossible."); + client.getCore().getLogger().warn("在此客户端实例中以 Bot 身份运行命令,这是不可能的"); } } @@ -439,14 +439,14 @@ private void exec(Runnable runnable, long startTimeStamp, String cmdLine) throws try { runnable.run(); } catch (Throwable e) { - client.getCore().getLogger().debug("The execution of command line '{}' is FAILED, time elapsed: {}ms", cmdLine, System.currentTimeMillis() - startTimeStamp); + client.getCore().getLogger().debug("命令 '{}' 执行失败,用时: {}ms", cmdLine, System.currentTimeMillis() - startTimeStamp); // Why Throwable? We need to keep the client safe. // it is easy to understand. NoClassDefError? NoSuchMethodError? // It is OutOfMemoryError? nothing matters lol. throw new CommandException("Something unexpected happened.", e); } // Do not put this in the try statement because we don't know if the logging system will throw an exception. - client.getCore().getLogger().debug("The execution of command line \"{}\" is done, time elapsed: {}ms", cmdLine, System.currentTimeMillis() - startTimeStamp); + client.getCore().getLogger().debug("命令 \"{}\" 执行完成,用时: {}ms", cmdLine, System.currentTimeMillis() - startTimeStamp); } private void reply(String content, String contentForConsole, CommandSender sender, @Nullable Message message) { diff --git a/src/main/java/snw/kookbc/impl/command/litecommands/KookScheduler.java b/src/main/java/snw/kookbc/impl/command/litecommands/KookScheduler.java index f299fe01..b63c8347 100644 --- a/src/main/java/snw/kookbc/impl/command/litecommands/KookScheduler.java +++ b/src/main/java/snw/kookbc/impl/command/litecommands/KookScheduler.java @@ -1,54 +1,54 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package snw.kookbc.impl.command.litecommands; - -import dev.rollczi.litecommands.scheduler.AbstractMainThreadBasedScheduler; -import snw.jkook.plugin.Plugin; -import snw.kookbc.impl.KBCClient; - -import java.time.Duration; - -public class KookScheduler extends AbstractMainThreadBasedScheduler { - private final KBCClient client; - private final Plugin plugin; - - public KookScheduler(KBCClient client, Plugin plugin) { - this.client = client; - this.plugin = plugin; - } - - - @Override - public void shutdown() { - - } - - @Override - protected void runSynchronous(Runnable task, Duration delay) { - if (delay.isZero() && client.isPrimaryThread()) { - task.run(); - } else { - plugin.getCore().getScheduler().runTaskLater(plugin, task, delay.toMillis()); - } - } - - @Override - protected void runAsynchronous(Runnable task, Duration delay) { - plugin.getCore().getScheduler().runTaskLater(plugin, task, delay.toMillis()); - } -} +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package snw.kookbc.impl.command.litecommands; + +import dev.rollczi.litecommands.scheduler.AbstractMainThreadBasedScheduler; +import snw.jkook.plugin.Plugin; +import snw.kookbc.impl.KBCClient; + +import java.time.Duration; + +public class KookScheduler extends AbstractMainThreadBasedScheduler { + private final KBCClient client; + private final Plugin plugin; + + public KookScheduler(KBCClient client, Plugin plugin) { + this.client = client; + this.plugin = plugin; + } + + + @Override + public void shutdown() { + + } + + @Override + protected void runSynchronous(Runnable task, Duration delay) { + if (delay.isZero() && client.isPrimaryThread()) { + task.run(); + } else { + plugin.getCore().getScheduler().runTaskLater(plugin, task, delay.toMillis()); + } + } + + @Override + protected void runAsynchronous(Runnable task, Duration delay) { + plugin.getCore().getScheduler().runTaskLater(plugin, task, delay.toMillis()); + } +} diff --git a/src/main/java/snw/kookbc/impl/command/litecommands/LiteKookFactory.java b/src/main/java/snw/kookbc/impl/command/litecommands/LiteKookFactory.java index f32a796f..a1afe597 100644 --- a/src/main/java/snw/kookbc/impl/command/litecommands/LiteKookFactory.java +++ b/src/main/java/snw/kookbc/impl/command/litecommands/LiteKookFactory.java @@ -93,7 +93,7 @@ public static ("只有后台才能执行该命令")) .scheduler(new KookScheduler(client, plugin)) - .selfProcessor((builder, internal) -> + .self((builder, internal) -> builder.argument(User.class, new UserArgument(httpAPI, internal.getMessageRegistry())) .argument(Guild.class, new GuildArgument(httpAPI, internal.getMessageRegistry())) .argument(Channel.class, new ChannelArgument<>(httpAPI, internal.getMessageRegistry())) diff --git a/src/main/java/snw/kookbc/impl/command/litecommands/annotations/result/ResultAnnotationResolver.java b/src/main/java/snw/kookbc/impl/command/litecommands/annotations/result/ResultAnnotationResolver.java index ed18a047..8298bbdb 100644 --- a/src/main/java/snw/kookbc/impl/command/litecommands/annotations/result/ResultAnnotationResolver.java +++ b/src/main/java/snw/kookbc/impl/command/litecommands/annotations/result/ResultAnnotationResolver.java @@ -43,7 +43,7 @@ public AnnotationInvoker process(AnnotationInvoker invoker) { resultTypes = annotation.custom().getConstructor().newInstance(); } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - logger.error("@Result(custom) create failure, using value()", e); + logger.error("@Result(custom) 创建失败,使用 value() 方法", e); } } if (resultTypes == ResultTypes.DEFAULT) return; diff --git a/src/main/java/snw/kookbc/impl/command/litecommands/argument/EmojiArgument.java b/src/main/java/snw/kookbc/impl/command/litecommands/argument/EmojiArgument.java index 1e6c130d..c76907dc 100644 --- a/src/main/java/snw/kookbc/impl/command/litecommands/argument/EmojiArgument.java +++ b/src/main/java/snw/kookbc/impl/command/litecommands/argument/EmojiArgument.java @@ -1,117 +1,117 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.command.litecommands.argument; - -import dev.rollczi.litecommands.argument.Argument; -import dev.rollczi.litecommands.argument.parser.ParseResult; -import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; -import dev.rollczi.litecommands.invocation.Invocation; -import dev.rollczi.litecommands.message.MessageKey; -import dev.rollczi.litecommands.message.MessageRegistry; -import dev.rollczi.litecommands.suggestion.SuggestionContext; -import dev.rollczi.litecommands.suggestion.SuggestionResult; -import snw.jkook.command.CommandException; -import snw.jkook.command.CommandSender; -import snw.jkook.command.ConsoleCommandSender; -import snw.jkook.entity.CustomEmoji; -import snw.jkook.entity.Guild; -import snw.jkook.message.ChannelMessage; -import snw.jkook.message.Message; -import snw.jkook.util.PageIterator; -import snw.kookbc.impl.KBCClient; - -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -public class EmojiArgument extends ArgumentResolver { - public static final MessageKey EMOJI_NOT_FOUND = MessageKey.of("emoji_not_found", "Emoji not found"); - public static final MessageKey NOT_CHANNEL = MessageKey.of("emoji_not_channel", "Not supporting finding emoji"); - public static final MessageKey SENDER_UNSUPPORTED = MessageKey.of("emoji_sender_unsupported", sender -> { - if (sender instanceof ConsoleCommandSender) { - return "Unsupported console command"; - } - return "Unsupported command"; - }); - public static final MessageKey EMOJI_FOUND_FAILURE = MessageKey.of("emoji_found_failure", "Emoji found failure"); - - private final KBCClient client; - private final MessageRegistry messageRegistry; - - public EmojiArgument(KBCClient client, MessageRegistry messageRegistry) { - this.client = client; - this.messageRegistry = messageRegistry; - } - - @Override - protected ParseResult parse(Invocation invocation, Argument context, String argument) { - String emojiName = argument; - String emojiId = argument; - if (emojiName.startsWith("(emj)")) { - emojiName = emojiName.substring(5); - } - int indexOf = emojiName.indexOf("(emj)"); - if (indexOf >= 0) { - emojiId = emojiName.substring(indexOf + 5); - emojiName = emojiName.substring(0, indexOf); - } - indexOf = emojiId.indexOf(']'); - if (indexOf >= 0) { - emojiId = emojiId.substring(1, indexOf); - } - try { - Optional optional = invocation.context().get(Message.class); - if (!optional.isPresent()) { - return ParseResult.failure(messageRegistry.getInvoked(SENDER_UNSUPPORTED, invocation, invocation.sender())); - } - Message message = optional.get(); - if (message instanceof ChannelMessage) { - ChannelMessage channelMessage = (ChannelMessage) message; - Guild guild = channelMessage.getChannel().getGuild(); - - CustomEmoji emoji = client.getStorage().getEmoji(emojiId); - if (emoji == null) { - PageIterator> emojis = guild.getCustomEmojis(); - FIND: - while (emojis.hasNext()) { - Set set = emojis.next(); - for (CustomEmoji r : set) { - if (Objects.equals(r.getId(), emojiId) && Objects.equals(r.getName(), emojiName)) { - emoji = r; - break FIND; - } - } - } - } - if (emoji == null) { - return ParseResult.failure(messageRegistry.getInvoked(EMOJI_NOT_FOUND, invocation, argument)); - } - return ParseResult.success(emoji); - } - return ParseResult.failure(messageRegistry.getInvoked(NOT_CHANNEL, invocation, message)); - } catch (final Exception e) { - return ParseResult.failure(messageRegistry.getInvoked(EMOJI_FOUND_FAILURE, invocation, new CommandException("CustomEmoji not found", e))); - } - } - - @Override - public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { - return super.suggest(invocation, argument, context); - } -} +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.command.litecommands.argument; + +import dev.rollczi.litecommands.argument.Argument; +import dev.rollczi.litecommands.argument.parser.ParseResult; +import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; +import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.message.MessageKey; +import dev.rollczi.litecommands.message.MessageRegistry; +import dev.rollczi.litecommands.suggestion.SuggestionContext; +import dev.rollczi.litecommands.suggestion.SuggestionResult; +import snw.jkook.command.CommandException; +import snw.jkook.command.CommandSender; +import snw.jkook.command.ConsoleCommandSender; +import snw.jkook.entity.CustomEmoji; +import snw.jkook.entity.Guild; +import snw.jkook.message.ChannelMessage; +import snw.jkook.message.Message; +import snw.jkook.util.PageIterator; +import snw.kookbc.impl.KBCClient; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public class EmojiArgument extends ArgumentResolver { + public static final MessageKey EMOJI_NOT_FOUND = MessageKey.of("emoji_not_found", "Emoji not found"); + public static final MessageKey NOT_CHANNEL = MessageKey.of("emoji_not_channel", "Not supporting finding emoji"); + public static final MessageKey SENDER_UNSUPPORTED = MessageKey.of("emoji_sender_unsupported", sender -> { + if (sender instanceof ConsoleCommandSender) { + return "Unsupported console command"; + } + return "Unsupported command"; + }); + public static final MessageKey EMOJI_FOUND_FAILURE = MessageKey.of("emoji_found_failure", "Emoji found failure"); + + private final KBCClient client; + private final MessageRegistry messageRegistry; + + public EmojiArgument(KBCClient client, MessageRegistry messageRegistry) { + this.client = client; + this.messageRegistry = messageRegistry; + } + + @Override + protected ParseResult parse(Invocation invocation, Argument context, String argument) { + String emojiName = argument; + String emojiId = argument; + if (emojiName.startsWith("(emj)")) { + emojiName = emojiName.substring(5); + } + int indexOf = emojiName.indexOf("(emj)"); + if (indexOf >= 0) { + emojiId = emojiName.substring(indexOf + 5); + emojiName = emojiName.substring(0, indexOf); + } + indexOf = emojiId.indexOf(']'); + if (indexOf >= 0) { + emojiId = emojiId.substring(1, indexOf); + } + try { + Optional optional = invocation.context().get(Message.class); + if (!optional.isPresent()) { + return ParseResult.failure(messageRegistry.getInvoked(SENDER_UNSUPPORTED, invocation, invocation.sender())); + } + Message message = optional.get(); + if (message instanceof ChannelMessage) { + ChannelMessage channelMessage = (ChannelMessage) message; + Guild guild = channelMessage.getChannel().getGuild(); + + CustomEmoji emoji = client.getStorage().getEmoji(emojiId); + if (emoji == null) { + PageIterator> emojis = guild.getCustomEmojis(); + FIND: + while (emojis.hasNext()) { + Set set = emojis.next(); + for (CustomEmoji r : set) { + if (Objects.equals(r.getId(), emojiId) && Objects.equals(r.getName(), emojiName)) { + emoji = r; + break FIND; + } + } + } + } + if (emoji == null) { + return ParseResult.failure(messageRegistry.getInvoked(EMOJI_NOT_FOUND, invocation, argument)); + } + return ParseResult.success(emoji); + } + return ParseResult.failure(messageRegistry.getInvoked(NOT_CHANNEL, invocation, message)); + } catch (final Exception e) { + return ParseResult.failure(messageRegistry.getInvoked(EMOJI_FOUND_FAILURE, invocation, new CommandException("CustomEmoji not found", e))); + } + } + + @Override + public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { + return super.suggest(invocation, argument, context); + } +} diff --git a/src/main/java/snw/kookbc/impl/command/litecommands/argument/RoleArgument.java b/src/main/java/snw/kookbc/impl/command/litecommands/argument/RoleArgument.java index cac22bb8..7cdbc671 100644 --- a/src/main/java/snw/kookbc/impl/command/litecommands/argument/RoleArgument.java +++ b/src/main/java/snw/kookbc/impl/command/litecommands/argument/RoleArgument.java @@ -1,114 +1,114 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.command.litecommands.argument; - -import dev.rollczi.litecommands.argument.Argument; -import dev.rollczi.litecommands.argument.parser.ParseResult; -import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; -import dev.rollczi.litecommands.invocation.Invocation; -import dev.rollczi.litecommands.message.MessageKey; -import dev.rollczi.litecommands.message.MessageRegistry; -import dev.rollczi.litecommands.suggestion.SuggestionContext; -import dev.rollczi.litecommands.suggestion.SuggestionResult; -import snw.jkook.command.CommandException; -import snw.jkook.command.CommandSender; -import snw.jkook.command.ConsoleCommandSender; -import snw.jkook.entity.Guild; -import snw.jkook.entity.Role; -import snw.jkook.entity.channel.Channel; -import snw.jkook.message.ChannelMessage; -import snw.jkook.message.Message; -import snw.jkook.util.PageIterator; -import snw.kookbc.impl.KBCClient; - -import java.util.Optional; -import java.util.Set; - -public class RoleArgument extends ArgumentResolver { - public static final MessageKey ROLE_NOT_FOUND = MessageKey.of("role_not_found", "User not found"); - public static final MessageKey NOT_CHANNEL = MessageKey.of("role_not_channel", "Not supporting finding role"); - public static final MessageKey SENDER_UNSUPPORTED = MessageKey.of("role_sender_unsupported", sender -> { - if (sender instanceof ConsoleCommandSender) { - return "Unsupported console command"; - } - return "Unsupported command"; - }); - public static final MessageKey ROLE_FOUND_FAILURE = MessageKey.of("role_found_failure", "Role found failure"); - - private final KBCClient client; - private final MessageRegistry messageRegistry; - - public RoleArgument(KBCClient client, MessageRegistry messageRegistry) { - this.client = client; - this.messageRegistry = messageRegistry; - } - - @Override - protected ParseResult parse(Invocation invocation, Argument context, String argument) { - String input = argument; - int index = input.indexOf("(rol)"); - if (index >= 0) { - input = input.substring(index + 5); - } - index = input.indexOf("(rol)"); - if (index >= 0) { - input = input.substring(0, index); - } - try { - Optional optional = invocation.context().get(Message.class); - if (!optional.isPresent()) { - return ParseResult.failure(messageRegistry.getInvoked(SENDER_UNSUPPORTED, invocation, invocation.sender())); - } - Message message = optional.get(); - if (message instanceof ChannelMessage) { - ChannelMessage channelMessage = (ChannelMessage) message; - Guild guild = channelMessage.getChannel().getGuild(); - - int roleId = Integer.parseInt(input); - - Role role = client.getStorage().getRole(guild, roleId); - if (role == null) { - PageIterator> roles = guild.getRoles(); - FIND: - while (roles.hasNext()) { - Set set = roles.next(); - for (Role r : set) { - if (r.getId() == roleId) { - role = r; - break FIND; - } - } - } - } - if (role == null) { - return ParseResult.failure(messageRegistry.getInvoked(ROLE_NOT_FOUND, invocation, argument)); - } - return ParseResult.success(role); - } - return ParseResult.failure(messageRegistry.getInvoked(NOT_CHANNEL, invocation, message)); - } catch (final Exception e) { - return ParseResult.failure(messageRegistry.getInvoked(ROLE_FOUND_FAILURE, invocation, new CommandException("Role not found", e))); - } - } - - @Override - public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { - return super.suggest(invocation, argument, context); - } -} +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.command.litecommands.argument; + +import dev.rollczi.litecommands.argument.Argument; +import dev.rollczi.litecommands.argument.parser.ParseResult; +import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; +import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.message.MessageKey; +import dev.rollczi.litecommands.message.MessageRegistry; +import dev.rollczi.litecommands.suggestion.SuggestionContext; +import dev.rollczi.litecommands.suggestion.SuggestionResult; +import snw.jkook.command.CommandException; +import snw.jkook.command.CommandSender; +import snw.jkook.command.ConsoleCommandSender; +import snw.jkook.entity.Guild; +import snw.jkook.entity.Role; +import snw.jkook.entity.channel.Channel; +import snw.jkook.message.ChannelMessage; +import snw.jkook.message.Message; +import snw.jkook.util.PageIterator; +import snw.kookbc.impl.KBCClient; + +import java.util.Optional; +import java.util.Set; + +public class RoleArgument extends ArgumentResolver { + public static final MessageKey ROLE_NOT_FOUND = MessageKey.of("role_not_found", "User not found"); + public static final MessageKey NOT_CHANNEL = MessageKey.of("role_not_channel", "Not supporting finding role"); + public static final MessageKey SENDER_UNSUPPORTED = MessageKey.of("role_sender_unsupported", sender -> { + if (sender instanceof ConsoleCommandSender) { + return "Unsupported console command"; + } + return "Unsupported command"; + }); + public static final MessageKey ROLE_FOUND_FAILURE = MessageKey.of("role_found_failure", "Role found failure"); + + private final KBCClient client; + private final MessageRegistry messageRegistry; + + public RoleArgument(KBCClient client, MessageRegistry messageRegistry) { + this.client = client; + this.messageRegistry = messageRegistry; + } + + @Override + protected ParseResult parse(Invocation invocation, Argument context, String argument) { + String input = argument; + int index = input.indexOf("(rol)"); + if (index >= 0) { + input = input.substring(index + 5); + } + index = input.indexOf("(rol)"); + if (index >= 0) { + input = input.substring(0, index); + } + try { + Optional optional = invocation.context().get(Message.class); + if (!optional.isPresent()) { + return ParseResult.failure(messageRegistry.getInvoked(SENDER_UNSUPPORTED, invocation, invocation.sender())); + } + Message message = optional.get(); + if (message instanceof ChannelMessage) { + ChannelMessage channelMessage = (ChannelMessage) message; + Guild guild = channelMessage.getChannel().getGuild(); + + int roleId = Integer.parseInt(input); + + Role role = client.getStorage().getRole(guild, roleId); + if (role == null) { + PageIterator> roles = guild.getRoles(); + FIND: + while (roles.hasNext()) { + Set set = roles.next(); + for (Role r : set) { + if (r.getId() == roleId) { + role = r; + break FIND; + } + } + } + } + if (role == null) { + return ParseResult.failure(messageRegistry.getInvoked(ROLE_NOT_FOUND, invocation, argument)); + } + return ParseResult.success(role); + } + return ParseResult.failure(messageRegistry.getInvoked(NOT_CHANNEL, invocation, message)); + } catch (final Exception e) { + return ParseResult.failure(messageRegistry.getInvoked(ROLE_FOUND_FAILURE, invocation, new CommandException("Role not found", e))); + } + } + + @Override + public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { + return super.suggest(invocation, argument, context); + } +} diff --git a/src/main/java/snw/kookbc/impl/command/litecommands/result/ResultTypes.java b/src/main/java/snw/kookbc/impl/command/litecommands/result/ResultTypes.java index a6f896c2..780eac6c 100644 --- a/src/main/java/snw/kookbc/impl/command/litecommands/result/ResultTypes.java +++ b/src/main/java/snw/kookbc/impl/command/litecommands/result/ResultTypes.java @@ -110,7 +110,7 @@ public static void execute(Message message, T result, BiConsumer public void message(Invocation invocation, Message message, Object result) { CommandSender sender = invocation.sender(); if (sender instanceof ConsoleCommandSender) { - ((ConsoleCommandSender) sender).getLogger().info("The execution result of command {}: {}", invocation.label(), result); + ((ConsoleCommandSender) sender).getLogger().info("命令 {} 的执行结果: {}", invocation.label(), result); } else if (sender instanceof User) { Class clazz = null; if (result instanceof BaseComponent) { diff --git a/src/main/java/snw/kookbc/impl/console/Console.java b/src/main/java/snw/kookbc/impl/console/Console.java index b0583891..06fd92af 100644 --- a/src/main/java/snw/kookbc/impl/console/Console.java +++ b/src/main/java/snw/kookbc/impl/console/Console.java @@ -46,16 +46,16 @@ protected void runCommand(String s) { try { foundCommand = client.getCore().getCommandManager().executeCommand(client.getCore().getConsoleCommandSender(), s); } catch (Exception e) { - client.getCore().getLogger().error("Unexpected situation happened during the execution of the command.", e); + client.getCore().getLogger().error("执行命令时发生意外情况", e); } if (!foundCommand) { - client.getCore().getLogger().info("Unknown command. Type \"/help\" for help."); + client.getCore().getLogger().info("未知命令,输入 \"/help\" 获取帮助"); } } @Override protected void shutdown() { - client.getCore().getLogger().debug("Got shutdown request from console! Stopping!"); + client.getCore().getLogger().debug("收到来自控制台的关闭请求!正在停止!"); client.shutdown(); } diff --git a/src/main/java/snw/kookbc/impl/entity/CustomEmojiImpl.java b/src/main/java/snw/kookbc/impl/entity/CustomEmojiImpl.java index d8e42801..dbf27d70 100644 --- a/src/main/java/snw/kookbc/impl/entity/CustomEmojiImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/CustomEmojiImpl.java @@ -19,13 +19,15 @@ package snw.kookbc.impl.entity; import static snw.jkook.util.Validate.isTrue; -import static snw.kookbc.util.GsonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.getRequiredString; +import static snw.kookbc.util.JacksonUtil.getStringOrDefault; import java.util.Collections; import java.util.Map; import java.util.Objects; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.Guild; @@ -84,8 +86,8 @@ public void setName0(String name) { } @Override - public synchronized void update(JsonObject data) { - isTrue(Objects.equals(getId(), getAsString(data, "id")), "You can't update the emoji by using different data"); - this.name = getAsString(data, "name"); + public synchronized void update(JsonNode data) { + isTrue(Objects.equals(getId(), getRequiredString(data, "id")), "You can't update the emoji by using different data"); + this.name = getStringOrDefault(data, "name", "Unknown Emoji"); } } diff --git a/src/main/java/snw/kookbc/impl/entity/GameImpl.java b/src/main/java/snw/kookbc/impl/entity/GameImpl.java index 28cc6654..7f2508c8 100644 --- a/src/main/java/snw/kookbc/impl/entity/GameImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/GameImpl.java @@ -18,13 +18,14 @@ package snw.kookbc.impl.entity; -import static snw.kookbc.util.GsonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.getStringOrDefault; import java.util.Map; import org.jetbrains.annotations.NotNull; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Game; import snw.kookbc.impl.KBCClient; @@ -98,8 +99,8 @@ public void setNameAndIcon(@NotNull String name, @NotNull String icon) { } @Override - public synchronized void update(JsonObject data) { - this.name = getAsString(data, "name"); - this.icon = getAsString(data, "icon"); + public synchronized void update(JsonNode data) { + this.name = getStringOrDefault(data, "name", "Unknown Game"); + this.icon = getStringOrDefault(data, "icon", ""); } } diff --git a/src/main/java/snw/kookbc/impl/entity/GuildImpl.java b/src/main/java/snw/kookbc/impl/entity/GuildImpl.java index e03d8152..68b3ae05 100644 --- a/src/main/java/snw/kookbc/impl/entity/GuildImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/GuildImpl.java @@ -18,9 +18,7 @@ package snw.kookbc.impl.entity; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.Request; @@ -50,7 +48,7 @@ import static java.util.Objects.requireNonNull; import static snw.jkook.util.Validate.isTrue; -import static snw.kookbc.util.GsonUtil.*; +import static snw.kookbc.util.JacksonUtil.*; public class GuildImpl implements Guild, Updatable, LazyLoadable { private final KBCClient client; @@ -121,16 +119,16 @@ public PageIterator> getCustomEmojis() { @Override public int getOnlineUserCount() { - JsonObject userStatus = client.getNetworkClient() + JsonNode userStatus = client.getNetworkClient() .get(String.format("%s?guild_id=%s", HttpAPIRoute.GUILD_USERS.toFullURL(), id)); - return userStatus.get("online_count").getAsInt(); + return userStatus.get("online_count").asInt(); } @Override public int getUserCount() { - JsonObject userStatus = client.getNetworkClient() + JsonNode userStatus = client.getNetworkClient() .get(String.format("%s?guild_id=%s", HttpAPIRoute.GUILD_USERS.toFullURL(), id)); - return userStatus.get("user_count").getAsInt(); + return userStatus.get("user_count").asInt(); } @Override @@ -146,17 +144,17 @@ public void setPublic(boolean value) { @Override public MuteResult getMuteStatus() { String url = String.format("%s?guild_id=%s", HttpAPIRoute.MUTE_LIST, getId()); - JsonObject object = client.getNetworkClient().get(url); + JsonNode object = client.getNetworkClient().get(url); MuteResultImpl result = new MuteResultImpl(); - for (JsonElement element : object.getAsJsonObject("mic").getAsJsonArray("user_ids")) { - String id = element.getAsString(); + for (JsonNode element : object.get("mic").get("user_ids")) { + String id = element.asText(); MuteDataImpl data = new MuteDataImpl(client.getStorage().getUser(id)); data.setInputDisabled(true); result.add(data); } - for (JsonElement element : object.getAsJsonObject("headset").getAsJsonArray("user_ids")) { - String id = element.getAsString(); + for (JsonNode element : object.get("headset").get("user_ids")) { + String id = element.asText(); MuteDataImpl resDef = (MuteDataImpl) result.getByUser(id); if (resDef == null) { resDef = new MuteDataImpl(client.getStorage().getUser(id)); @@ -252,7 +250,7 @@ public Role createRole(String s) { .put("guild_id", getId()) .put("name", s) .build(); - JsonObject res = client.getNetworkClient().post(HttpAPIRoute.ROLE_CREATE.toFullURL(), body); + JsonNode res = client.getNetworkClient().post(HttpAPIRoute.ROLE_CREATE.toFullURL(), body); Role result = client.getEntityBuilder().buildRole(this, res); client.getStorage().addRole(this, result); return result; @@ -284,8 +282,7 @@ public CustomEmoji uploadEmoji(byte[] content, String type, @Nullable String nam .post(requestBody) .addHeader("Authorization", client.getNetworkClient().getTokenWithPrefix()) .build(); - JsonObject object = JsonParser.parseString(client.getNetworkClient().call(request)).getAsJsonObject() - .getAsJsonObject("data"); + JsonNode object = parse(client.getNetworkClient().call(request)).get("data"); CustomEmoji emoji = client.getEntityBuilder().buildEmoji(object); client.getStorage().addEmoji(emoji); return emoji; @@ -314,17 +311,16 @@ public Collection getBoostInfo(int start, int end) throws IllegalArgu Validate.isTrue(start >= 0, "The paramater 'start' cannot be negative"); Validate.isTrue(end > 0, "The parameter 'end' cannot be negative"); Validate.isTrue(start < end, "The parameter 'start' cannot be greater than the parameter 'end'"); - JsonObject object = client.getNetworkClient().get( + JsonNode object = client.getNetworkClient().get( String.format("%s?guild_id=%s&start_time=%s&end_time=%s", HttpAPIRoute.GUILD_BOOST_HISTORY.toFullURL(), getId(), start, end)); Collection result = new HashSet<>(); - for (JsonElement item : object.getAsJsonArray("items")) { - JsonObject data = item.getAsJsonObject(); + for (JsonNode item : object.get("items")) { result.add( new BoostInfoImpl( - client.getStorage().getUser(data.get("user_id").getAsString()), - data.get("start_time").getAsInt(), - data.get("end_time").getAsInt())); + client.getStorage().getUser(item.get("user_id").asText()), + item.get("start_time").asInt(), + item.get("end_time").asInt())); } return Collections.unmodifiableCollection(result); } @@ -341,8 +337,8 @@ public String createInvite(int validSeconds, int validTimes) { .put("duration", validSeconds) .put("setting_times", validTimes) .build(); - JsonObject object = client.getNetworkClient().post(HttpAPIRoute.INVITE_CREATE.toFullURL(), body); - return get(object, "url").getAsString(); + JsonNode object = client.getNetworkClient().post(HttpAPIRoute.INVITE_CREATE.toFullURL(), body); + return get(object, "url").asText(); } @Override @@ -370,17 +366,17 @@ public void setAvatar(String avatarUrl) { } @Override - public synchronized void update(JsonObject data) { - final String id = getAsString(data, "id"); - final int notifyTypeId = getAsInt(data, "notify_type"); + public synchronized void update(JsonNode data) { + final String id = data.get("id").asText(); + final int notifyTypeId = data.get("notify_type").asInt(); final Supplier notifyErr = () -> "Unexpected NotifyType, got " + notifyTypeId; isTrue(Objects.equals(getId(), id), "You can't update guild by using different data"); - this.name = getAsString(data, "name"); - this.public_ = getAsBoolean(data, "enable_open"); - this.region = getAsString(data, "region"); + this.name = data.get("name").asText(); + this.public_ = data.get("enable_open").asBoolean(); + this.region = data.get("region").asText(); this.notifyType = requireNonNull(NotifyType.value(notifyTypeId), notifyErr); - this.avatarUrl = getAsString(data, "icon"); - this.master = new UserImpl(client, getAsString(data, "user_id")); + this.avatarUrl = data.get("icon").asText(); + this.master = new UserImpl(client, data.get("user_id").asText()); } @Override @@ -390,7 +386,7 @@ public boolean isCompleted() { @Override public void initialize() { - final JsonObject data = client.getNetworkClient() + final JsonNode data = client.getNetworkClient() .get(String.format("%s?guild_id=%s", HttpAPIRoute.GUILD_INFO.toFullURL(), id)); update(data); completed = true; diff --git a/src/main/java/snw/kookbc/impl/entity/RoleImpl.java b/src/main/java/snw/kookbc/impl/entity/RoleImpl.java index a634c9f3..c49b2f89 100644 --- a/src/main/java/snw/kookbc/impl/entity/RoleImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/RoleImpl.java @@ -18,7 +18,7 @@ package snw.kookbc.impl.entity; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.Permission; import snw.jkook.entity.Guild; import snw.jkook.entity.Role; @@ -30,8 +30,11 @@ import java.util.Map; import static snw.jkook.util.Validate.isTrue; -import static snw.kookbc.util.GsonUtil.getAsInt; -import static snw.kookbc.util.GsonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.getAsInt; +import static snw.kookbc.util.JacksonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.getRequiredInt; +import static snw.kookbc.util.JacksonUtil.getIntOrDefault; +import static snw.kookbc.util.JacksonUtil.getStringOrDefault; public class RoleImpl implements Role, Updatable { private final KBCClient client; @@ -164,13 +167,13 @@ public void setMentionable0(boolean mentionable) { } @Override - public synchronized void update(JsonObject data) { - isTrue(getId() == getAsInt(data, "role_id"), "You can't update the role by using different data"); - this.color = getAsInt(data, "color"); - this.position = getAsInt(data, "position"); - this.permSum = getAsInt(data, "permissions"); - this.mentionable = getAsInt(data, "mentionable") == 1; - this.hoist = getAsInt(data, "hoist") == 1; - this.name = getAsString(data, "name"); + public synchronized void update(JsonNode data) { + isTrue(getId() == getRequiredInt(data, "role_id"), "You can't update the role by using different data"); + this.color = getIntOrDefault(data, "color", 0); + this.position = getIntOrDefault(data, "position", 0); + this.permSum = getIntOrDefault(data, "permissions", 0); + this.mentionable = getIntOrDefault(data, "mentionable", 0) == 1; + this.hoist = getIntOrDefault(data, "hoist", 0) == 1; + this.name = getStringOrDefault(data, "name", "Unknown Role"); } } diff --git a/src/main/java/snw/kookbc/impl/entity/UserImpl.java b/src/main/java/snw/kookbc/impl/entity/UserImpl.java index 3630556a..9b37e57b 100644 --- a/src/main/java/snw/kookbc/impl/entity/UserImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/UserImpl.java @@ -20,9 +20,7 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import snw.jkook.Permission; @@ -55,9 +53,10 @@ import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static java.util.Objects.requireNonNull; -import static snw.kookbc.util.GsonUtil.get; +import static snw.kookbc.util.JacksonUtil.get; public class UserImpl implements User, Updatable, LazyLoadable { private final KBCClient client; @@ -106,7 +105,7 @@ public String getNickName(Guild guild) { return client.getNetworkClient() .get(String.format("%s?user_id=%s&guild_id=%s", HttpAPIRoute.USER_WHO.toFullURL(), id, guild.getId())) .get("nickname") - .getAsString(); + .asText(); } @Override @@ -149,7 +148,7 @@ public boolean isBot() { @Override public boolean isOnline() { return client.getNetworkClient().get(String.format("%s?user_id=%s", HttpAPIRoute.USER_WHO.toFullURL(), id)) - .get("online").getAsBoolean(); + .get("online").asBoolean(); } @Override @@ -160,15 +159,15 @@ public boolean isBanned() { @Override public Collection getRoles(Guild guild) { - JsonArray array = client.getNetworkClient() + JsonNode array = client.getNetworkClient() .get(String.format("%s?user_id=%s&guild_id=%s", HttpAPIRoute.USER_WHO.toFullURL(), id, guild.getId())) - .getAsJsonArray("roles"); + .get("roles"); HashSet result = new HashSet<>(); - for (JsonElement element : array) { - result.add(element.getAsInt()); + for (JsonNode element : array) { + result.add(element.asInt()); } return Collections.unmodifiableSet(result); } @@ -201,7 +200,7 @@ public String sendPrivateMessage(BaseComponent component, PrivateMessage quote) .putIfNotNull("quote", quote, Message::getId) .build(); return client.getNetworkClient().post(HttpAPIRoute.USER_CHAT_MESSAGE_CREATE.toFullURL(), body).get("msg_id") - .getAsString(); + .asText(); } @Override @@ -213,23 +212,22 @@ public PageIterator> getJoinedVoiceChannel(Guild guild) public int getIntimacy() { return client.getNetworkClient() .get(String.format("%s?user_id=%s", HttpAPIRoute.INTIMACY_INFO.toFullURL(), getId())).get("score") - .getAsInt(); + .asInt(); } @Override public IntimacyInfo getIntimacyInfo() { - JsonObject object = client.getNetworkClient() + JsonNode object = client.getNetworkClient() .get(String.format("%s?user_id=%s", HttpAPIRoute.INTIMACY_INFO.toFullURL(), getId())); - String socialImage = get(object, "img_url").getAsString(); - String socialInfo = get(object, "social_info").getAsString(); - int lastRead = get(object, "last_read").getAsInt(); - int score = get(object, "score").getAsInt(); - JsonArray socialImageListRaw = get(object, "img_list").getAsJsonArray(); + String socialImage = get(object, "img_url").asText(); + String socialInfo = get(object, "social_info").asText(); + int lastRead = get(object, "last_read").asInt(); + int score = get(object, "score").asInt(); + JsonNode socialImageListRaw = get(object, "img_list"); Collection socialImages = new ArrayList<>(socialImageListRaw.size()); - for (JsonElement element : socialImageListRaw) { - JsonObject obj = element.getAsJsonObject(); - String id = obj.get("id").getAsString(); - String url = obj.get("url").getAsString(); + for (JsonNode element : socialImageListRaw) { + String id = element.get("id").asText(); + String url = element.get("url").asText(); socialImages.add( new SocialImageImpl(id, url)); } @@ -347,17 +345,32 @@ public void setVipAvatarUrl(String vipAvatarUrl) { } @Override - public void update(JsonObject data) { - Validate.isTrue(Objects.equals(getId(), get(data, "id").getAsString()), + public synchronized void update(JsonNode data) { + Validate.isTrue(Objects.equals(getId(), data.get("id").asText()), "You can't update user by using different data"); - synchronized (this) { - name = get(data, "username").getAsString(); - bot = get(data, "bot").getAsBoolean(); - avatarUrl = get(data, "avatar").getAsString(); - vipAvatarUrl = get(data, "vip_avatar").getAsString(); - identify = get(data, "identify_num").getAsInt(); - ban = get(data, "status").getAsInt() == 10; - vip = get(data, "is_vip").getAsBoolean(); + + // 安全获取字段,某些 API 返回的用户数据可能不完整 + // 注意: 方法已经是 synchronized,无需内部再加锁 + if (data.has("username")) { + name = data.get("username").asText(); + } + if (data.has("bot")) { + bot = data.get("bot").asBoolean(); + } + if (data.has("avatar")) { + avatarUrl = data.get("avatar").asText(); + } + if (data.has("vip_avatar")) { + vipAvatarUrl = data.get("vip_avatar").asText(); + } + if (data.has("identify_num")) { + identify = data.get("identify_num").asInt(); + } + if (data.has("status")) { + ban = data.get("status").asInt() == 10; + } + if (data.has("is_vip")) { + vip = data.get("is_vip").asBoolean(); } } @@ -368,7 +381,7 @@ public boolean isCompleted() { @Override public void initialize() { - final JsonObject data = client.getNetworkClient().get( + final JsonNode data = client.getNetworkClient().get( String.format("%s?user_id=%s", HttpAPIRoute.USER_WHO.toFullURL(), id)); update(data); completed = true; @@ -405,30 +418,36 @@ public void recalculatePermissions() { } public Map calculateChannel(Channel channel) { - Map result = new HashMap<>(); Collection cached = cacheRoleIds.asMap().get(id); if (cached == null) { cacheRoleIds.put(id, cached = getRoles(channel.getGuild())); } - Collection userRoleIds = new HashSet<>(cached); + final Collection userRoleIds = new HashSet<>(cached); HashSet guildRoles = new HashSet<>(); List cachedGuildRoles = client.getStorage().getRoles(channel.getGuild()); if (!cachedGuildRoles.isEmpty()) { guildRoles.addAll(cachedGuildRoles); } - for (Permission value : Permission.values()) { - boolean calculated = false; - try { - calculated = calculateDefaultPerms(value, channel, userRoleIds, guildRoles); - } catch (BadResponseException e) { - this.client.getCore().getLogger().error("Error occurred while calculating built-in permissions", e); - break; - } catch (Exception e) { - this.client.getCore().getLogger().error("Error occurred while calculating built-in permissions", e); - } - result.put(value, calculated); - } - return result; + final Collection finalGuildRoles = guildRoles; + + // 性能优化:使用虚拟线程并行计算所有权限 + // Permission.values() 通常有多个权限,并行计算可大幅提升性能 + return Arrays.stream(Permission.values()) + .parallel() // 启用并行流,自动使用虚拟线程池 + .collect(Collectors.toConcurrentMap( + perm -> perm, + perm -> { + try { + return calculateDefaultPerms(perm, channel, userRoleIds, finalGuildRoles); + } catch (BadResponseException e) { + client.getCore().getLogger().error("计算内置权限时发生错误", e); + return false; + } catch (Exception e) { + client.getCore().getLogger().error("计算内置权限时发生错误", e); + return false; + } + } + )); } public boolean calculateDefaultPerms(Permission permission, Channel channel, Collection userRoleIds, Collection guildRoles) { diff --git a/src/main/java/snw/kookbc/impl/entity/builder/CardBuilder.java b/src/main/java/snw/kookbc/impl/entity/builder/CardBuilder.java index 9f575dd4..a6777413 100644 --- a/src/main/java/snw/kookbc/impl/entity/builder/CardBuilder.java +++ b/src/main/java/snw/kookbc/impl/entity/builder/CardBuilder.java @@ -18,52 +18,90 @@ package snw.kookbc.impl.entity.builder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.message.component.card.CardComponent; import snw.jkook.message.component.card.MultipleCardComponent; import snw.jkook.util.Validate; -import snw.kookbc.util.GsonUtil; +import snw.kookbc.util.JacksonCardUtil; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Objects; -import static snw.kookbc.util.GsonUtil.get; - -// Just for (de)serialize the CardMessages. +/** + * Jackson-based高性能卡片消息构建器 + * 提供null-safe的卡片消息序列化/反序列化功能 + */ public class CardBuilder { - public static MultipleCardComponent buildCard(JsonArray array) { - List components = new LinkedList<>(); - for (JsonElement jsonElement : array) { - components.add(buildCard(jsonElement.getAsJsonObject())); + // ===== Jackson版本方法(推荐使用)===== + + /** + * 从JsonNode数组构建多卡片组件 + * @param arrayNode Jackson JsonNode数组 + * @return MultipleCardComponent + */ + public static MultipleCardComponent buildCardArray(JsonNode arrayNode) { + Validate.notNull(arrayNode, "JsonNode array cannot be null"); + Validate.isTrue(arrayNode.isArray(), "JsonNode must be an array"); + + List components = new ArrayList<>(); + for (JsonNode jsonElement : arrayNode) { + if (jsonElement.isObject()) { + components.add(buildCardObject(jsonElement)); + } } return new MultipleCardComponent(components); } - public static CardComponent buildCard(JsonObject object) { - Validate.isTrue(Objects.equals(get(object, "type").getAsString(), "card"), "The provided element is not a card."); - return GsonUtil.CARD_GSON.fromJson(object, CardComponent.class); - } + /** + * 从JsonNode对象构建单个卡片组件 + * @param objectNode Jackson JsonNode对象 + * @return CardComponent + */ + public static CardComponent buildCardObject(JsonNode objectNode) { + Validate.notNull(objectNode, "JsonNode object cannot be null"); + Validate.isTrue(objectNode.isObject(), "JsonNode must be an object"); + Validate.isTrue(Objects.equals(JacksonCardUtil.getStringOrDefault(objectNode, "type", ""), "card"), + "The provided element is not a card."); - public static JsonArray serialize(CardComponent component) { - JsonArray result = new JsonArray(); - result.add(serialize0(component)); - return result; + return JacksonCardUtil.fromJson(objectNode, CardComponent.class); } - public static JsonArray serialize(MultipleCardComponent component) { - JsonArray array = new JsonArray(); - for (CardComponent card : component.getComponents()) { - array.add(serialize0(card)); + /** + * 从JSON字符串构建卡片组件 + * @param jsonString JSON字符串 + * @return CardComponent或MultipleCardComponent + */ + public static Object buildCard(String jsonString) { + JsonNode root = JacksonCardUtil.parse(jsonString); + if (root.isArray()) { + return buildCardArray(root); + } else if (root.isObject()) { + return buildCardObject(root); + } else { + throw new IllegalArgumentException("JSON must be object or array"); } - return array; } - public static JsonObject serialize0(CardComponent component) { - return GsonUtil.CARD_GSON.toJsonTree(component).getAsJsonObject(); + // ===== 序列化方法 ===== + + /** + * 序列化单个卡片组件为JsonNode + * @param component CardComponent + * @return JsonNode + */ + public static JsonNode serializeToNode(CardComponent component) { + return JacksonCardUtil.toJsonNode(component); + } + + /** + * 序列化多卡片组件为JsonNode + * @param component MultipleCardComponent + * @return JsonNode(数组类型) + */ + public static JsonNode serializeToNode(MultipleCardComponent component) { + return JacksonCardUtil.toJsonNode(component); } } diff --git a/src/main/java/snw/kookbc/impl/entity/builder/EntityBuildUtil.java b/src/main/java/snw/kookbc/impl/entity/builder/EntityBuildUtil.java index 0a56bad2..6950e7ea 100644 --- a/src/main/java/snw/kookbc/impl/entity/builder/EntityBuildUtil.java +++ b/src/main/java/snw/kookbc/impl/entity/builder/EntityBuildUtil.java @@ -19,15 +19,11 @@ package snw.kookbc.impl.entity.builder; import static snw.jkook.util.Validate.notNull; -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.getAsInt; import java.util.Collection; import java.util.concurrent.ConcurrentLinkedQueue; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import org.jetbrains.annotations.Nullable; import snw.jkook.entity.Guild; @@ -35,75 +31,110 @@ import snw.jkook.entity.channel.Channel; import snw.jkook.entity.channel.NonCategoryChannel; import snw.kookbc.impl.KBCClient; +import snw.kookbc.util.JacksonUtil; import snw.kookbc.impl.entity.channel.CategoryImpl; import snw.kookbc.impl.entity.channel.NonCategoryChannelImpl; import snw.kookbc.impl.entity.channel.TextChannelImpl; +import snw.kookbc.impl.entity.channel.ThreadChannelImpl; import snw.kookbc.impl.entity.channel.VoiceChannelImpl; public class EntityBuildUtil { - public static Collection parseRPO(JsonObject object) { - JsonArray array = get(object, "permission_overwrites").getAsJsonArray(); + @Nullable + public static Channel parseChannel(KBCClient client, String id, int type) { + switch (type) { + case 0: + return new CategoryImpl(client, id); + case 1: + return new TextChannelImpl(client, id); + case 2: + return new VoiceChannelImpl(client, id); + case 4: + // 帖子频道 (Thread Channel) + return new ThreadChannelImpl(client, id); + default: + return null; + } + } + + // ===== Jackson API - 安全版本(处理不完整JSON数据) ===== + + /** + * 解析角色权限覆写 (Jackson版本,安全处理不完整JSON) + */ + public static Collection parseRPO(JsonNode node) { + JsonNode array = JacksonUtil.getArrayOrNull(node, "permission_overwrites"); Collection rpo = new ConcurrentLinkedQueue<>(); - for (JsonElement element : array) { - JsonObject orpo = element.getAsJsonObject(); - rpo.add( - new Channel.RolePermissionOverwrite( - orpo.get("role_id").getAsInt(), - orpo.get("allow").getAsInt(), - orpo.get("deny").getAsInt())); + + if (array != null && array.isArray()) { + for (JsonNode element : array) { + if (element != null && element.isObject()) { + int roleId = JacksonUtil.getIntOrDefault(element, "role_id", 0); + int allow = JacksonUtil.getIntOrDefault(element, "allow", 0); + int deny = JacksonUtil.getIntOrDefault(element, "deny", 0); + + rpo.add(new Channel.RolePermissionOverwrite(roleId, allow, deny)); + } + } } return rpo; } - public static Collection parseUPO(KBCClient client, JsonObject object) { - JsonArray array = get(object, "permission_users").getAsJsonArray(); + /** + * 解析用户权限覆写 (Jackson版本,安全处理不完整JSON) + */ + public static Collection parseUPO(KBCClient client, JsonNode node) { + JsonNode array = JacksonUtil.getArrayOrNull(node, "permission_users"); Collection upo = new ConcurrentLinkedQueue<>(); - for (JsonElement element : array) { - JsonObject oupo = element.getAsJsonObject(); - JsonObject rawUser = oupo.getAsJsonObject("user"); - upo.add( - new Channel.UserPermissionOverwrite( - client.getStorage().getUser(rawUser.get("id").getAsString(), rawUser), - oupo.get("allow").getAsInt(), - oupo.get("deny").getAsInt())); + + if (array != null && array.isArray()) { + for (JsonNode element : array) { + if (element != null && element.isObject()) { + JsonNode rawUser = JacksonUtil.getObjectOrNull(element, "user"); + if (rawUser != null) { + String userId = JacksonUtil.getRequiredString(rawUser, "id"); + int allow = JacksonUtil.getIntOrDefault(element, "allow", 0); + int deny = JacksonUtil.getIntOrDefault(element, "deny", 0); + + // 直接使用Jackson JsonNode + upo.add(new Channel.UserPermissionOverwrite( + client.getStorage().getUser(userId, rawUser), + allow, deny)); + } + } + } } return upo; } - public static NotifyType parseNotifyType(JsonObject object) { - Guild.NotifyType type = null; - int rawNotifyType = getAsInt(object, "notify_type"); + /** + * 解析通知类型 (Jackson版本,安全处理不完整JSON) + */ + public static NotifyType parseNotifyType(JsonNode node) { + int rawNotifyType = JacksonUtil.getIntOrDefault(node, "notify_type", 0); + for (Guild.NotifyType value : Guild.NotifyType.values()) { if (value.getValue() == rawNotifyType) { - type = value; - break; + return value; } } - notNull(type, String.format("Internal Error: Unexpected NotifyType from remote: %s", rawNotifyType)); - return type; + + // 如果找不到匹配的通知类型,使用默认值而不是抛出异常 + return Guild.NotifyType.values()[0]; // 使用第一个枚举值作为默认 } - public static Guild parseEmojiGuild(String id, KBCClient client, JsonObject object) { + /** + * 解析表情所属服务器 (Jackson版本,安全处理不完整JSON) + */ + public static Guild parseEmojiGuild(String id, KBCClient client, JsonNode node) { Guild guild = null; - if (id.contains("/")) { - guild = client.getStorage().getGuild(id.substring(0, id.indexOf("/"))); + if (id != null && id.contains("/")) { + String guildId = id.substring(0, id.indexOf("/")); + if (!guildId.isEmpty()) { + guild = client.getStorage().getGuild(guildId); + } } return guild; } - @Nullable - public static Channel parseChannel(KBCClient client, String id, int type) { - switch (type) { - case 0: - return new CategoryImpl(client, id); - case 1: - return new TextChannelImpl(client, id); - case 2: - return new VoiceChannelImpl(client, id); - default: - return null; - } - } - -} +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/entity/builder/EntityBuilder.java b/src/main/java/snw/kookbc/impl/entity/builder/EntityBuilder.java index 7c10d871..15150b46 100644 --- a/src/main/java/snw/kookbc/impl/entity/builder/EntityBuilder.java +++ b/src/main/java/snw/kookbc/impl/entity/builder/EntityBuilder.java @@ -22,13 +22,12 @@ import static snw.kookbc.impl.entity.builder.EntityBuildUtil.parseNotifyType; import static snw.kookbc.impl.entity.builder.EntityBuildUtil.parseRPO; import static snw.kookbc.impl.entity.builder.EntityBuildUtil.parseUPO; -import static snw.kookbc.util.GsonUtil.getAsBoolean; -import static snw.kookbc.util.GsonUtil.getAsInt; -import static snw.kookbc.util.GsonUtil.getAsString; +// Jackson utils for safe field access +import static snw.kookbc.util.JacksonUtil.*; import java.util.Collection; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.Game; @@ -46,6 +45,7 @@ import snw.kookbc.impl.entity.UserImpl; import snw.kookbc.impl.entity.channel.CategoryImpl; import snw.kookbc.impl.entity.channel.TextChannelImpl; +import snw.kookbc.impl.entity.channel.ThreadChannelImpl; import snw.kookbc.impl.entity.channel.VoiceChannelImpl; // The class for building entities. @@ -56,88 +56,174 @@ public EntityBuilder(KBCClient client) { this.client = client; } - public User buildUser(JsonObject s) { - final String id = getAsString(s, "id"); - final boolean bot = getAsBoolean(s, "bot"); - final String name = getAsString(s, "username"); - final int identify = getAsInt(s, "identify_num"); - final boolean ban = getAsInt(s, "status") == 10; - final boolean vip = getAsBoolean(s, "is_vip"); - final String avatarUrl = getAsString(s, "avatar"); - final String vipAvatarUrl = getAsString(s, "vip_avatar"); + // ===== Jackson API - 高性能版本(处理Kook不完整JSON数据)===== + + /** + * 构建User对象 (Jackson版本,安全处理不完整JSON) + * 处理Kook可能发送不完整JSON的情况 + */ + public User buildUser(JsonNode node) { + // 必需字段 - 如果不存在会抛出异常 + final String id = getRequiredString(node, "id"); + + // 可选字段 - 提供合适的默认值 + final boolean bot = getBooleanOrDefault(node, "bot", false); + final String name = getStringOrDefault(node, "username", "Unknown User"); + final int identify = getIntOrDefault(node, "identify_num", 0); + + // 状态字段 - 默认为正常状态(非封禁) + final int status = getIntOrDefault(node, "status", 0); + final boolean ban = (status == 10); + + // VIP状态默认为false + final boolean vip = getBooleanOrDefault(node, "is_vip", false); + + // 头像URL - 提供空字符串作为默认值 + final String avatarUrl = getStringOrDefault(node, "avatar", ""); + final String vipAvatarUrl = getStringOrDefault(node, "vip_avatar", ""); + return new UserImpl(client, id, bot, name, identify, ban, vip, avatarUrl, vipAvatarUrl); } - public Guild buildGuild(JsonObject object) { - final String id = getAsString(object, "id"); - final NotifyType notifyType = parseNotifyType(object); - final User master = client.getStorage().getUser(getAsString(object, "master_id")); - final String name = getAsString(object, "name"); - final boolean public_ = getAsBoolean(object, "enable_open"); - final String region = getAsString(object, "region"); - final String avatarUrl = getAsString(object, "icon"); + /** + * 构建Guild对象 (Jackson版本,安全处理不完整JSON) + * 处理Kook可能发送不完整JSON的情况 + */ + public Guild buildGuild(JsonNode node) { + // 必需字段 + final String id = getRequiredString(node, "id"); + + // 通知类型 - 使用EntityBuildUtil解析 + final NotifyType notifyType = parseNotifyType(node); + + // 服务器主人 - 必需字段 + final String masterId = getRequiredString(node, "master_id"); + final User master = client.getStorage().getUser(masterId); + + // 服务器基本信息 + final String name = getStringOrDefault(node, "name", "Unknown Guild"); + final boolean public_ = getBooleanOrDefault(node, "enable_open", false); + final String region = getStringOrDefault(node, "region", "unknown"); + final String avatarUrl = getStringOrDefault(node, "icon", ""); + return new GuildImpl(client, id, notifyType, master, name, public_, region, avatarUrl); } - public Channel buildChannel(JsonObject object) { - final String id = getAsString(object, "id"); - final String name = getAsString(object, "name"); - final Guild guild = client.getStorage().getGuild(getAsString(object, "guild_id")); - final User master = client.getStorage().getUser(getAsString(object, "user_id")); - final boolean isPermSync = getAsInt(object, "permission_sync") != 0; - final int level = getAsInt(object, "level"); - final Collection rpo = parseRPO(object); - final Collection upo = parseUPO(client, object); - if (getAsBoolean(object, "is_category")) { + /** + * 构建Channel对象 (Jackson版本,安全处理不完整JSON) + * 处理Kook可能发送不完整JSON的情况 + */ + public Channel buildChannel(JsonNode node) { + // 必需字段 + final String id = getRequiredString(node, "id"); + final String name = getStringOrDefault(node, "name", "Unknown Channel"); + + // 所属服务器和创建者 + final String guildId = getRequiredString(node, "guild_id"); + final Guild guild = client.getStorage().getGuild(guildId); + + final String userId = getRequiredString(node, "user_id"); + final User master = client.getStorage().getUser(userId); + + // 权限同步和级别 + final boolean isPermSync = getIntOrDefault(node, "permission_sync", 0) != 0; + final int level = getIntOrDefault(node, "level", 0); + + // 权限覆写 - 使用EntityBuildUtil安全解析 + final Collection rpo = parseRPO(node); + final Collection upo = parseUPO(client, node); + + // 检查是否为分类频道 + if (getBooleanOrDefault(node, "is_category", false)) { return new CategoryImpl(client, id, master, guild, isPermSync, rpo, upo, level, name); } - final String parentId = getAsString(object, "parent_id"); + + // 处理父级分类 + final String parentId = getStringOrDefault(node, "parent_id", ""); final Boolean needCategory = "".equals(parentId) || "0".equals(parentId); final Category parent = needCategory ? null : new CategoryImpl(client, parentId); - switch (getAsInt(object, "type")) { + + // 根据频道类型创建对应对象 + final int channelType = getIntOrDefault(node, "type", 1); // 默认为文本频道 + + switch (channelType) { case 1: { - final int chatLimitTime = getAsInt(object, "slow_mode"); - final String topic = getAsString(object, "topic"); + // 文本频道 + final int chatLimitTime = getIntOrDefault(node, "slow_mode", 0); + final String topic = getStringOrDefault(node, "topic", ""); return new TextChannelImpl(client, id, master, guild, isPermSync, parent, name, rpo, upo, level, chatLimitTime, topic); } case 2: { - final int chatLimitTime = getAsInt(object, "slow_mode"); - final boolean hasPassword = object.has("has_password") && getAsBoolean(object, "has_password"); - final int size = getAsInt(object, "limit_amount"); - final int quality = getAsInt(object, "voice_quality"); + // 语音频道 + final int chatLimitTime = getIntOrDefault(node, "slow_mode", 0); + final boolean hasPassword = hasNonNull(node, "has_password") && getBooleanOrDefault(node, "has_password", false); + final int size = getIntOrDefault(node, "limit_amount", 99); + final int quality = getIntOrDefault(node, "voice_quality", 1); return new VoiceChannelImpl(client, id, master, guild, isPermSync, parent, name, rpo, upo, level, hasPassword, size, quality, chatLimitTime); } + case 4: { + // 帖子频道 (Thread Channel) + final int chatLimitTime = getIntOrDefault(node, "slow_mode", 0); + return new ThreadChannelImpl(client, id, master, guild, isPermSync, parent, name, rpo, upo, level, + chatLimitTime); + } default: { - final String msg = "We can't construct the Channel using given information. Is your information correct?"; + final String msg = "We can't construct the Channel using given information. Unknown channel type: " + channelType; throw new IllegalArgumentException(msg); } } } - public Role buildRole(Guild guild, JsonObject object) { - final int id = getAsInt(object, "role_id"); - final String name = getAsString(object, "name"); - final int color = getAsInt(object, "color"); - final int pos = getAsInt(object, "position"); - final boolean hoist = getAsInt(object, "hoist") == 1; - final boolean mentionable = getAsInt(object, "mentionable") == 1; - final int permissions = getAsInt(object, "permissions"); + /** + * 构建Role对象 (Jackson版本,安全处理不完整JSON) + * 处理Kook可能发送不完整JSON的情况 + */ + public Role buildRole(Guild guild, JsonNode node) { + // 必需字段 + final int id = getRequiredInt(node, "role_id"); + final String name = getStringOrDefault(node, "name", "Unknown Role"); + + // 外观属性 + final int color = getIntOrDefault(node, "color", 0); // 默认无颜色 + final int pos = getIntOrDefault(node, "position", 0); // 默认位置0 + + // 显示设置 (0/1值转换为布尔值) + final boolean hoist = getIntOrDefault(node, "hoist", 0) == 1; + final boolean mentionable = getIntOrDefault(node, "mentionable", 0) == 1; + + // 权限位掩码 + final int permissions = getIntOrDefault(node, "permissions", 0); + return new RoleImpl(client, guild, id, color, pos, permissions, mentionable, hoist, name); } - public CustomEmoji buildEmoji(JsonObject object) { - final String id = getAsString(object, "id"); - final Guild guild = parseEmojiGuild(id, client, object); - final String name = getAsString(object, "name"); + /** + * 构建CustomEmoji对象 (Jackson版本,安全处理不完整JSON) + * 处理Kook可能发送不完整JSON的情况 + */ + public CustomEmoji buildEmoji(JsonNode node) { + // 必需字段 + final String id = getRequiredString(node, "id"); + final String name = getStringOrDefault(node, "name", "Unknown Emoji"); + + // 解析表情所属服务器 - 使用EntityBuildUtil + final Guild guild = parseEmojiGuild(id, client, node); + return new CustomEmojiImpl(client, id, guild, name); } - public Game buildGame(JsonObject object) { - final int id = getAsInt(object, "id"); - final String name = getAsString(object, "name"); - final String icon = getAsString(object, "icon"); + /** + * 构建Game对象 (Jackson版本,安全处理不完整JSON) + * 处理Kook可能发送不完整JSON的情况 + */ + public Game buildGame(JsonNode node) { + // 必需字段 + final int id = getRequiredInt(node, "id"); + final String name = getStringOrDefault(node, "name", "Unknown Game"); + final String icon = getStringOrDefault(node, "icon", ""); + return new GameImpl(client, id, name, icon); } } diff --git a/src/main/java/snw/kookbc/impl/entity/builder/MessageBuilder.java b/src/main/java/snw/kookbc/impl/entity/builder/MessageBuilder.java index f0135d97..b62d4946 100644 --- a/src/main/java/snw/kookbc/impl/entity/builder/MessageBuilder.java +++ b/src/main/java/snw/kookbc/impl/entity/builder/MessageBuilder.java @@ -18,8 +18,9 @@ package snw.kookbc.impl.entity.builder; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import snw.kookbc.util.JacksonUtil; +import snw.kookbc.util.JacksonCardUtil; import snw.jkook.entity.User; import snw.jkook.entity.channel.TextChannel; import snw.jkook.entity.channel.VoiceChannel; @@ -33,13 +34,14 @@ import snw.jkook.message.component.card.Theme; import snw.jkook.message.component.card.module.FileModule; import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.entity.builder.CardBuilder; import snw.kookbc.impl.entity.channel.TextChannelImpl; import snw.kookbc.impl.entity.channel.VoiceChannelImpl; import snw.kookbc.impl.message.*; import java.util.NoSuchElementException; -import static snw.kookbc.util.GsonUtil.*; +import static snw.kookbc.util.JacksonUtil.*; public class MessageBuilder { private final KBCClient client; @@ -58,11 +60,11 @@ public static Object[] serialize(BaseComponent component) { } else if (component instanceof TextComponent) { return new Object[] { 1, component.toString() }; } else if (component instanceof CardComponent) { - return new Object[] { 10, CARD_GSON.toJson(CardBuilder.serialize((CardComponent) component)) }; + return new Object[] { 10, JacksonCardUtil.toJson(component) }; } else if (component instanceof MultipleCardComponent) { - return new Object[]{10, CARD_GSON.toJson(component)}; + return new Object[]{10, JacksonCardUtil.toJson(component)}; } else if (component instanceof TemplateMessage) { - return new Object[]{ ((TemplateMessage) component).getType(), CARD_GSON.toJson(component) }; + return new Object[]{ ((TemplateMessage) component).getType(), JacksonCardUtil.toJson(component) }; } else if (component instanceof FileComponent) { FileComponent fileComponent = (FileComponent) component; MultipleCardComponent fileCard; @@ -85,82 +87,81 @@ public static Object[] serialize(BaseComponent component) { throw new RuntimeException("Unsupported component"); } - public PrivateMessage buildPrivateMessage(JsonObject object) { - final String id = getAsString(object, "msg_id"); - final JsonObject extra = getAsJsonObject(object, "extra"); + private ChannelMessageImpl buildMessage(String id, User author, BaseComponent component, long timeStamp, + Message message, String targetId, int channelType) { + if (channelType == CHANNEL_TYPE_TEXT) { + final TextChannel channel = new TextChannelImpl(client, targetId); + return new TextChannelMessageImpl(client, id, author, component, timeStamp, message, channel); + } else if (channelType == CHANNEL_TYPE_VOICE) { + final VoiceChannel channel = new VoiceChannelImpl(client, targetId); + return new VoiceChannelMessageImpl(client, id, author, component, timeStamp, message, channel); + } + throw new RuntimeException("We can not found channel type: " + channelType); + } + + // ===== Jackson API - 高性能版本 ===== + + public PrivateMessage buildPrivateMessage(JsonNode node) { + final String id = JacksonUtil.get(node, "msg_id").asText(); + final JsonNode extra = JacksonUtil.get(node, "extra"); final User author = getAuthor(extra); - final long timeStamp = getAsLong(object, "msg_timestamp"); + final long timeStamp = JacksonUtil.get(node, "msg_timestamp").asLong(); final Message quote = getQuote(extra); - return new PrivateMessageImpl(client, id, author, buildComponent(object), timeStamp, quote); + return new PrivateMessageImpl(client, id, author, buildComponent(node), timeStamp, quote); } - public ChannelMessage buildChannelMessage(JsonObject object) { - final String id = getAsString(object, "msg_id"); - final JsonObject extra = getAsJsonObject(object, "extra"); + public ChannelMessage buildChannelMessage(JsonNode node) { + final String id = JacksonUtil.get(node, "msg_id").asText(); + final JsonNode extra = JacksonUtil.get(node, "extra"); final User author = getAuthor(extra); - final long timeStamp = getAsLong(object, "msg_timestamp"); + final long timeStamp = JacksonUtil.get(node, "msg_timestamp").asLong(); final Message quote = getQuote(extra); - final String targetId = getAsString(object, "target_id"); - final int channelType = getAsInt(extra, "channel_type"); - return buildMessage(id, author, buildComponent(object), timeStamp, quote, targetId, channelType); + final String targetId = JacksonUtil.get(node, "target_id").asText(); + final int channelType = JacksonUtil.get(extra, "channel_type").asInt(); + return buildMessage(id, author, buildComponent(node), timeStamp, quote, targetId, channelType); } - private User getAuthor(JsonObject extra) { - final JsonObject authorObj = getAsJsonObject(extra, "author"); - final String id = getAsString(authorObj, "id"); - return client.getStorage().getUser(id, authorObj); + private User getAuthor(JsonNode extra) { + final JsonNode authorObj = JacksonUtil.get(extra, "author"); + final String id = authorObj.get("id").asText(); + return client.getStorage().getUser(id, authorObj); // 直接使用Jackson版本 } - private Message getQuote(JsonObject extra) { + private Message getQuote(JsonNode extra) { try { - final String quoteId = getAsString(getAsJsonObject(extra, "quote"), "rong_id"); + final JsonNode quoteNode = extra.get("quote"); + if (quoteNode == null || quoteNode.isNull()) { + return null; + } + final String quoteId = quoteNode.get("rong_id").asText(); Message quote = client.getStorage().getMessage(quoteId); if (quote == null) { quote = client.getCore().getHttpAPI().getChannelMessage(quoteId); } return quote; - } catch (NoSuchElementException e) { + } catch (Exception e) { return null; } } - private ChannelMessageImpl buildMessage(String id, User author, BaseComponent component, long timeStamp, - Message message, String targetId, int channelType) { - if (channelType == CHANNEL_TYPE_TEXT) { - final TextChannel channel = new TextChannelImpl(client, targetId); - return new TextChannelMessageImpl(client, id, author, component, timeStamp, message, channel); - } else if (channelType == CHANNEL_TYPE_VOICE) { - final VoiceChannel channel = new VoiceChannelImpl(client, targetId); - return new VoiceChannelMessageImpl(client, id, author, component, timeStamp, message, channel); - } - throw new RuntimeException("We can not found channel type: " + channelType); - } - - public Message buildQuote(JsonObject object) { - if (object == null) { - return null; - } - final String id = getAsString(object, "rong_id"); // WARNING: this is not described in Kook developer document, - // maybe unavailable in the future - final BaseComponent component = buildComponent(object); - final long timeStamp = getAsLong(object, "create_at"); - final JsonObject rawUser = getAsJsonObject(object, "author"); - final User author = client.getStorage().getUser(getAsString(rawUser, "id"), rawUser); - return new QuoteImpl(component, id, author, timeStamp); - } - - public BaseComponent buildComponent(JsonObject object) { + public BaseComponent buildComponent(JsonNode node) { // we use text channel message format - String content = getAsString(object, "content"); - switch (getAsInt(object, "type")) { + String content = JacksonUtil.get(node, "content").asText(); + switch (JacksonUtil.get(node, "type").asInt()) { case 9: return new MarkdownComponent(content); case 10: - MultipleCardComponent card = CardBuilder.buildCard(JsonParser.parseString(content).getAsJsonArray()); - if (card.getComponents().size() == 1) { - return card.getComponents().get(0); + // 直接使用Jackson版本的CardBuilder方法 + JsonNode contentNode = JacksonUtil.parse(content); + if (contentNode.isArray()) { + MultipleCardComponent card = CardBuilder.buildCardArray(contentNode); + if (card.getComponents().size() == 1) { + return card.getComponents().get(0); + } else { + return card; + } } else { - return card; + return CardBuilder.buildCardObject(contentNode); } case 2: case 3: @@ -169,15 +170,15 @@ public BaseComponent buildComponent(JsonObject object) { String title = ""; int size = -1; FileComponent.Type type = FileComponent.Type.FILE; - if (object.has("extra")) { // standard component format - JsonObject attachment = object.getAsJsonObject("extra").getAsJsonObject("attachments"); - url = getAsString(attachment, "url"); - title = getAsString(attachment, "name"); + if (node.has("extra")) { // standard component format + JsonNode attachment = node.get("extra").get("attachments"); + url = attachment.get("url").asText(); + title = attachment.get("name").asText(); // -1 for image files, because Kook does not provide size for image files. - if (attachment.has("size") && !attachment.get("size").isJsonNull()) { - size = getAsInt(attachment, "size"); + if (attachment.has("size") && !attachment.get("size").isNull()) { + size = attachment.get("size").asInt(); } - String ftype = getAsString(attachment, "type"); + String ftype = attachment.get("type").asText(); switch (ftype) { case "file": break; @@ -188,7 +189,7 @@ public BaseComponent buildComponent(JsonObject object) { type = FileComponent.Type.IMAGE; break; default: - if (getAsString(attachment, "file_type").startsWith("audio")) { + if (attachment.get("file_type").asText().startsWith("audio")) { type = FileComponent.Type.AUDIO; } else { throw new RuntimeException("Unexpected file_type"); @@ -204,4 +205,17 @@ public BaseComponent buildComponent(JsonObject object) { } throw new RuntimeException("Unknown component type"); } + + public Message buildQuote(JsonNode node) { + if (node == null || node.isNull()) { + return null; + } + final String id = node.get("rong_id").asText(); // WARNING: this is not described in Kook developer document, + // maybe unavailable in the future + final BaseComponent component = buildComponent(node); + final long timeStamp = node.get("create_at").asLong(); + final JsonNode rawUser = node.get("author"); + final User author = client.getStorage().getUser(rawUser.get("id").asText(), rawUser); // 直接使用Jackson版本 + return new QuoteImpl(component, id, author, timeStamp); + } } diff --git a/src/main/java/snw/kookbc/impl/entity/channel/CategoryImpl.java b/src/main/java/snw/kookbc/impl/entity/channel/CategoryImpl.java index 791d4aa8..c569e5bd 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/CategoryImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/CategoryImpl.java @@ -18,14 +18,14 @@ package snw.kookbc.impl.entity.channel; -import static snw.kookbc.util.GsonUtil.getAsJsonArray; +import static snw.kookbc.util.JacksonUtil.get; +import static snw.kookbc.util.JacksonUtil.getAsJsonArray; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.User; @@ -48,15 +48,13 @@ public CategoryImpl(KBCClient client, String id, User master, Guild guild, boole @Override public Collection getChannels() { - final JsonObject object = client.getNetworkClient() + final JsonNode object = client.getNetworkClient() .get(HttpAPIRoute.CHANNEL_INFO.toFullURL() + "?target_id=" + getId() + "&need_children=true"); update(object); final Collection channels = new LinkedList<>(); - getAsJsonArray(object, "children") - .asList() - .stream() - .map(JsonElement::getAsString) - .forEach(id -> channels.add(client.getStorage().getChannel(id))); + get(object, "children") + .elements() + .forEachRemaining(element -> channels.add(client.getStorage().getChannel(element.asText()))); return Collections.unmodifiableCollection(channels); } diff --git a/src/main/java/snw/kookbc/impl/entity/channel/ChannelImpl.java b/src/main/java/snw/kookbc/impl/entity/channel/ChannelImpl.java index 485d1a6d..7c07c7e2 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/ChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/ChannelImpl.java @@ -18,7 +18,7 @@ package snw.kookbc.impl.entity.channel; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import org.jetbrains.annotations.Nullable; import snw.jkook.Permission; import snw.jkook.entity.Guild; @@ -29,6 +29,7 @@ import snw.kookbc.impl.network.HttpAPIRoute; import snw.kookbc.interfaces.LazyLoadable; import snw.kookbc.interfaces.Updatable; +import snw.kookbc.util.JacksonUtil; import snw.kookbc.util.MapBuilder; import java.util.Collection; @@ -39,8 +40,8 @@ import static snw.jkook.util.Validate.isTrue; import static snw.kookbc.impl.entity.builder.EntityBuildUtil.parseRPO; import static snw.kookbc.impl.entity.builder.EntityBuildUtil.parseUPO; -import static snw.kookbc.util.GsonUtil.getAsInt; -import static snw.kookbc.util.GsonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.getAsInt; +import static snw.kookbc.util.JacksonUtil.getAsString; public abstract class ChannelImpl implements Channel, Updatable, LazyLoadable { protected final KBCClient client; @@ -319,12 +320,11 @@ public User getMaster() { return master; } - @Override - public synchronized void update(JsonObject data) { - isTrue(Objects.equals(getId(), getAsString(data, "id")), "You can't update channel by using different data"); - this.name = getAsString(data, "name"); - this.permSync = getAsInt(data, "permission_sync") != 0; - this.guild = client.getStorage().getGuild(getAsString(data, "guild_id")); + public synchronized void update(JsonNode data) { + isTrue(Objects.equals(getId(), JacksonUtil.get(data, "id").asText()), "You can't update channel by using different data"); + this.name = JacksonUtil.get(data, "name").asText(); + this.permSync = JacksonUtil.get(data, "permission_sync").asInt() != 0; + this.guild = client.getStorage().getGuild(JacksonUtil.get(data, "guild_id").asText()); this.rpo = parseRPO(data); this.upo = parseUPO(client, data); @@ -344,7 +344,7 @@ public boolean isCompleted() { @Override public void initialize() { - final JsonObject data = client.getNetworkClient() + final JsonNode data = client.getNetworkClient() .get(String.format("%s?target_id=%s", HttpAPIRoute.CHANNEL_INFO.toFullURL(), this.id)); update(data); completed = true; diff --git a/src/main/java/snw/kookbc/impl/entity/channel/NonCategoryChannelImpl.java b/src/main/java/snw/kookbc/impl/entity/channel/NonCategoryChannelImpl.java index 5b6d1772..e4e9de2f 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/NonCategoryChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/NonCategoryChannelImpl.java @@ -18,7 +18,7 @@ package snw.kookbc.impl.entity.channel; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import org.jetbrains.annotations.Nullable; import snw.jkook.entity.Guild; import snw.jkook.entity.Invitation; @@ -42,8 +42,9 @@ import java.util.Map; import java.util.Set; -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.get; +import static snw.kookbc.util.JacksonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.getStringOrDefault; public abstract class NonCategoryChannelImpl extends ChannelImpl implements NonCategoryChannel { @@ -69,8 +70,8 @@ public String createInvite(int validSeconds, int validTimes) { .put("duration", validSeconds) .put("setting_times", validTimes) .build(); - JsonObject object = client.getNetworkClient().post(HttpAPIRoute.INVITE_CREATE.toFullURL(), body); - return get(object, "url").getAsString(); + JsonNode object = client.getNetworkClient().post(HttpAPIRoute.INVITE_CREATE.toFullURL(), body); + return get(object, "url").asText(); } @Override @@ -122,7 +123,7 @@ public String sendComponent(BaseComponent component, @Nullable ChannelMessage qu .build(); try { return client.getNetworkClient().post(HttpAPIRoute.CHANNEL_MESSAGE_SEND.toFullURL(), body).get("msg_id") - .getAsString(); + .asText(); } catch (BadResponseException e) { if ("资源不存在".equals(e.getRawMessage())) { // 2023/1/17: special case for the resources that aren't created by Bots. @@ -151,9 +152,9 @@ public void setChatLimitTime(int chatLimitTime) { } @Override - public synchronized void update(JsonObject data) { + public synchronized void update(JsonNode data) { super.update(data); - final String parentId = getAsString(data, "parent_id"); + final String parentId = getStringOrDefault(data, "parent_id", ""); final Boolean needParent = "".equals(parentId) || "0".equals(parentId); this.parent = needParent ? null : new CategoryImpl(client, parentId); } diff --git a/src/main/java/snw/kookbc/impl/entity/channel/TextChannelImpl.java b/src/main/java/snw/kookbc/impl/entity/channel/TextChannelImpl.java index b5705d10..20bd908d 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/TextChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/TextChannelImpl.java @@ -18,8 +18,10 @@ package snw.kookbc.impl.entity.channel; -import static snw.kookbc.util.GsonUtil.getAsInt; -import static snw.kookbc.util.GsonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.getAsInt; +import static snw.kookbc.util.JacksonUtil.getAsString; +import static snw.kookbc.util.JacksonUtil.getIntOrDefault; +import static snw.kookbc.util.JacksonUtil.getStringOrDefault; import java.util.Collection; import java.util.Map; @@ -27,7 +29,7 @@ import org.jetbrains.annotations.Nullable; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.User; @@ -91,9 +93,9 @@ public PageIterator> getMessages(@Nullable String ref } @Override - public synchronized void update(JsonObject data) { + public synchronized void update(JsonNode data) { super.update(data); - this.chatLimitTime = getAsInt(data, "slow_mode"); - this.topic = getAsString(data, "topic"); + this.chatLimitTime = getIntOrDefault(data, "slow_mode", 0); + this.topic = getStringOrDefault(data, "topic", ""); } } diff --git a/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java b/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java new file mode 100644 index 00000000..a684690d --- /dev/null +++ b/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java @@ -0,0 +1,230 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.entity.channel; + +import static snw.kookbc.util.JacksonUtil.getIntOrDefault; + +import java.util.Collection; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import com.fasterxml.jackson.databind.JsonNode; + +import snw.jkook.entity.Guild; +import snw.jkook.entity.User; +import snw.jkook.entity.channel.Category; +import snw.jkook.entity.channel.Channel.RolePermissionOverwrite; +import snw.jkook.entity.channel.Channel.UserPermissionOverwrite; +import snw.jkook.entity.channel.ThreadChannel; +import snw.jkook.entity.thread.ThreadPost; +import snw.jkook.util.PageIterator; +import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.entity.thread.ThreadCategoryImpl; +import snw.kookbc.impl.entity.thread.ThreadPostImpl; +import snw.kookbc.impl.network.HttpAPIRoute; +import snw.kookbc.impl.pageiter.ThreadPostIterator; +import snw.kookbc.util.MapBuilder; + +/** + * 帖子频道实现 (Thread Channel - Type 4) + * + *

帖子频道是 Kook 提供的内容型频道,允许用户生成结构化内容, + * 支持知识分享和经验交流。帖子支持富文本内容(文字+图片)。 + * + *

帖子频道特性: + *

    + *
  • 支持分类管理
  • + *
  • 支持主贴、回复和楼中楼
  • + *
  • 主贴和回复支持富媒体(文字+图片)
  • + *
  • 楼中楼仅支持 KMD 和表情
  • + *
  • 支持 @提及功能
  • + *
+ * + * + * @see Kook 帖子频道文档 + * @since KookBC 0.33.0 + */ +public class ThreadChannelImpl extends NonCategoryChannelImpl implements ThreadChannel { + + /** + * 慢速模式时间限制 (秒) + */ + private int chatLimitTime; + + /** + * 构造一个未完全初始化的帖子频道对象 + * + * @param client KBCClient 实例 + * @param id 频道 ID + */ + public ThreadChannelImpl(KBCClient client, String id) { + super(client, id); + } + + /** + * 构造一个完全初始化的帖子频道对象 + * + * @param client KBCClient 实例 + * @param id 频道 ID + * @param master 频道创建者 + * @param guild 所属服务器 + * @param permSync 权限是否与父分类同步 + * @param parent 父分类频道 + * @param name 频道名称 + * @param rpo 角色权限覆写集合 + * @param upo 用户权限覆写集合 + * @param level 频道排序等级 + * @param chatLimitTime 慢速模式时间限制 + */ + public ThreadChannelImpl(KBCClient client, String id, User master, Guild guild, boolean permSync, Category parent, + String name, Collection rpo, Collection upo, int level, + int chatLimitTime) { + super(client, id, master, guild, permSync, parent, name, rpo, upo, level, chatLimitTime); + this.chatLimitTime = chatLimitTime; + this.completed = true; + } + + @Override + public ThreadPost createThread(String title, String content, @Nullable String categoryId) { + // 将纯文本内容转换为简单的卡片消息格式 + // Kook 帖子 API 要求 content 必须是卡片消息的 JSON 字符串 + String cardContent = String.format( + "[{\"type\":\"card\",\"theme\":\"invisible\",\"size\":\"lg\",\"modules\":[{\"type\":\"section\",\"text\":{\"type\":\"plain-text\",\"content\":\"%s\"}}]}]", + content.replace("\"", "\\\"").replace("\n", "\\n") + ); + + Map body = new MapBuilder() + .put("channel_id", getId()) + .put("guild_id", getGuild().getId()) + .put("title", title) + .put("content", cardContent) + .build(); + + if (categoryId != null && !categoryId.isEmpty()) { + body.put("category_id", categoryId); + } + + JsonNode response = client.getNetworkClient().post( + HttpAPIRoute.THREAD_CREATE.toFullURL(), + body + ); + + // 从响应中构建 ThreadPost 对象 + return new ThreadPostImpl(client, this, response); + } + + @Override + @Nullable + public ThreadPost getThreadPost(String threadId) { + Map queryParams = new MapBuilder() + .put("channel_id", getId()) + .put("thread_id", threadId) + .build(); + + try { + JsonNode response = client.getNetworkClient().get( + HttpAPIRoute.THREAD_VIEW.toFullURL() + + "?channel_id=" + getId() + + "&thread_id=" + threadId + ); + + return new ThreadPostImpl(client, this, response); + } catch (Exception e) { + // 帖子不存在或访问出错 + client.getCore().getLogger().warn("获取帖子失败: " + threadId, e); + return null; + } + } + + @Override + public PageIterator> getThreadPosts(@Nullable String categoryId) { + return new ThreadPostIterator(client, this, categoryId); + } + + @Override + public Collection getCategories() { + try { + String url = HttpAPIRoute.THREAD_CATEGORY_LIST.toFullURL() + "?channel_id=" + getId(); + JsonNode response = client.getNetworkClient().get(url); + + Collection categories = new java.util.ArrayList<>(); + + // API 返回的数据结构: { "list": [...] } + // NetworkClient.get() 已经提取了 "data" 字段,所以需要再获取 "list" + JsonNode listNode = response.get("list"); + if (listNode != null && listNode.isArray()) { + for (JsonNode categoryNode : listNode) { + ThreadCategory category = new ThreadCategoryImpl(client, categoryNode); + categories.add(category); + } + } + + return categories; + } catch (Exception e) { + client.getCore().getLogger().warn("获取帖子分类失败", e); + return java.util.Collections.emptyList(); + } + } + + /** + * 获取慢速模式时间限制 + * + * @return 时间限制(秒) + */ + public int getChatLimitTime() { + initIfNeeded(); + return chatLimitTime; + } + + /** + * 设置慢速模式时间限制 + * + * @param chatLimitTime 时间限制(秒) + */ + public void setChatLimitTime(int chatLimitTime) { + this.chatLimitTime = chatLimitTime; + } + + /** + * 从 Jackson JsonNode 更新频道信息 + * + *

此方法会安全地提取以下字段: + *

    + *
  • slow_mode - 慢速模式时间限制
  • + *
+ * + * @param data JSON 数据节点 + */ + @Override + public synchronized void update(JsonNode data) { + super.update(data); + this.chatLimitTime = getIntOrDefault(data, "slow_mode", 0); + } + + /** + * 返回帖子频道的字符串表示 + * + * @return 包含频道ID和名称的字符串 + */ + @Override + public String toString() { + return String.format("ThreadChannel{id=%s, name=%s}", getId(), getName()); + } +} diff --git a/src/main/java/snw/kookbc/impl/entity/channel/VoiceChannelImpl.java b/src/main/java/snw/kookbc/impl/entity/channel/VoiceChannelImpl.java index ebfa9e55..7237a4e6 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/VoiceChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/VoiceChannelImpl.java @@ -18,9 +18,9 @@ package snw.kookbc.impl.entity.channel; -import static snw.kookbc.util.GsonUtil.NORMAL_GSON; -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; +import static snw.kookbc.util.JacksonUtil.getMapper; +import static snw.kookbc.util.JacksonUtil.get; +import static snw.kookbc.util.JacksonUtil.has; import java.util.Collection; import java.util.Collections; @@ -31,10 +31,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.User; @@ -43,6 +42,7 @@ import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.network.HttpAPIRoute; import snw.kookbc.util.MapBuilder; +import snw.kookbc.util.JacksonUtil; public class VoiceChannelImpl extends NonCategoryChannelImpl implements VoiceChannel { private boolean passwordProtected; @@ -74,8 +74,8 @@ public String createInvite(int validSeconds, int validTimes) { .put("duration", validSeconds) .put("setting_times", validTimes) .build(); - JsonObject object = client.getNetworkClient().post(HttpAPIRoute.INVITE_CREATE.toFullURL(), body); - return get(object, "url").getAsString(); + JsonNode object = client.getNetworkClient().post(HttpAPIRoute.INVITE_CREATE.toFullURL(), body); + return get(object, "url").asText(); } @Override @@ -116,9 +116,9 @@ public void setSize(int size) { @Override public int getQuality() { // must query because we can't update this value by update(JsonObject) method - final JsonObject self = client.getNetworkClient() + final JsonNode self = client.getNetworkClient() .get(HttpAPIRoute.CHANNEL_INFO.toFullURL() + "?target_id=" + getId()); - return get(self, "voice_quality").getAsInt(); + return get(self, "voice_quality").asInt(); } @Override @@ -135,11 +135,11 @@ public void setQuality(int i) { public Collection getUsers() { String rawContent = client.getNetworkClient() .getRawContent(HttpAPIRoute.CHANNEL_USER_LIST.toFullURL() + "?channel_id=" + getId()); - JsonArray array = JsonParser.parseString(rawContent).getAsJsonObject().getAsJsonArray("data"); + JsonNode rootNode = JacksonUtil.parse(rawContent); + JsonNode array = JacksonUtil.get(rootNode, "data"); Set users = new HashSet<>(); - for (JsonElement element : array) { - JsonObject obj = element.getAsJsonObject(); - users.add(client.getStorage().getUser(obj.get("id").getAsString(), obj)); + for (JsonNode element : array) { + users.add(client.getStorage().getUser(element.get("id").asText(), element)); } return Collections.unmodifiableCollection(users); } @@ -158,10 +158,10 @@ public void setPasswordProtected(boolean passwordProtected) { } @Override - public synchronized void update(JsonObject data) { + public synchronized void update(JsonNode data) { super.update(data); - boolean hasPassword = has(data, "has_password") && get(data, "has_password").getAsBoolean(); - int size = has(data, "limit_amount") ? get(data, "limit_amount").getAsInt() : 0; + boolean hasPassword = data.has("has_password") && data.get("has_password").asBoolean(); + int size = data.has("limit_amount") ? data.get("limit_amount").asInt() : 0; // KOOK does not provide voice quality value here! this.passwordProtected = hasPassword; this.maxSize = size; @@ -173,8 +173,12 @@ public StreamingInfo requestStreamingInfo(@Nullable String password) { .put("channel_id", getId()) .putIfNotNull("password", password) .build(); - final JsonObject res = client.getNetworkClient().post(HttpAPIRoute.VOICE_JOIN.toFullURL(), body); - return NORMAL_GSON.fromJson(res, StreamingInfoImpl.class); + final JsonNode res = client.getNetworkClient().post(HttpAPIRoute.VOICE_JOIN.toFullURL(), body); + try { + return getMapper().readValue(res.toString(), StreamingInfoImpl.class); + } catch (Exception e) { + throw new RuntimeException("Failed to parse StreamingInfo", e); + } } @Override @@ -184,8 +188,12 @@ public StreamingInfo requestStreamingInfo(@Nullable String password, boolean rtc .putIfNotNull("password", password) .put("rtcp_mux", rtcpMux) .build(); - final JsonObject res = client.getNetworkClient().post(HttpAPIRoute.VOICE_JOIN.toFullURL(), body); - return NORMAL_GSON.fromJson(res, StreamingInfoImpl.class); + final JsonNode res = client.getNetworkClient().post(HttpAPIRoute.VOICE_JOIN.toFullURL(), body); + try { + return getMapper().readValue(res.toString(), StreamingInfoImpl.class); + } catch (Exception e) { + throw new RuntimeException("Failed to parse StreamingInfo", e); + } } @Override @@ -198,8 +206,12 @@ public StreamingInfo requestStreamingInfo(@Nullable String password, String audi .put("audio_pt", audioPayloadType) .put("rtcp_mux", rtcpMux) .build(); - final JsonObject res = client.getNetworkClient().post(HttpAPIRoute.VOICE_JOIN.toFullURL(), body); - return NORMAL_GSON.fromJson(res, StreamingInfoImpl.class); + final JsonNode res = client.getNetworkClient().post(HttpAPIRoute.VOICE_JOIN.toFullURL(), body); + try { + return getMapper().readValue(res.toString(), StreamingInfoImpl.class); + } catch (Exception e) { + throw new RuntimeException("Failed to parse StreamingInfo", e); + } } @Override @@ -219,7 +231,14 @@ public static final class StreamingInfoImpl implements StreamingInfo { private final String audio_ssrc; private final String audio_pt; - public StreamingInfoImpl(String ip, int port, int rtcp_port, int bitrate, String audioSsrc, String audioPt) { + @JsonCreator + public StreamingInfoImpl( + @JsonProperty("ip") String ip, + @JsonProperty("port") int port, + @JsonProperty("rtcp_port") int rtcp_port, + @JsonProperty("bitrate") int bitrate, + @JsonProperty("audio_ssrc") String audioSsrc, + @JsonProperty("audio_pt") String audioPt) { this.ip = ip; this.port = port; this.rtcp_port = rtcp_port; diff --git a/src/main/java/snw/kookbc/impl/entity/thread/ThreadCategoryImpl.java b/src/main/java/snw/kookbc/impl/entity/thread/ThreadCategoryImpl.java new file mode 100644 index 00000000..e6950cbb --- /dev/null +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadCategoryImpl.java @@ -0,0 +1,125 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.entity.thread; + +import static snw.kookbc.util.JacksonUtil.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import com.fasterxml.jackson.databind.JsonNode; + +import snw.jkook.entity.channel.Channel; +import snw.jkook.entity.channel.ThreadChannel.ThreadCategory; +import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.entity.builder.EntityBuildUtil; + +/** + * ThreadCategory 实体实现 + * + *

代表帖子频道中的分类,用于组织和管理帖子 + * + * @see ThreadCategory + * @since KookBC 0.33.0 + */ +public class ThreadCategoryImpl implements ThreadCategory { + + private final KBCClient client; + private final String id; + private String name; + private int allow; + private int deny; + private Collection> roles; + private boolean isDefault; + + /** + * 从 Jackson JsonNode 构建 ThreadCategory + * + * @param client KBCClient 实例 + * @param data JSON 数据节点 + */ + public ThreadCategoryImpl(KBCClient client, JsonNode data) { + this.client = client; + this.id = getStringOrDefault(data, "id", ""); + update(data); + } + + /** + * 从 JSON 数据更新分类信息 + * + * @param data JSON 数据节点 + */ + public synchronized void update(JsonNode data) { + this.name = getStringOrDefault(data, "name", ""); + this.allow = getIntOrDefault(data, "allow", 0); + this.deny = getIntOrDefault(data, "deny", 0); + this.isDefault = getBooleanOrDefault(data, "is_default", false); + + // 解析权限覆写列表 + // 使用 EntityBuildUtil 的 Jackson 版本方法解析角色权限覆写和用户权限覆写 + Collection> permissionOverwrites = new ArrayList<>(); + + // 解析角色权限覆写 (permission_overwrites) + Collection rolePermissions = EntityBuildUtil.parseRPO(data); + permissionOverwrites.addAll(rolePermissions); + + // 解析用户权限覆写 (permission_users) + Collection userPermissions = EntityBuildUtil.parseUPO(client, data); + permissionOverwrites.addAll(userPermissions); + + this.roles = Collections.unmodifiableCollection(permissionOverwrites); + } + + @Override + public String getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public int getAllow() { + return allow; + } + + @Override + public int getDeny() { + return deny; + } + + @Override + public Collection> getRoles() { + return roles; + } + + @Override + public boolean isDefault() { + return isDefault; + } + + @Override + public String toString() { + return String.format("ThreadCategory{id=%s, name=%s, isDefault=%s}", + id, name, isDefault); + } +} diff --git a/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java b/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java new file mode 100644 index 00000000..948cb745 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java @@ -0,0 +1,298 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.entity.thread; + +import static snw.kookbc.util.JacksonUtil.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import com.fasterxml.jackson.databind.JsonNode; + +import snw.jkook.entity.User; +import snw.jkook.entity.channel.ThreadChannel; +import snw.jkook.entity.thread.ThreadPost; +import snw.jkook.entity.thread.ThreadReply; +import snw.jkook.message.component.BaseComponent; +import snw.jkook.message.component.card.CardComponent; +import snw.jkook.message.component.card.MultipleCardComponent; +import snw.jkook.util.PageIterator; +import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.entity.builder.CardBuilder; +import snw.kookbc.impl.entity.builder.MessageBuilder; +import snw.kookbc.impl.network.HttpAPIRoute; +import snw.kookbc.impl.pageiter.ThreadReplyIterator; +import snw.kookbc.util.MapBuilder; + +/** + * ThreadPost 实体实现 + * + *

代表帖子频道中的一个帖子,支持富文本内容、回复和统计信息 + * + * @see ThreadPost + * @since KookBC 0.33.0 + */ +public class ThreadPostImpl implements ThreadPost { + + private final KBCClient client; + private final String id; + private final ThreadChannel channel; + private User author; + private String title; + private MultipleCardComponent content; + private String previewContent; + private String cover; + private int status; + private String categoryId; + private long createTime; + private long latestActiveTime; + private boolean updated; + private boolean contentDeleted; + private int contentDeletedType; + private int collectNum; + private Collection tags; + private int replyCount; + private int viewCount; + + /** + * 从 Jackson JsonNode 构建 ThreadPost + * + * @param client KBCClient 实例 + * @param channel 所属频道 + * @param data JSON 数据节点 + */ + public ThreadPostImpl(KBCClient client, ThreadChannel channel, JsonNode data) { + this.client = client; + this.channel = channel; + // Kook API 统一使用 "id" 字段表示帖子 ID + this.id = getAsString(data, "id"); + update(data); + } + + /** + * 从 JSON 数据更新帖子信息 + * + * @param data JSON 数据节点 + */ + public synchronized void update(JsonNode data) { + // 基础信息 + this.title = getStringOrDefault(data, "title", ""); + this.previewContent = getStringOrDefault(data, "preview_content", ""); + this.cover = getStringOrDefault(data, "cover", ""); + this.status = getIntOrDefault(data, "status", 0); + this.categoryId = getStringOrDefault(data, "category_id", ""); + + // 时间信息 + this.createTime = getLongOrDefault(data, "create_time", 0L); + this.latestActiveTime = getLongOrDefault(data, "latest_active_time", this.createTime); + + // 状态标志 + this.updated = getBooleanOrDefault(data, "is_updated", false); + this.contentDeleted = getBooleanOrDefault(data, "content_deleted", false); + this.contentDeletedType = getIntOrDefault(data, "content_deleted_type", 0); + + // 统计信息 + this.collectNum = getIntOrDefault(data, "collect_num", 0); + this.replyCount = getIntOrDefault(data, "reply_count", 0); + this.viewCount = getIntOrDefault(data, "view_count", 0); + + // 作者信息 + String authorId = getStringOrDefault(data, "author_id", null); + if (authorId != null) { + this.author = client.getStorage().getUser(authorId); + } + + // 标签列表 + JsonNode tagsNode = data.get("tags"); + if (tagsNode != null && tagsNode.isArray()) { + Collection tagList = new ArrayList<>(); + for (JsonNode tag : tagsNode) { + tagList.add(tag.asText()); + } + this.tags = Collections.unmodifiableCollection(tagList); + } else { + this.tags = Collections.emptyList(); + } + + // 解析卡片消息组件 + // API 返回的 content 是一个 JSON 字符串,需要先解析为 JsonNode + JsonNode contentNode = data.get("content"); + if (contentNode != null && !contentNode.isNull() && !contentNode.asText().isEmpty()) { + try { + // content 是 JSON 字符串,需要先解析 + String contentStr = contentNode.asText(); + JsonNode contentJson = parse(contentStr); + + // 使用 CardBuilder 构建卡片组件 + if (contentJson.isArray()) { + // 多个卡片 + this.content = CardBuilder.buildCardArray(contentJson); + } else if (contentJson.isObject()) { + // 单个卡片,包装成 MultipleCardComponent + CardComponent card = CardBuilder.buildCardObject(contentJson); + this.content = new MultipleCardComponent(Collections.singletonList(card)); + } else { + this.content = null; + } + } catch (Exception e) { + // 解析失败时记录日志并设置为 null + client.getCore().getLogger().warn("解析帖子内容失败: {}", e.getMessage()); + this.content = null; + } + } else { + this.content = null; + } + } + + @Override + public String getId() { + return id; + } + + @Override + public ThreadChannel getChannel() { + return channel; + } + + @Override + public User getAuthor() { + return author; + } + + @Override + public String getTitle() { + return title; + } + + @Override + @Nullable + public MultipleCardComponent getContent() { + return content; + } + + @Override + public String getPreviewContent() { + return previewContent; + } + + @Override + public String getCover() { + return cover; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCategoryId() { + return categoryId; + } + + @Override + public long getCreateTime() { + return createTime; + } + + @Override + public long getLatestActiveTime() { + return latestActiveTime; + } + + @Override + public boolean isUpdated() { + return updated; + } + + @Override + public boolean isContentDeleted() { + return contentDeleted; + } + + @Override + public int getContentDeletedType() { + return contentDeletedType; + } + + @Override + public int getCollectNum() { + return collectNum; + } + + @Override + public Collection getTags() { + return tags; + } + + @Override + public int getReplyCount() { + return replyCount; + } + + @Override + public int getViewCount() { + return viewCount; + } + + @Override + public PageIterator> getReplies() { + return new ThreadReplyIterator(client, this); + } + + @Override + public ThreadReply reply(String content) { + Map body = new MapBuilder() + .put("channel_id", channel.getId()) + .put("thread_id", id) + .put("content", content) + .build(); + + JsonNode response = client.getNetworkClient().post( + HttpAPIRoute.THREAD_REPLY.toFullURL(), + body + ); + + // 从响应中构建 ThreadReply 对象 + return new ThreadReplyImpl(client, this, response); + } + + @Override + public void delete() { + Map body = new MapBuilder() + .put("channel_id", channel.getId()) + .put("thread_id", id) + .build(); + + client.getNetworkClient().post( + HttpAPIRoute.THREAD_DELETE.toFullURL(), + body + ); + } + + @Override + public String toString() { + return String.format("ThreadPost{id=%s, title=%s, author=%s}", + id, title, author != null ? author.getName() : "unknown"); + } +} diff --git a/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java b/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java new file mode 100644 index 00000000..833011e2 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java @@ -0,0 +1,226 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.entity.thread; + +import static snw.kookbc.util.JacksonUtil.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import com.fasterxml.jackson.databind.JsonNode; + +import snw.jkook.entity.User; +import snw.jkook.entity.thread.ThreadPost; +import snw.jkook.entity.thread.ThreadReply; +import snw.jkook.message.component.BaseComponent; +import snw.jkook.message.component.card.CardComponent; +import snw.jkook.message.component.card.MultipleCardComponent; +import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.entity.builder.MessageBuilder; +import snw.kookbc.impl.network.HttpAPIRoute; +import snw.kookbc.util.MapBuilder; + +/** + * ThreadReply 实体实现 + * + *

代表帖子频道中对主贴的回复,支持富文本内容和嵌套回复(楼中楼) + * + * @see ThreadReply + * @since KookBC 0.33.0 + */ +public class ThreadReplyImpl implements ThreadReply { + + private final KBCClient client; + private final String id; + private final ThreadPost threadPost; + private User author; + private MultipleCardComponent content; + private long createTime; + private String belongToPostId; + private String replyToPostId; + private Collection replies; + private boolean updated; + + /** + * 从 Jackson JsonNode 构建 ThreadReply + * + * @param client KBCClient 实例 + * @param threadPost 所属的主贴 + * @param data JSON 数据节点 + */ + public ThreadReplyImpl(KBCClient client, ThreadPost threadPost, JsonNode data) { + this.client = client; + this.threadPost = threadPost; + this.id = getAsString(data, "reply_id"); + update(data); + } + + /** + * 从 JSON 数据更新回复信息 + * + * @param data JSON 数据节点 + */ + public synchronized void update(JsonNode data) { + // 时间信息 + this.createTime = getLongOrDefault(data, "create_time", 0L); + + // 状态标志 + this.updated = getBooleanOrDefault(data, "is_updated", false); + + // 关系信息 + this.belongToPostId = getStringOrDefault(data, "belong_to_post_id", null); + this.replyToPostId = getStringOrDefault(data, "reply_id", null); + + // 作者信息 + String authorId = getStringOrDefault(data, "author_id", null); + if (authorId != null) { + this.author = client.getStorage().getUser(authorId); + } + + // 嵌套回复列表 + JsonNode repliesNode = data.get("replies"); + if (repliesNode != null && repliesNode.isArray()) { + Collection replyList = new ArrayList<>(); + for (JsonNode replyNode : repliesNode) { + ThreadReply nestedReply = new ThreadReplyImpl(client, threadPost, replyNode); + replyList.add(nestedReply); + } + this.replies = Collections.unmodifiableCollection(replyList); + } else { + this.replies = Collections.emptyList(); + } + + // 解析卡片消息组件 + JsonNode contentNode = data.get("content"); + if (contentNode != null && !contentNode.isNull() && !contentNode.asText().isEmpty()) { + try { + MessageBuilder messageBuilder = new MessageBuilder(client); + BaseComponent component = messageBuilder.buildComponent(data); + + // 如果是 MultipleCardComponent 或者可以转换为 MultipleCardComponent + if (component instanceof MultipleCardComponent) { + this.content = (MultipleCardComponent) component; + } else if (component instanceof CardComponent) { + // 单个卡片包装成 MultipleCardComponent + this.content = new MultipleCardComponent( + Collections.singletonList((CardComponent) component) + ); + } else { + // 其他类型的消息组件暂不支持 + this.content = null; + } + } catch (Exception e) { + // 解析失败时记录日志并设置为 null + client.getCore().getLogger().warn("解析帖子回复内容失败: {}", e.getMessage()); + this.content = null; + } + } else { + this.content = null; + } + } + + @Override + public String getId() { + return id; + } + + @Override + public ThreadPost getThreadPost() { + return threadPost; + } + + @Override + public User getAuthor() { + return author; + } + + @Override + @Nullable + public MultipleCardComponent getContent() { + return content; + } + + @Override + public long getCreateTime() { + return createTime; + } + + @Override + @Nullable + public String getBelongToPostId() { + return belongToPostId; + } + + @Override + @Nullable + public String getReplyToPostId() { + return replyToPostId; + } + + @Override + public Collection getReplies() { + return replies; + } + + @Override + public boolean isUpdated() { + return updated; + } + + @Override + public ThreadReply reply(String content) { + Map body = new MapBuilder() + .put("channel_id", threadPost.getChannel().getId()) + .put("thread_id", threadPost.getId()) + .put("reply_id", id) + .put("content", content) + .build(); + + JsonNode response = client.getNetworkClient().post( + HttpAPIRoute.THREAD_REPLY.toFullURL(), + body + ); + + // 从响应中构建嵌套回复对象 + return new ThreadReplyImpl(client, threadPost, response); + } + + @Override + public void delete() { + Map body = new MapBuilder() + .put("channel_id", threadPost.getChannel().getId()) + .put("reply_id", id) + .build(); + + client.getNetworkClient().post( + HttpAPIRoute.THREAD_DELETE.toFullURL(), + body + ); + } + + @Override + public String toString() { + return String.format("ThreadReply{id=%s, author=%s, createTime=%d}", + id, author != null ? author.getName() : "unknown", createTime); + } +} diff --git a/src/main/java/snw/kookbc/impl/event/EventFactory.java b/src/main/java/snw/kookbc/impl/event/EventFactory.java index fceeb266..56a22597 100644 --- a/src/main/java/snw/kookbc/impl/event/EventFactory.java +++ b/src/main/java/snw/kookbc/impl/event/EventFactory.java @@ -18,9 +18,8 @@ package snw.kookbc.impl.event; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import snw.jkook.event.Event; import snw.jkook.event.channel.*; import snw.jkook.event.guild.*; @@ -33,129 +32,123 @@ import snw.jkook.event.role.RoleInfoUpdateEvent; import snw.jkook.event.user.*; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.channel.*; -import snw.kookbc.impl.serializer.event.guild.*; -import snw.kookbc.impl.serializer.event.item.ItemConsumedEventDeserializer; -import snw.kookbc.impl.serializer.event.pm.PrivateMessageDeleteEventDeserializer; -import snw.kookbc.impl.serializer.event.pm.PrivateMessageReceivedEventDeserializer; -import snw.kookbc.impl.serializer.event.pm.PrivateMessageUpdateEventDeserializer; -import snw.kookbc.impl.serializer.event.role.RoleCreateEventDeserializer; -import snw.kookbc.impl.serializer.event.role.RoleDeleteEventDeserializer; -import snw.kookbc.impl.serializer.event.role.RoleInfoUpdateEventDeserializer; -import snw.kookbc.impl.serializer.event.user.*; - -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; +import snw.kookbc.impl.serializer.event.jackson.JKookEventModule; +import static snw.kookbc.util.JacksonUtil.get; +import static snw.kookbc.util.JacksonUtil.has; + +/** + * 事件工厂类 - 负责从 JSON 数据创建事件对象 + * + * @since 0.52.0 使用 Jackson 作为唯一 JSON 引擎 + */ public class EventFactory { protected final KBCClient client; protected final EventManagerImpl eventManager; - protected final Gson gson; + protected final ObjectMapper jacksonMapper; public EventFactory(KBCClient client) { this.client = client; this.eventManager = ((EventManagerImpl) client.getCore().getEventManager()); - this.gson = createGson(); + this.jacksonMapper = createJacksonMapper(); } - public Event createEvent(JsonObject object) { + /** + * 从 Jackson JsonNode 创建事件对象 + * + * @param object JSON 事件数据 + * @return 事件对象,如果无法解析则返回 null + */ + public Event createEvent(JsonNode object) { final Class eventType = parseEventType(object); if (eventType == null) { return null; // unknown event type } + + // 检查是否有监听器订阅此事件,避免创建无用对象 if (!eventManager.isSubscribed(eventType)) { - // if not message event, ensure command system can receive event. + // 特殊处理:命令系统需要接收消息事件 if (eventType != ChannelMessageEvent.class && eventType != PrivateMessageReceivedEvent.class) { return null; } } + + // 特殊处理: GuildUserNickNameUpdateEvent 使用特殊字段判断 if (eventType == GuildInfoUpdateEvent.class) { - if (has( - get(get(object, "extra").getAsJsonObject(), "body").getAsJsonObject(), - "my_nickname")) { - return this.gson.fromJson(object, GuildUserNickNameUpdateEvent.class); // force convert + if (has(get(get(object, "extra"), "body"), "my_nickname")) { + // 修正事件类型为 GuildUserNickNameUpdateEvent + try { + return jacksonMapper.readValue(object.toString(), GuildUserNickNameUpdateEvent.class); + } catch (Exception e) { + client.getCore().getLogger().warn("使用 Jackson 解析 GuildUserNickNameUpdateEvent 失败", e); + return null; + } } } - final Event result = this.gson.fromJson(object, eventType); - // why the second condition? see ChannelInfoUpdateEventDeserializer - if (result == null && !(eventType == ChannelInfoUpdateEvent.class)) { - client.getCore().getLogger().error("We cannot understand the frame."); - client.getCore().getLogger().error("Frame content: {}", object); + // 使用 Jackson 反序列化事件对象 + try { + Event result = jacksonMapper.readValue(object.toString(), eventType); + if (result != null) { + return result; + } + } catch (Exception e) { + client.getCore().getLogger().error("反序列化类型为 {} 的事件失败: {}", + eventType.getSimpleName(), e.getMessage()); + client.getCore().getLogger().debug("事件 JSON: {}", object); } - return result; + + // 如果 Jackson 反序列化失败,记录错误 + if (!(eventType == ChannelInfoUpdateEvent.class)) { + client.getCore().getLogger().error("无法理解此数据帧"); + client.getCore().getLogger().error("数据帧内容: {}", object); + } + return null; } - protected Class parseEventType(JsonObject object) { - final String type = get(get(object, "extra").getAsJsonObject(), "type").getAsString(); + /** + * 解析事件类型 + * + * @param object JSON 事件数据 + * @return 事件类型 Class,如果无法识别则返回 null + */ + protected Class parseEventType(JsonNode object) { + final String type = get(get(object, "extra"), "type").asText(); + + // 特殊事件:物品消耗事件 if ("12".equals(type)) { return ItemConsumedEvent.class; } + + // 标准事件映射 if (EventTypeMap.MAP.containsKey(type)) { return EventTypeMap.MAP.get(type); } + + // 验证是否为数字类型 try { Integer.parseInt(type); } catch (NumberFormatException e) { return null; // unknown event type } - // must be number at this time? - if ("PERSON".equals(get(object, "channel_type").getAsString())) { + + // 消息事件特殊处理:根据频道类型区分 + if ("PERSON".equals(get(object, "channel_type").asText())) { return PrivateMessageReceivedEvent.class; } else { return ChannelMessageEvent.class; } } - // NOT static, so it can be override. - protected Gson createGson() { - final KBCClient client = this.client; - return new GsonBuilder() - // --- UNUSUAL EVENTS START --- - .registerTypeAdapter(ChannelMessageEvent.class, new ChannelMessageEventDeserializer(client)) - .registerTypeAdapter(ItemConsumedEvent.class, new ItemConsumedEventDeserializer(client)) - .registerTypeAdapter(PrivateMessageReceivedEvent.class, new PrivateMessageReceivedEventDeserializer(client)) - // --- UNUSUAL EVENTS END --- - - // Channel Event - .registerTypeAdapter(ChannelCreateEvent.class, new ChannelCreateEventDeserializer(client)) - .registerTypeAdapter(ChannelDeleteEvent.class, new ChannelDeleteEventDeserializer(client)) - .registerTypeAdapter(ChannelInfoUpdateEvent.class, new ChannelInfoUpdateEventDeserializer(client)) - .registerTypeAdapter(ChannelMessageDeleteEvent.class, new ChannelMessageDeleteEventDeserializer(client)) - .registerTypeAdapter(ChannelMessagePinEvent.class, new ChannelMessagePinEventDeserializer(client)) - .registerTypeAdapter(ChannelMessageUnpinEvent.class, new ChannelMessageUnpinEventDeserializer(client)) - .registerTypeAdapter(ChannelMessageUpdateEvent.class, new ChannelMessageUpdateEventDeserializer(client)) - - // Guild Event - .registerTypeAdapter(GuildAddEmojiEvent.class, new GuildAddEmojiEventDeserializer(client)) - .registerTypeAdapter(GuildBanUserEvent.class, new GuildBanUserEventDeserializer(client)) - .registerTypeAdapter(GuildDeleteEvent.class, new GuildDeleteEventDeserializer(client)) - .registerTypeAdapter(GuildInfoUpdateEvent.class, new GuildInfoUpdateEventDeserializer(client)) - .registerTypeAdapter(GuildRemoveEmojiEvent.class, new GuildRemoveEmojiEventDeserializer(client)) - .registerTypeAdapter(GuildUnbanUserEvent.class, new GuildUnbanUserEventDeserializer(client)) - .registerTypeAdapter(GuildUpdateEmojiEvent.class, new GuildUpdateEmojiEventDeserializer(client)) - .registerTypeAdapter(GuildUserNickNameUpdateEvent.class, new GuildUserNickNameUpdateEventDeserializer(client)) - - // PrivateMessage Event - .registerTypeAdapter(PrivateMessageDeleteEvent.class, new PrivateMessageDeleteEventDeserializer(client)) - .registerTypeAdapter(PrivateMessageUpdateEvent.class, new PrivateMessageUpdateEventDeserializer(client)) - - // Role Event - .registerTypeAdapter(RoleCreateEvent.class, new RoleCreateEventDeserializer(client)) - .registerTypeAdapter(RoleDeleteEvent.class, new RoleDeleteEventDeserializer(client)) - .registerTypeAdapter(RoleInfoUpdateEvent.class, new RoleInfoUpdateEventDeserializer(client)) - - // User Event - .registerTypeAdapter(UserAddReactionEvent.class, new UserAddReactionEventDeserializer(client)) - .registerTypeAdapter(UserClickButtonEvent.class, new UserClickButtonEventDeserializer(client)) - .registerTypeAdapter(UserInfoUpdateEvent.class, new UserInfoUpdateEventDeserializer(client)) - .registerTypeAdapter(UserJoinGuildEvent.class, new UserJoinGuildEventDeserializer(client)) - .registerTypeAdapter(UserJoinVoiceChannelEvent.class, new UserJoinVoiceChannelEventDeserializer(client)) - .registerTypeAdapter(UserLeaveGuildEvent.class, new UserLeaveGuildEventDeserializer(client)) - .registerTypeAdapter(UserLeaveVoiceChannelEvent.class, new UserLeaveVoiceChannelEventDeserializer(client)) - .registerTypeAdapter(UserOfflineEvent.class, new UserOfflineEventDeserializer(client)) - .registerTypeAdapter(UserOnlineEvent.class, new UserOnlineEventDeserializer(client)) - .registerTypeAdapter(UserRemoveReactionEvent.class, new UserRemoveReactionEventDeserializer(client)) - .create(); + /** + * 创建并配置 Jackson ObjectMapper + * + * @return 配置好的 ObjectMapper 实例 + */ + protected ObjectMapper createJacksonMapper() { + ObjectMapper mapper = new ObjectMapper(); + // 注册 JKook 事件反序列化模块 + mapper.registerModule(new JKookEventModule(client)); + return mapper; } } diff --git a/src/main/java/snw/kookbc/impl/event/EventManagerImpl.java b/src/main/java/snw/kookbc/impl/event/EventManagerImpl.java index 82de0a12..cffe4784 100644 --- a/src/main/java/snw/kookbc/impl/event/EventManagerImpl.java +++ b/src/main/java/snw/kookbc/impl/event/EventManagerImpl.java @@ -28,11 +28,12 @@ import snw.jkook.event.Listener; import snw.jkook.plugin.Plugin; import snw.kookbc.impl.KBCClient; +import snw.kookbc.util.VirtualThreadUtil; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.*; import static snw.kookbc.util.Util.ensurePluginEnabled; @@ -42,19 +43,89 @@ public class EventManagerImpl implements EventManager { private final MethodSubscriptionAdapter msa; private final Map> listeners = new ConcurrentHashMap<>(); + // 优化的并行事件处理 + private final ExecutorService eventExecutor; + private final boolean parallelEventProcessing; + public EventManagerImpl(KBCClient client) { this.client = client; this.bus = new SimpleEventBus<>(Event.class); this.msa = new SimpleMethodSubscriptionAdapter<>(bus, EventExecutorFactoryImpl.INSTANCE, MethodScannerImpl.INSTANCE); + + // 从配置读取是否启用并行事件处理 + this.parallelEventProcessing = client.getConfig().getBoolean("enable-parallel-event-processing", true); + + // 创建专用的虚拟线程执行器用于并行事件处理 + this.eventExecutor = parallelEventProcessing ? + VirtualThreadUtil.newVirtualThreadExecutor() : + null; + + + client.getCore().getLogger().info("事件管理器初始化完成 - 并行处理: {}", + parallelEventProcessing ? "启用" : "禁用"); } @Override public void callEvent(Event event) { + if (event == null) { + return; + } + + + if (parallelEventProcessing && eventExecutor != null) { + // 并行模式:使用虚拟线程执行事件处理 + callEventParallel(event); + } else { + // 传统同步模式:保持向后兼容 + callEventSync(event); + } + } + + /** + * 并行事件处理方法 - 符合 Kook SN 顺序要求 + * + * 关键设计: + * 1. 全局事件顺序已由 ListenerImpl 保证(通过 SN 检查) + * 2. 单个事件内部的监听器可以并行处理 + * 3. 使用虚拟线程提高吞吐量,减少上下文切换开销 + */ + private void callEventParallel(Event event) { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + return bus.post(event); + }, eventExecutor); + + try { + // 等待事件处理完成,不设置超时以避免中断重要事件 + PostResult result = future.get(); + handlePostResult(result, event); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + client.getCore().getLogger().warn("事件处理被中断: {}", event.getClass().getSimpleName(), e); + // 回退到同步处理 + callEventSync(event); + } catch (ExecutionException e) { + client.getCore().getLogger().error("并行事件处理异常: {}", event.getClass().getSimpleName(), e.getCause()); + // 回退到同步处理 + callEventSync(event); + } + } + + /** + * 传统同步事件处理方法 + */ + private void callEventSync(Event event) { final PostResult result = bus.post(event); + handlePostResult(result, event); + } + + /** + * 处理事件处理结果 + */ + private void handlePostResult(PostResult result, Event event) { if (!result.wasSuccessful()) { - client.getCore().getLogger().error("Unexpected exception while posting event."); + client.getCore().getLogger().error("事件处理异常: {}", event.getClass().getSimpleName()); for (final Throwable t : result.exceptions().values()) { - t.printStackTrace(); + client.getCore().getLogger().error("监听器异常", t); } } } @@ -89,6 +160,34 @@ public boolean isSubscribed(Class type) { return bus.hasSubscribers(type); } + + /** + * 关闭事件管理器,清理资源 + */ + public void shutdown() { + client.getCore().getLogger().info("正在关闭事件管理器..."); + + + // 关闭事件执行器 + if (eventExecutor != null && !eventExecutor.isShutdown()) { + client.getCore().getLogger().info("正在关闭事件处理器..."); + eventExecutor.shutdown(); + try { + if (!eventExecutor.awaitTermination(10, TimeUnit.SECONDS)) { + client.getCore().getLogger().warn("事件处理器未在10秒内正常关闭,强制关闭"); + eventExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + eventExecutor.shutdownNow(); + } + } + + + client.getCore().getLogger().info("事件管理器已关闭"); + } + + private List getListeners(Plugin plugin) { return listeners.computeIfAbsent(plugin, p -> new LinkedList<>()); } diff --git a/src/main/java/snw/kookbc/impl/event/internal/UserClickButtonListener.java b/src/main/java/snw/kookbc/impl/event/internal/UserClickButtonListener.java index 2930a967..3585b86b 100644 --- a/src/main/java/snw/kookbc/impl/event/internal/UserClickButtonListener.java +++ b/src/main/java/snw/kookbc/impl/event/internal/UserClickButtonListener.java @@ -1,7 +1,7 @@ package snw.kookbc.impl.event.internal; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import snw.kookbc.util.JacksonUtil; import snw.jkook.event.EventHandler; import snw.jkook.event.Listener; import snw.jkook.event.user.UserClickButtonEvent; @@ -39,10 +39,10 @@ public void event(UserClickButtonEvent event) { if (!value.startsWith(HELP_VALUE_HEADER)) { return; } - JsonObject detail = JsonParser.parseString(value.substring(HELP_VALUE_HEADER.length())).getAsJsonObject(); - int page = detail.get("page").getAsInt(); - int currentPage = detail.get("current").getAsInt(); - String messageType = detail.get("messageType").getAsString(); + JsonNode detail = JacksonUtil.parse(value.substring(HELP_VALUE_HEADER.length())); + int page = detail.get("page").asInt(); + int currentPage = detail.get("current").asInt(); + String messageType = detail.get("messageType").asText(); if (page == currentPage) { return; } diff --git a/src/main/java/snw/kookbc/impl/launch/LaunchClassLoader.java b/src/main/java/snw/kookbc/impl/launch/LaunchClassLoader.java index eea3bfba..e6a15766 100644 --- a/src/main/java/snw/kookbc/impl/launch/LaunchClassLoader.java +++ b/src/main/java/snw/kookbc/impl/launch/LaunchClassLoader.java @@ -218,7 +218,7 @@ public Class findClass(final String name) throws ClassNotFoundException { if (pkg == null) { pkg = definePackage(packageName, null, null, null, null, null, null, null); } else if (pkg.isSealed()) { - LogWrapper.LOGGER.warn("The URL {} is defining elements for sealed path {}", urlConnection == null ? "null" : urlConnection.getURL(), packageName); + LogWrapper.LOGGER.warn("URL {} 正在为密封路径 {} 定义元素", urlConnection == null ? "null" : urlConnection.getURL(), packageName); } } } @@ -246,7 +246,7 @@ public Class findClass(final String name) throws ClassNotFoundException { } invalidClasses.add(name); if (DEBUG) { - LogWrapper.LOGGER.error("Exception encountered attempting classloading of {}", name, e); + LogWrapper.LOGGER.error("尝试加载类 {} 时遇到异常", name, e); } throw new ClassNotFoundException(name, e); } @@ -356,7 +356,7 @@ private byte[] readFully(InputStream stream) { System.arraycopy(buffer, 0, result, 0, totalLength); return result; } catch (Throwable t) { - LogWrapper.LOGGER.error("Problem loading class", t); + LogWrapper.LOGGER.error("加载类时出现问题", t); return new byte[0]; } } @@ -407,14 +407,14 @@ public byte[] getClassBytes(String name) throws IOException { if (classResource == null) { if (DEBUG) - LogWrapper.LOGGER.warn("Failed to find class resource {}", resourcePath); + LogWrapper.LOGGER.warn("未能找到类资源 {}", resourcePath); negativeResourceCache.add(name); return null; } classStream = classResource.openStream(); if (DEBUG) - LogWrapper.LOGGER.warn("Loading class {} from resource {}", name, classResource); + LogWrapper.LOGGER.warn("从资源 {} 加载类 {}", name, classResource); final byte[] data = readFully(classStream); resourceCache.put(name, data); return data; diff --git a/src/main/java/snw/kookbc/impl/message/ChannelMessageImpl.java b/src/main/java/snw/kookbc/impl/message/ChannelMessageImpl.java index 0eed6525..eeacbd13 100644 --- a/src/main/java/snw/kookbc/impl/message/ChannelMessageImpl.java +++ b/src/main/java/snw/kookbc/impl/message/ChannelMessageImpl.java @@ -1,6 +1,6 @@ package snw.kookbc.impl.message; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.User; import snw.jkook.entity.channel.NonCategoryChannel; @@ -17,7 +17,7 @@ import java.util.Collections; import java.util.Map; -import static snw.kookbc.util.GsonUtil.*; +import static snw.kookbc.util.JacksonUtil.*; public class ChannelMessageImpl extends MessageImpl implements ChannelMessage { @@ -135,17 +135,17 @@ public void delete() { @Override public void initialize() { final String id = getId(); - final JsonObject object = client.getNetworkClient() + final JsonNode object = client.getNetworkClient() .get(HttpAPIRoute.CHANNEL_MESSAGE_INFO.toFullURL() + "?msg_id=" + id); final BaseComponent component = client.getMessageBuilder().buildComponent(object); - final long timeStamp = getAsLong(object, "create_at"); + final long timeStamp = get(object, "create_at").asLong(); ChannelMessage quote = null; if (has(object, "quote")) { - final JsonObject rawQuote = getAsJsonObject(object, "quote"); - final String quoteId = getAsString(rawQuote, "id"); + final JsonNode rawQuote = get(object, "quote"); + final String quoteId = rawQuote.get("id").asText(); quote = client.getCore().getHttpAPI().getChannelMessage(quoteId); } - final String channelId = getAsString(object, "channel_id"); + final String channelId = get(object, "channel_id").asText(); final NonCategoryChannel channel = retrieveOwningChannel(channelId); this.component = component; this.timeStamp = timeStamp; diff --git a/src/main/java/snw/kookbc/impl/message/MessageImpl.java b/src/main/java/snw/kookbc/impl/message/MessageImpl.java index 4a1b856b..df638a14 100644 --- a/src/main/java/snw/kookbc/impl/message/MessageImpl.java +++ b/src/main/java/snw/kookbc/impl/message/MessageImpl.java @@ -18,9 +18,7 @@ package snw.kookbc.impl.message; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; import org.jetbrains.annotations.Nullable; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.User; @@ -36,6 +34,7 @@ import snw.kookbc.impl.entity.builder.MessageBuilder; import snw.kookbc.impl.network.HttpAPIRoute; import snw.kookbc.interfaces.LazyLoadable; +import snw.kookbc.util.JacksonUtil; import snw.kookbc.util.MapBuilder; import java.io.UnsupportedEncodingException; @@ -107,7 +106,7 @@ public long getTimeStamp() { @Override public Collection getUserByReaction(CustomEmoji customEmoji) { - JsonArray array; + JsonNode array; try { String rawStr = client.getNetworkClient().getRawContent( String.format( @@ -117,7 +116,8 @@ public Collection getUserByReaction(CustomEmoji customEmoji) { .toFullURL(), getId(), URLEncoder.encode(customEmoji.getId(), StandardCharsets.UTF_8.name()))); - array = JsonParser.parseString(rawStr).getAsJsonObject().getAsJsonArray("data"); + JsonNode root = JacksonUtil.parse(rawStr); + array = root.get("data"); } catch (BadResponseException e) { if (e.getCode() == 40300) { // 40300, so we should throw IllegalStateException throw new IllegalStateException(e); @@ -129,8 +129,8 @@ public Collection getUserByReaction(CustomEmoji customEmoji) { throw new Error("No UTF-8 encoding?"); } Collection result = new ArrayList<>(array.size()); - for (JsonElement element : array) { - result.add(client.getStorage().getUser(element.getAsJsonObject().get("id").getAsString())); + for (JsonNode element : array) { + result.add(client.getStorage().getUser(element.get("id").asText())); } return Collections.unmodifiableCollection(result); } diff --git a/src/main/java/snw/kookbc/impl/message/PrivateMessageImpl.java b/src/main/java/snw/kookbc/impl/message/PrivateMessageImpl.java index 338c5f2e..d65a8f56 100644 --- a/src/main/java/snw/kookbc/impl/message/PrivateMessageImpl.java +++ b/src/main/java/snw/kookbc/impl/message/PrivateMessageImpl.java @@ -18,15 +18,14 @@ package snw.kookbc.impl.message; -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; +import static snw.kookbc.util.JacksonUtil.get; +import static snw.kookbc.util.JacksonUtil.has; import java.util.Collections; import java.util.Map; import java.util.NoSuchElementException; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.User; @@ -100,8 +99,8 @@ public void initialize() { final String chatCode = get(client.getNetworkClient() .post(HttpAPIRoute.USER_CHAT_SESSION_CREATE.toFullURL(), // KOOK won't create multiple session Collections.singletonMap("target_id", getSender().getId())), - "code").getAsString(); - final JsonObject object; + "code").asText(); + final JsonNode object; try { object = client.getNetworkClient() .get(HttpAPIRoute.USER_CHAT_MESSAGE_INFO.toFullURL() + "?chat_code=" + chatCode + "&msg_id=" @@ -115,13 +114,12 @@ public void initialize() { throw e; } final BaseComponent component = client.getMessageBuilder().buildComponent(object); - long timeStamp = get(object, "create_at").getAsLong(); + long timeStamp = object.get("create_at").asLong(); PrivateMessage quote = null; - if (has(object, "quote")) { - JsonElement rawQuote = get(object, "quote"); - if (rawQuote.isJsonObject()) { - final JsonObject quoteObj = rawQuote.getAsJsonObject(); - final String quoteId = get(quoteObj, "id").getAsString(); + if (object.has("quote")) { + JsonNode rawQuote = object.get("quote"); + if (rawQuote.isObject()) { + final String quoteId = rawQuote.get("id").asText(); quote = client.getCore().getHttpAPI().getPrivateMessage(getSender(), quoteId); } } diff --git a/src/main/java/snw/kookbc/impl/mixin/MixinServiceKookBC.java b/src/main/java/snw/kookbc/impl/mixin/MixinServiceKookBC.java index 96afdcaf..9e667659 100644 --- a/src/main/java/snw/kookbc/impl/mixin/MixinServiceKookBC.java +++ b/src/main/java/snw/kookbc/impl/mixin/MixinServiceKookBC.java @@ -24,6 +24,21 @@ */ package snw.kookbc.impl.mixin; +import snw.kookbc.LaunchMain; +import snw.kookbc.impl.launch.IClassNameTransformer; +import snw.kookbc.impl.launch.IClassTransformer; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; import org.jetbrains.annotations.ApiStatus; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; @@ -39,7 +54,14 @@ import org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel; import org.spongepowered.asm.mixin.MixinEnvironment.Phase; import org.spongepowered.asm.mixin.throwables.MixinException; -import org.spongepowered.asm.service.*; +import org.spongepowered.asm.service.IClassBytecodeProvider; +import org.spongepowered.asm.service.IClassProvider; +import org.spongepowered.asm.service.IClassTracker; +import org.spongepowered.asm.service.ILegacyClassTransformer; +import org.spongepowered.asm.service.IMixinAuditTrail; +import org.spongepowered.asm.service.ITransformer; +import org.spongepowered.asm.service.ITransformerProvider; +import org.spongepowered.asm.service.MixinServiceAbstract; import org.spongepowered.asm.transformers.MixinClassReader; import org.spongepowered.asm.util.Constants; import org.spongepowered.asm.util.Files; @@ -49,17 +71,6 @@ import org.spongepowered.include.com.google.common.collect.Sets; import org.spongepowered.include.com.google.common.io.ByteStreams; import org.spongepowered.include.com.google.common.io.Closeables; -import snw.kookbc.LaunchMain; -import snw.kookbc.impl.launch.IClassNameTransformer; -import snw.kookbc.impl.launch.IClassTransformer; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.*; /** * Mixin service for launchwrapper @@ -154,7 +165,7 @@ public void prepare() { public Phase getInitialPhase() { System.setProperty("mixin.env.disableRefMap", "true"); - if (MixinServiceKookBC.findInStackTrace("snw.kookbc.LaunchMain", "launch") > 189) { + if (MixinServiceKookBC.findInStackTrace("snw.kookbc.LaunchMain", "launch") > 190) { return Phase.DEFAULT; } return Phase.PREINIT; @@ -181,7 +192,7 @@ protected ILogger createLogger(String name) { public void init() { int launch = MixinServiceKookBC.findInStackTrace("snw.kookbc.LaunchMain", "launch"); if (launch < 4) { - MixinServiceKookBC.logger.error("MixinBootstrap.doInit() called during a tweak constructor! {}", launch); + MixinServiceKookBC.logger.error("MixinBootstrap.doInit() 在调整构造函数期间被调用!{}", launch); } List tweakClasses = GlobalProperties.get(MixinServiceKookBC.BLACKBOARD_KEY_TWEAKCLASSES); @@ -229,7 +240,7 @@ private void getContainersFromClassPath(ImmutableList.Builder for (URL url : sources) { try { URI uri = url.toURI(); - MixinServiceKookBC.logger.debug("Scanning {} for mixin tweaker", uri); + MixinServiceKookBC.logger.debug("正在扫描 {} 以查找 mixin 调整器", uri); if (!"file".equals(uri.getScheme()) || !Files.toFile(uri).exists()) { continue; } @@ -364,7 +375,7 @@ public Collection getTransformers() { } if (transformer instanceof IClassNameTransformer) { - MixinServiceKookBC.logger.debug("Found name transformer: {}", transformer.getClass().getName()); + MixinServiceKookBC.logger.debug("找到名称转换器:{}", transformer.getClass().getName()); this.nameTransformer = (IClassNameTransformer) transformer; } @@ -398,7 +409,7 @@ private List getDelegatedLegacyTransformers() { * list just once per environment and cache the result. */ private void buildTransformerDelegationList() { - MixinServiceKookBC.logger.debug("Rebuilding transformer delegation list:"); + MixinServiceKookBC.logger.debug("正在重建转换器委托列表:"); this.delegatedTransformers = new ArrayList<>(); for (ITransformer transformer : this.getTransformers()) { if (!(transformer instanceof ILegacyClassTransformer)) { @@ -415,14 +426,14 @@ private void buildTransformerDelegationList() { } } if (include && !legacyTransformer.isDelegationExcluded()) { - MixinServiceKookBC.logger.debug(" Adding: {}", transformerName); + MixinServiceKookBC.logger.debug(" 正在添加: {}", transformerName); this.delegatedTransformers.add(legacyTransformer); } else { - MixinServiceKookBC.logger.debug(" Excluding: {}", transformerName); + MixinServiceKookBC.logger.debug(" 正在排除: {}", transformerName); } } - MixinServiceKookBC.logger.debug("Transformer delegation list created with {} entries", this.delegatedTransformers.size()); + MixinServiceKookBC.logger.debug("转换器委托列表已创建,包含 {} 个条目", this.delegatedTransformers.size()); } /** @@ -539,7 +550,7 @@ private byte[] applyTransformers(String name, String transformedName, byte[] bas this.addTransformerExclusion(transformer.getName()); this.lock.clear(); - MixinServiceKookBC.logger.info("A re-entrant transformer '{}' was detected and will no longer process meta class data", + MixinServiceKookBC.logger.info("检测到重入转换器 '{}',将不再处理元类数据", transformer.getName()); } } @@ -563,7 +574,7 @@ private void findNameTransformer() { List transformers = LaunchMain.classLoader.getTransformers(); for (IClassTransformer transformer : transformers) { if (transformer instanceof IClassNameTransformer) { - MixinServiceKookBC.logger.debug("Found name transformer: {}", transformer.getClass().getName()); + MixinServiceKookBC.logger.debug("找到名称转换器:{}", transformer.getClass().getName()); this.nameTransformer = (IClassNameTransformer) transformer; } } diff --git a/src/main/java/snw/kookbc/impl/network/Bucket.java b/src/main/java/snw/kookbc/impl/network/Bucket.java index aa91bf95..1bf7a541 100644 --- a/src/main/java/snw/kookbc/impl/network/Bucket.java +++ b/src/main/java/snw/kookbc/impl/network/Bucket.java @@ -64,9 +64,9 @@ public synchronized void check() { if (availableTimes.get() <= 10) { // why not 0? Giving the server more time is better than real over limit final int resetTime = this.resetTime.get(); if (Objects.equals(client.getConfig().getString("over-limit-warning-log-level"), "INFO")) { - client.getCore().getLogger().info("Route '{}' over limit! Current reset time: {}", name, resetTime); + client.getCore().getLogger().info("路由 '{}' 超出限制!当前重置时间: {}", name, resetTime); } else { - client.getCore().getLogger().debug("Route '{}' over limit! Current reset time: {}", name, resetTime); + client.getCore().getLogger().debug("路由 '{}' 超出限制!当前重置时间: {}", name, resetTime); } RateLimitPolicy.getDefault().perform(client, name, resetTime); return; @@ -179,5 +179,13 @@ public static Bucket get(KBCClient client, HttpAPIRoute route) { bucketNameMap.put(HttpAPIRoute.FRIEND_LIST, "friend"); bucketNameMap.put(HttpAPIRoute.FRIEND_BLOCK, "friend/block"); bucketNameMap.put(HttpAPIRoute.FRIEND_UNBLOCK, "friend/unblock"); + // Thread (帖子频道) API - 新增支持 + bucketNameMap.put(HttpAPIRoute.THREAD_CATEGORY_LIST, "category/list"); + bucketNameMap.put(HttpAPIRoute.THREAD_CREATE, "thread/create"); + bucketNameMap.put(HttpAPIRoute.THREAD_REPLY, "thread/reply"); + bucketNameMap.put(HttpAPIRoute.THREAD_VIEW, "thread/view"); + bucketNameMap.put(HttpAPIRoute.THREAD_LIST, "thread/list"); + bucketNameMap.put(HttpAPIRoute.THREAD_DELETE, "thread/delete"); + bucketNameMap.put(HttpAPIRoute.THREAD_POST_LIST, "thread/post"); } } diff --git a/src/main/java/snw/kookbc/impl/network/Frame.java b/src/main/java/snw/kookbc/impl/network/Frame.java index 0e7c93c7..14d95e80 100644 --- a/src/main/java/snw/kookbc/impl/network/Frame.java +++ b/src/main/java/snw/kookbc/impl/network/Frame.java @@ -18,7 +18,7 @@ package snw.kookbc.impl.network; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import java.util.Objects; @@ -26,9 +26,9 @@ public class Frame { private final MessageType type; private final int sn; - private final JsonObject d; + private final JsonNode d; - public Frame(int s, int sn, JsonObject d) { + public Frame(int s, int sn, JsonNode d) { this.type = Objects.requireNonNull(MessageType.valueOf(s)); this.sn = sn; this.d = d; @@ -42,7 +42,7 @@ public int getSN() { return sn; } - public JsonObject getData() { + public JsonNode getData() { return d; } diff --git a/src/main/java/snw/kookbc/impl/network/HttpAPIRoute.java b/src/main/java/snw/kookbc/impl/network/HttpAPIRoute.java index 0b7b6982..f1d5d502 100644 --- a/src/main/java/snw/kookbc/impl/network/HttpAPIRoute.java +++ b/src/main/java/snw/kookbc/impl/network/HttpAPIRoute.java @@ -163,7 +163,17 @@ public enum HttpAPIRoute { FRIEND_BLOCK("/v3/friend/block"), - FRIEND_UNBLOCK("/v3/friend/unblock"); + FRIEND_UNBLOCK("/v3/friend/unblock"), + + // ------ THREAD (5e165b5098919053) ------ + + THREAD_CATEGORY_LIST("/v3/category/list"), + THREAD_CREATE("/v3/thread/create"), + THREAD_REPLY("/v3/thread/reply"), + THREAD_VIEW("/v3/thread/view"), + THREAD_LIST("/v3/thread/list"), + THREAD_DELETE("/v3/thread/delete"), + THREAD_POST_LIST("/v3/thread/post"); private static final Map map = new HashMap<>(); diff --git a/src/main/java/snw/kookbc/impl/network/IgnoreSNListenerImpl.java b/src/main/java/snw/kookbc/impl/network/IgnoreSNListenerImpl.java index 932a7644..17fe1c2f 100644 --- a/src/main/java/snw/kookbc/impl/network/IgnoreSNListenerImpl.java +++ b/src/main/java/snw/kookbc/impl/network/IgnoreSNListenerImpl.java @@ -25,12 +25,14 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import static snw.kookbc.util.VirtualThreadUtil.startVirtualThread; + public class IgnoreSNListenerImpl extends ListenerImpl { private final List processedSN = new LinkedList<>(); public IgnoreSNListenerImpl(KBCClient client, Connector connector) { super(client, connector); - new Thread(() -> { + startVirtualThread(() -> { while (client.isRunning()) { try { //noinspection BusyWait @@ -51,7 +53,7 @@ public IgnoreSNListenerImpl(KBCClient client, Connector connector) { protected void event(Frame frame) { synchronized (lck) { if (processedSN.contains(frame.getSN())) { - client.getCore().getLogger().warn("Duplicated message from remote. Ignored."); + client.getCore().getLogger().warn("收到来自远程的重复消息,已忽略"); return; } client.getSession().increaseSN(); diff --git a/src/main/java/snw/kookbc/impl/network/ListenerImpl.java b/src/main/java/snw/kookbc/impl/network/ListenerImpl.java index 5935b930..038f491f 100644 --- a/src/main/java/snw/kookbc/impl/network/ListenerImpl.java +++ b/src/main/java/snw/kookbc/impl/network/ListenerImpl.java @@ -18,18 +18,17 @@ package snw.kookbc.impl.network; -import static snw.kookbc.util.GsonUtil.get; +import static snw.kookbc.util.JacksonUtil.get; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.Iterator; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; +import snw.kookbc.util.JacksonUtil; import snw.jkook.command.CommandException; import snw.jkook.entity.User; @@ -63,10 +62,10 @@ public ListenerImpl(KBCClient client, Connector connector) { @Override public void handle(Frame frame) { if (!(frame.getType() == MessageType.PONG)) { // I hate PONG logging messages - client.getCore().getLogger().debug("Got payload frame: {}", frame); + client.getCore().getLogger().debug("收到载荷帧: {}", frame); } if (frame.getType() == null) { - client.getCore().getLogger().warn("Unknown event type!"); + client.getCore().getLogger().warn("未知的事件类型!"); return; } switch (frame.getType()) { @@ -77,79 +76,80 @@ public void handle(Frame frame) { hello(frame); break; case PING: - client.getCore().getLogger().debug("Impossible Message from remote: type is PING."); + client.getCore().getLogger().debug("收到不可能的远程消息: 类型为 PING"); break; case PONG: - client.getCore().getLogger().trace("Got PONG"); + client.getCore().getLogger().trace("收到 PONG"); connector.pong(); break; case RESUME: - client.getCore().getLogger().debug("Impossible Message from remote: type is RESUME."); + client.getCore().getLogger().debug("收到不可能的远程消息: 类型为 RESUME"); break; case RECONNECT: - client.getCore().getLogger().warn("Got RECONNECT request from remote. Attempting to reconnect."); + client.getCore().getLogger().warn("收到远程重连请求,正在尝试重连"); connector.requestReconnect(); break; case RESUME_ACK: - client.getCore().getLogger().info("Resume finished"); - client.getSession().setId(frame.getData().get("session_id").getAsString()); + client.getCore().getLogger().info("恢复完成"); + JsonNode sessionIdNode = frame.getData().get("session_id"); + if (sessionIdNode != null) { + client.getSession().setId(sessionIdNode.asText()); + } break; } } protected void event(Frame frame) { synchronized (lck) { - client.getCore().getLogger().debug("Got EVENT"); + client.getCore().getLogger().debug("收到 EVENT"); Session session = client.getSession(); AtomicInteger sn = session.getSN(); - Set buffer = session.getBuffer(); int expected = Session.UPDATE_FUNC.applyAsInt(sn.get()); int actual = frame.getSN(); + if (actual > expected) { - client.getCore().getLogger().warn("Unexpected wrong SN, expected {}, got {}", expected, actual); - client.getCore().getLogger().warn("We will process it later."); - buffer.add(frame); + client.getCore().getLogger().warn("意外的错误 SN,期望 {},实际收到 {}", expected, actual); + session.getBuffer().add(frame); } else if (expected == actual) { event0(frame); session.increaseSN(); saveSN(); - if (!buffer.isEmpty()) { - int continueId = sn.get() + 1; - do { - boolean found = false; - Iterator bufferIterator = buffer.iterator(); - while (bufferIterator.hasNext()) { - Frame bufFrame = bufferIterator.next(); - if (bufFrame.getSN() == continueId) { - found = true; // we found the frame matching the continueId, - // so we will continue after the frame got processed - event0(bufFrame); - session.increaseSN(); // make sure the SN will update! - saveSN(); - continueId++; - bufferIterator.remove(); // we won't need this frame, because it has processed - client.getCore().getLogger().debug("Processed message in buffer with SN {}", bufFrame.getSN()); - break; - } - } - if (!found) { - break; - } - } while (true); + + // 处理缓冲区中的连续帧 + int continueId = sn.get() + 1; + Frame bufFrame; + while ((bufFrame = find(continueId)) != null) { + event0(bufFrame); + session.increaseSN(); + saveSN(); + continueId++; + client.getCore().getLogger().debug("已处理缓冲消息,SN: {}", bufFrame.getSN()); } - } else if(client.getConfig().getBoolean("allow-warn-old-message")){ - client.getCore().getLogger().warn("Unexpected old message from remote. Dropped it."); + } else if (client.getConfig().getBoolean("allow-warn-old-message")) { + client.getCore().getLogger().warn("收到来自远程的意外旧消息,已丢弃"); + } + } + } + + private Frame find(int sn) { + for (Frame frame : client.getSession().getBuffer()) { + if (frame.getSN() == sn) { + client.getSession().getBuffer().remove(frame); + return frame; } } + return null; } protected void event0(Frame frame) { Event event; try { - event = client.getEventFactory().createEvent(frame.getData()); + // 直接使用 Jackson JsonNode 进行事件创建 + JsonNode jacksonData = frame.getData(); + event = client.getEventFactory().createEvent(jacksonData); } catch (Exception e) { - client.getCore().getLogger().error("Unable to create event from payload."); - client.getCore().getLogger().error("Event payload: {}", frame); + client.getCore().getLogger().error("无法从载荷创建事件"); + client.getCore().getLogger().error("事件载荷: {}", frame); e.printStackTrace(); return; } @@ -173,18 +173,22 @@ protected void saveSN() { writer.write(String.valueOf(client.getSession().getSN().get())); writer.close(); } catch (IOException e) { - client.getCore().getLogger().warn("Unable to write SN to local.", e); + client.getCore().getLogger().warn("无法将 SN 写入本地", e); } } } protected void hello(Frame frame) { - client.getCore().getLogger().debug("Got HELLO"); + client.getCore().getLogger().debug("收到 HELLO"); connector.setConnected(true); - JsonObject object = frame.getData(); - int status = get(object, "code").getAsInt(); + JsonNode object = frame.getData(); + JsonNode codeNode = object.get("code"); + int status = codeNode != null ? codeNode.asInt() : -1; if (status == 0) { - client.getSession().setId(get(object, "session_id").getAsString()); + JsonNode sessionIdNode = object.get("session_id"); + if (sessionIdNode != null) { + client.getSession().setId(sessionIdNode.asText()); + } } else { connector.requestReconnect(); } @@ -264,10 +268,10 @@ protected boolean executeCommand(Event event) { sender.sendPrivateMessage(content); } } catch (BadResponseException ex) { // too long? or timed out? however, we won't retry. - client.getCore().getLogger().error("Unable to send command failure message.", ex); + client.getCore().getLogger().error("无法发送命令失败消息", ex); } } - client.getCore().getLogger().error("Unexpected exception while we attempting to execute command from remote.", e); + client.getCore().getLogger().error("执行来自远程的命令时发生意外异常", e); return true; // Although this failed, but it is a valid command } } diff --git a/src/main/java/snw/kookbc/impl/network/NetworkClient.java b/src/main/java/snw/kookbc/impl/network/NetworkClient.java index 10487de7..93c77bc2 100644 --- a/src/main/java/snw/kookbc/impl/network/NetworkClient.java +++ b/src/main/java/snw/kookbc/impl/network/NetworkClient.java @@ -19,18 +19,25 @@ package snw.kookbc.impl.network; import static snw.kookbc.CLIOptions.NO_BUCKET; -import static snw.kookbc.util.GsonUtil.NORMAL_GSON; +import static snw.kookbc.util.JacksonUtil.parse; +import static snw.kookbc.util.JacksonUtil.toJson; import java.io.IOException; import java.time.Duration; import java.util.Map; import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.List; +import java.util.stream.Collectors; +import java.util.concurrent.TimeUnit; +import java.util.Arrays; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import snw.kookbc.util.JacksonUtil; +import snw.kookbc.util.VirtualThreadUtil; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -39,6 +46,9 @@ import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; +import okhttp3.ConnectionPool; +import okhttp3.Dispatcher; +import okhttp3.Protocol; import snw.jkook.exceptions.BadResponseException; import snw.kookbc.impl.KBCClient; @@ -46,18 +56,39 @@ public class NetworkClient { private final KBCClient kbcClient; private final String tokenWithPrefix; - private final OkHttpClient client; + private final ConnectionPool connectionPool; public NetworkClient(KBCClient kbcClient, String token) { this.kbcClient = kbcClient; tokenWithPrefix = "Bot " + token; + // 高性能连接池配置 - 适应高并发场景 + this.connectionPool = new ConnectionPool( + 50, // 最大空闲连接数(大幅提升以支持更高并发) + 15, // 连接存活时间(15分钟,减少频繁重连) + TimeUnit.MINUTES + ); + + // 虚拟线程调度器配置 - 利用 Java 21 性能优势 + Dispatcher dispatcher = new Dispatcher(VirtualThreadUtil.getHttpExecutor()); + dispatcher.setMaxRequests(200); // 最大并发请求数(提升至200) + dispatcher.setMaxRequestsPerHost(50); // 每个主机最大并发请求数(提升至50) + final OkHttpClient.Builder builder = new OkHttpClient.Builder() - .writeTimeout(Duration.ofMinutes(1)) - .readTimeout(Duration.ofMinutes(1)); + .connectionPool(connectionPool) + .dispatcher(dispatcher) + .connectTimeout(Duration.ofSeconds(10)) // 连接超时(优化为10秒) + .readTimeout(Duration.ofSeconds(45)) // 读取超时(增加到45秒,适应复杂响应) + .writeTimeout(Duration.ofSeconds(30)) // 写入超时(保持30秒) + .callTimeout(Duration.ofMinutes(3)) // 总调用超时(增加到3分钟) + .retryOnConnectionFailure(true) // 连接失败重试 + .followRedirects(true) // 自动跟随重定向 + .followSslRedirects(true) // 自动跟随SSL重定向 + .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)); // HTTP/2 优先,HTTP/1.1 兼容 + if (kbcClient.getConfig().getBoolean("ignore-ssl")) { - kbcClient.getCore().getLogger().warn("Ignoring SSL verification for networking!!!"); + kbcClient.getCore().getLogger().warn("网络请求忽略 SSL 验证!!!"); builder.sslSocketFactory(IgnoreSSLHelper.getSSLSocketFactory(), IgnoreSSLHelper.TRUST_MANAGER) .hostnameVerifier(IgnoreSSLHelper.getHostnameVerifier()); } @@ -68,13 +99,65 @@ public OkHttpClient getOkHttpClient() { return client; } - public JsonObject get(String fullUrl) { - return checkResponse(JsonParser.parseString(getRawContent(fullUrl)).getAsJsonObject()).getAsJsonObject("data"); + // ===== 连接池监控和统计 ===== + + /** + * 获取连接池统计信息 + * + * @return 连接池统计信息字符串 + */ + public String getConnectionPoolStats() { + return String.format( + "ConnectionPool Stats - Idle: %d, Total: %d, Active: %d", + connectionPool.idleConnectionCount(), + connectionPool.connectionCount(), + connectionPool.connectionCount() - connectionPool.idleConnectionCount() + ); + } + + /** + * 获取空闲连接数 + * + * @return 当前空闲连接数 + */ + public int getIdleConnectionCount() { + return connectionPool.idleConnectionCount(); + } + + /** + * 获取总连接数 + * + * @return 当前总连接数 + */ + public int getTotalConnectionCount() { + return connectionPool.connectionCount(); } - public JsonObject post(String fullUrl, Map body) { - return checkResponse(JsonParser.parseString(postContent(fullUrl, body)).getAsJsonObject()) - .getAsJsonObject("data"); + /** + * 获取活跃连接数 + * + * @return 当前活跃连接数 + */ + public int getActiveConnectionCount() { + return connectionPool.connectionCount() - connectionPool.idleConnectionCount(); + } + + /** + * 清理连接池中的空闲连接 + */ + public void evictIdleConnections() { + connectionPool.evictAll(); + kbcClient.getCore().getLogger().debug("已从连接池中清理所有空闲连接"); + } + + // Jackson API - 高性能JSON处理 + public JsonNode get(String fullUrl) { + return checkResponseJackson(parse(getRawContent(fullUrl))).get("data"); + } + + public JsonNode post(String fullUrl, Map body) { + return checkResponseJackson(parse(postContent(fullUrl, body))) + .get("data"); } public String getRawContent(String fullUrl) { @@ -88,7 +171,7 @@ public String getRawContent(String fullUrl) { } public String postContent(String fullUrl, Map body) { - return postContent(fullUrl, NORMAL_GSON.toJson(body), "application/json"); + return postContent(fullUrl, toJson(body), "application/json"); } public String postContent(String fullUrl, String body, String mediaType) { @@ -120,7 +203,7 @@ public String call(Request request) { final String body = Objects.requireNonNull(response.body()).string(); if (!response.isSuccessful()) { - kbcClient.getCore().getLogger().debug("Request failed. Full response object: {}", response); + kbcClient.getCore().getLogger().debug("请求失败,完整响应对象: {}", response); throw new BadResponseException(response.code(), body); } return body; @@ -144,18 +227,114 @@ protected Bucket getBucket(Request request) { } protected void logRequest(String method, String fullUrl, @Nullable String postBodyJson) { - kbcClient.getCore().getLogger().debug("Sending HTTP API Request: Method {}, URL: {}, Body (POST only): {}", + kbcClient.getCore().getLogger().debug("正在发送 HTTP API 请求: 方法 {}, URL: {}, 请求体 (仅 POST): {}", method, fullUrl, postBodyJson); } - // Return original object if check OK - public JsonObject checkResponse(JsonObject response) { - int code = response.get("code").getAsInt(); + // ===== 虚拟线程异步 API ===== + + /** + * 异步 GET 请求 - 使用虚拟线程 + * + * @param fullUrl 完整 URL + * @return 异步结果 + */ + public CompletableFuture getAsync(String fullUrl) { + return CompletableFuture.supplyAsync(() -> get(fullUrl), VirtualThreadUtil.getHttpExecutor()); + } + + /** + * 异步 POST 请求 - 使用虚拟线程 + * + * @param fullUrl 完整 URL + * @param body 请求体 + * @return 异步结果 + */ + public CompletableFuture postAsync(String fullUrl, Map body) { + return CompletableFuture.supplyAsync(() -> post(fullUrl, body), VirtualThreadUtil.getHttpExecutor()); + } + + /** + * 异步获取原始内容 - 使用虚拟线程 + * + * @param fullUrl 完整 URL + * @return 异步结果 + */ + public CompletableFuture getRawContentAsync(String fullUrl) { + return CompletableFuture.supplyAsync(() -> getRawContent(fullUrl), VirtualThreadUtil.getHttpExecutor()); + } + + /** + * 异步 POST 原始内容 - 使用虚拟线程 + * + * @param fullUrl 完整 URL + * @param body 请求体 + * @param mediaType 媒体类型 + * @return 异步结果 + */ + public CompletableFuture postContentAsync(String fullUrl, String body, String mediaType) { + return CompletableFuture.supplyAsync(() -> postContent(fullUrl, body, mediaType), VirtualThreadUtil.getHttpExecutor()); + } + + /** + * 批量异步 GET 请求 - 使用虚拟线程 + * + *

所有请求并行执行,显著提升性能 + * + * @param urls URL 列表 + * @return 批量异步结果 + */ + public CompletableFuture> batchGetAsync(List urls) { + List> futures = urls.stream() + .map(this::getAsync) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())); + } + + /** + * 批量异步 POST 请求 - 使用虚拟线程 + * + * @param requests 请求列表 (URL 和 Body 的映射) + * @return 批量异步结果 + */ + public CompletableFuture> batchPostAsync(Map> requests) { + List> futures = requests.entrySet().stream() + .map(entry -> postAsync(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())); + } + + /** + * 异步调用请求 - 使用虚拟线程 + * + *

底层方法,支持自定义 Request 对象 + * + * @param request OkHttp Request 对象 + * @return 异步结果 + */ + public CompletableFuture callAsync(Request request) { + return CompletableFuture.supplyAsync(() -> call(request), VirtualThreadUtil.getHttpExecutor()); + } + + // ===== 原有同步方法(保持向后兼容)===== + + // Jackson响应检查 + public JsonNode checkResponseJackson(JsonNode response) { + int code = response.get("code").asInt(); if (code != 0) { - String message = response.get("message").getAsString(); + String message = response.get("message").asText(); throw new BadResponseException(code, message); } return response; } + } diff --git a/src/main/java/snw/kookbc/impl/network/Session.java b/src/main/java/snw/kookbc/impl/network/Session.java index 07bfefbf..5196263e 100644 --- a/src/main/java/snw/kookbc/impl/network/Session.java +++ b/src/main/java/snw/kookbc/impl/network/Session.java @@ -57,4 +57,4 @@ public void setId(String id) { public Set getBuffer() { return buffer; } -} +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/network/webhook/EncryptUtils.java b/src/main/java/snw/kookbc/impl/network/webhook/EncryptUtils.java index 59aa7ec3..43193c3d 100644 --- a/src/main/java/snw/kookbc/impl/network/webhook/EncryptUtils.java +++ b/src/main/java/snw/kookbc/impl/network/webhook/EncryptUtils.java @@ -18,7 +18,8 @@ package snw.kookbc.impl.network.webhook; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import snw.kookbc.util.JacksonUtil; import snw.kookbc.impl.KBCClient; import javax.crypto.Cipher; @@ -36,7 +37,7 @@ public static String decrypt(KBCClient client, String src) { if (key != null && !key.isEmpty()) { // decryption String decodedBase64 = new String( Base64.getDecoder().decode( - JsonParser.parseString(src).getAsJsonObject().get("encrypt").getAsString() + JacksonUtil.parse(src).get("encrypt").asText() ) ); String iv = decodedBase64.substring(0, 16); diff --git a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequest.java b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequest.java index 0ba4079f..9cf9335d 100644 --- a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequest.java +++ b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequest.java @@ -18,8 +18,9 @@ package snw.kookbc.impl.network.webhook; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import snw.kookbc.util.JacksonUtil; import net.freeutils.httpserver.HTTPServer; import snw.kookbc.impl.KBCClient; import snw.kookbc.interfaces.network.webhook.Request; @@ -32,7 +33,7 @@ import static snw.kookbc.util.Util.decompressDeflate; import static snw.kookbc.util.Util.inputStreamToByteArray; -public class JLHttpRequest implements Request { +public class JLHttpRequest implements Request { private final KBCClient client; private final HTTPServer.Request request; private final HTTPServer.Response response; @@ -68,13 +69,13 @@ public String getRawBody() { } @Override - public JsonObject toJson() { + public JsonNode toJson() { final String rawBody = getRawBody(); if (rawBody.isEmpty()) { - return new JsonObject(); + return JacksonUtil.createObjectNode(); } final String decryptedBody = EncryptUtils.decrypt(client, rawBody); - return JsonParser.parseString(decryptedBody).getAsJsonObject(); + return JacksonUtil.parse(decryptedBody); } @Override diff --git a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequestHandler.java b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequestHandler.java index da72ee72..ee0e9747 100644 --- a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequestHandler.java +++ b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequestHandler.java @@ -18,16 +18,18 @@ package snw.kookbc.impl.network.webhook; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import snw.kookbc.util.JacksonUtil; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.network.Frame; import snw.kookbc.interfaces.network.FrameHandler; import snw.kookbc.interfaces.network.webhook.Request; import snw.kookbc.interfaces.network.webhook.RequestHandler; -import static snw.kookbc.util.GsonUtil.*; +import static snw.kookbc.util.JacksonUtil.*; -public class JLHttpRequestHandler implements RequestHandler { +public class JLHttpRequestHandler implements RequestHandler { private final String ourToken; private final FrameHandler handler; @@ -39,31 +41,44 @@ public JLHttpRequestHandler(KBCClient client, FrameHandler handler) { } } - @Override - public void handle(Request request) { - final JsonObject object = request.toJson(); - final int signalType = get(object, "s").getAsInt(); + public void handle(Request request) { + final JsonNode object = request.toJson(); + final int signalType = object.get("s").asInt(); final int sn; - if (has(object, "sn")) { - sn = get(object, "sn").getAsInt(); + JsonNode snNode = object.get("sn"); + if (snNode != null && !snNode.isNull()) { + sn = snNode.asInt(); } else { sn = -1; } - final JsonObject data = get(object, "d").getAsJsonObject(); + final JsonNode data = object.get("d"); Frame frame = new Frame(signalType, sn, data); - final String gotToken = get(frame.getData(), "verify_token").getAsString(); + + JsonNode verifyTokenNode = frame.getData().get("verify_token"); + if (verifyTokenNode == null || verifyTokenNode.isNull()) { + request.reply(400, ""); + return; + } + final String gotToken = verifyTokenNode.asText(); if (!ourToken.equals(gotToken)) { request.reply(400, ""); return; } - if (has(frame.getData(), "channel_type")) { - final String channelType = get(frame.getData(), "channel_type").getAsString(); + + JsonNode channelTypeNode = frame.getData().get("channel_type"); + if (channelTypeNode != null && !channelTypeNode.isNull()) { + final String channelType = channelTypeNode.asText(); if ("WEBHOOK_CHALLENGE".equals(channelType)) { // challenge part - String challengeValue = frame.getData().get("challenge").getAsString(); - JsonObject obj = new JsonObject(); - obj.addProperty("challenge", challengeValue); - String challengeJson = NORMAL_GSON.toJson(obj); + JsonNode challengeNode = frame.getData().get("challenge"); + if (challengeNode == null || challengeNode.isNull()) { + request.reply(400, ""); + return; + } + String challengeValue = challengeNode.asText(); + ObjectNode obj = JacksonUtil.createObjectNode(); + obj.put("challenge", challengeValue); + String challengeJson = JacksonUtil.toJsonString(obj); request.reply(200, challengeJson); return; // end challenge part diff --git a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequestWrapper.java b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequestWrapper.java index c86d4ab1..8ed1b55e 100644 --- a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequestWrapper.java +++ b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequestWrapper.java @@ -18,7 +18,7 @@ package snw.kookbc.impl.network.webhook; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import net.freeutils.httpserver.HTTPServer; import snw.kookbc.impl.KBCClient; import snw.kookbc.interfaces.network.webhook.RequestHandler; @@ -27,9 +27,9 @@ public class JLHttpRequestWrapper implements HTTPServer.ContextHandler { private final KBCClient client; - private final RequestHandler handler; + private final RequestHandler handler; - public JLHttpRequestWrapper(KBCClient client, RequestHandler handler) { + public JLHttpRequestWrapper(KBCClient client, RequestHandler handler) { this.client = client; this.handler = handler; } @@ -44,7 +44,7 @@ public int serve(HTTPServer.Request request, HTTPServer.Response response) throw try { handler.handle(wrapped); } catch (Exception e) { - client.getCore().getLogger().error("Unable to process request", e); + client.getCore().getLogger().error("无法处理请求", e); throw new IOException(e); } if (!wrapped.isReplyPresent()) { diff --git a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpWebhookNetworkSystem.java b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpWebhookNetworkSystem.java index ef235959..742f9683 100644 --- a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpWebhookNetworkSystem.java +++ b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpWebhookNetworkSystem.java @@ -54,7 +54,7 @@ public JLHttpWebhookNetworkSystem(KBCClient client, @Nullable FrameHandler handl @Override public void start() { try { - client.getCore().getLogger().debug("Initializing SN from local file."); + client.getCore().getLogger().debug("正在从本地文件初始化 SN"); int initSN = 0; File snfile = new File(client.getPluginsFolder(), "sn"); if (snfile.exists()) { @@ -68,12 +68,12 @@ public void start() { throw new RuntimeException(e); } server.start(); - client.getCore().getLogger().info("Webhook HTTP server listening on port " + port); + client.getCore().getLogger().info("Webhook HTTP 服务器正在监听端口 " + port); } @Override public void stop() { - client.getCore().getLogger().info("Stopping Webhook HTTP server"); + client.getCore().getLogger().info("正在停止 Webhook HTTP 服务器"); server.stop(); } } diff --git a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpWebhookServer.java b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpWebhookServer.java index f717d029..e4a9fcbc 100644 --- a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpWebhookServer.java +++ b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpWebhookServer.java @@ -18,16 +18,16 @@ package snw.kookbc.impl.network.webhook; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import net.freeutils.httpserver.HTTPServer; import snw.kookbc.impl.KBCClient; import snw.kookbc.interfaces.network.FrameHandler; import snw.kookbc.interfaces.network.webhook.RequestHandler; import snw.kookbc.interfaces.network.webhook.WebhookServer; -import snw.kookbc.util.PrefixThreadFactory; import java.io.IOException; -import java.util.concurrent.Executors; + +import static snw.kookbc.util.VirtualThreadUtil.newVirtualThreadExecutor; public class JLHttpWebhookServer implements WebhookServer { private final KBCClient client; @@ -38,7 +38,7 @@ public JLHttpWebhookServer(KBCClient client, String route, int port, FrameHandle this.client = client; this.route = route; this.server = new HTTPServer(port); - this.server.setExecutor(Executors.newCachedThreadPool(new PrefixThreadFactory("Webhook Thread #"))); + this.server.setExecutor(newVirtualThreadExecutor("Webhook-Thread")); this.setHandler(new JLHttpRequestHandler(client, listener)); } @@ -57,7 +57,7 @@ public void stop() { } @Override - public void setHandler(RequestHandler handler) { + public void setHandler(RequestHandler handler) { if (server != null) { HTTPServer.VirtualHost virtualHost = server.getVirtualHost(null); virtualHost.addContext('/' + route, new JLHttpRequestWrapper(client, handler), "POST"); diff --git a/src/main/java/snw/kookbc/impl/network/ws/Connector.java b/src/main/java/snw/kookbc/impl/network/ws/Connector.java index 6189ef17..25ff6035 100644 --- a/src/main/java/snw/kookbc/impl/network/ws/Connector.java +++ b/src/main/java/snw/kookbc/impl/network/ws/Connector.java @@ -29,6 +29,7 @@ // The Connector. It will communicate with Kook WebSocket Server. public class Connector { private final KBCClient kbcClient; + private final ReconnectStrategy reconnectStrategy; private String wsLink = ""; private WebSocket ws; private volatile boolean firstConnected = false; // sub-threads should not work on startup @@ -40,6 +41,7 @@ public class Connector { public Connector(KBCClient kbcClient) { this.kbcClient = kbcClient; + this.reconnectStrategy = new ReconnectStrategy(); new PingThread().start(); new Reconnector(kbcClient, reconnectLock, this).start(); } @@ -51,45 +53,79 @@ public void start() { } private void start0() { - getGateway(); - start1(); + try { + getGateway(); + start1(); + } catch (Exception e) { + kbcClient.getCore().getLogger().error("连接启动失败: {}", e.getMessage(), e); + throw e; // 向上抛出以便 restart() 处理 + } } private void start1() { do { connected = false; - // if self connected is true, call shutdownHttp() - if (kbcClient.getNetworkClient().get(HttpAPIRoute.USER_ME.toFullURL()).get("online").getAsBoolean()) { - shutdownHttp(); + try { + // if self connected is true, call shutdownHttp() + if (kbcClient.getNetworkClient().get(HttpAPIRoute.USER_ME.toFullURL()).get("online").asBoolean()) { + shutdownHttp(); + } + } catch (Exception e) { + kbcClient.getCore().getLogger().warn("检查在线状态失败(可能是网络问题),继续尝试连接: {}", e.getMessage()); } + int times = 0; do { - ws = kbcClient.getNetworkClient().newWebSocket( - new Request.Builder() - .url(wsLink) - .build(), - new WebSocketMessageProcessor(kbcClient, this) - ); - long ts = System.currentTimeMillis(); - while (System.currentTimeMillis() - ts < 6000L) { - if (connected) { - break; + try { + ws = kbcClient.getNetworkClient().newWebSocket( + new Request.Builder() + .url(wsLink) + .build(), + new WebSocketMessageProcessor(kbcClient, this) + ); + long ts = System.currentTimeMillis(); + // 增加超时时间从 6 秒到 15 秒,适应网络波动 + while (System.currentTimeMillis() - ts < 15000L) { + if (connected) { + break; + } + Thread.sleep(100); // 避免忙等待 } + } catch (Exception e) { + kbcClient.getCore().getLogger().warn("WebSocket 连接尝试失败 (第 {} 次): {}", times + 1, e.getMessage()); } - if (!connected) { // I WASTE 2 HOURS ON THIS + + if (!connected) { shutdownWs(); times++; } } while (!connected && times < 2); - if (!connected) { // if this round failed, then we need to get a new WS link - getGateway(); + + if (!connected) { + // if this round failed, then we need to get a new WS link + try { + getGateway(); + } catch (Exception e) { + kbcClient.getCore().getLogger().error("获取 Gateway 失败(可能是 DNS 解析或网络问题): {}", e.getMessage()); + throw new RuntimeException("无法获取 WebSocket Gateway", e); + } } } while (!connected); - kbcClient.getCore().getLogger().info("WebSocket Connection OK"); + + kbcClient.getCore().getLogger().info("WebSocket 连接成功"); + // 连接成功后通知重连策略 + reconnectStrategy.onConnectionSuccess(); } private void getGateway() { - wsLink = kbcClient.getNetworkClient().get(HttpAPIRoute.GATEWAY.toFullURL()).get("url").getAsString(); + try { + wsLink = kbcClient.getNetworkClient().get(HttpAPIRoute.GATEWAY.toFullURL()).get("url").asText(); + kbcClient.getCore().getLogger().debug("成功获取 WebSocket Gateway: {}", wsLink); + } catch (Exception e) { + kbcClient.getCore().getLogger().error("获取 WebSocket Gateway 失败: {}", e.getMessage(), e); + // 抛出异常以便上层处理 + throw new RuntimeException("无法获取 WebSocket Gateway(可能是 DNS 解析失败或网络连接问题)", e); + } } public void shutdown() { @@ -106,19 +142,51 @@ private void shutdownWs() { public void shutdownHttp() { try { - kbcClient.getCore().getLogger().debug("Called HTTP Bot offline API. Response: {}", kbcClient.getNetworkClient().postContent(HttpAPIRoute.USER_BOT_OFFLINE.toFullURL(), "", "")); + kbcClient.getCore().getLogger().debug("已调用 HTTP Bot 离线 API,响应: {}", kbcClient.getNetworkClient().postContent(HttpAPIRoute.USER_BOT_OFFLINE.toFullURL(), "", "")); } catch (Exception e) { - kbcClient.getCore().getLogger().error("Unexpected Exception when we attempting to request HTTP Bot offline API.", e); + kbcClient.getCore().getLogger().error("尝试请求 HTTP Bot 离线 API 时发生意外异常", e); } } // following methods should be called by other class: public synchronized void restart() { + restart(null); + } + + public synchronized void restart(Throwable exception) { + // 检查是否应该重连 + if (!reconnectStrategy.shouldReconnect(exception)) { + kbcClient.getCore().getLogger().error("重连策略决定不再重连,停止重连尝试"); + kbcClient.getCore().getLogger().info(reconnectStrategy.getStatisticsReport()); + return; + } + + // 关闭当前连接 shutdown(); kbcClient.getSession().getSN().set(0); kbcClient.getSession().getBuffer().clear(); - start0(); + + // 计算延迟并等待 + int delay = reconnectStrategy.getNextDelay(); + kbcClient.getCore().getLogger().info("准备在 {} 秒后重连...", delay); + + if (!reconnectStrategy.waitBeforeReconnect(delay)) { + kbcClient.getCore().getLogger().warn("重连等待被中断,取消重连"); + return; + } + + // 尝试重连 + try { + kbcClient.getCore().getLogger().info("开始重连..."); + start0(); + reconnectStrategy.onConnectionSuccess(); + } catch (Exception e) { + kbcClient.getCore().getLogger().error("重连过程中发生异常", e); + reconnectStrategy.onConnectionFailure(); + // 递归重试(会被 shouldReconnect 限制次数) + restart(e); + } } public void setConnected(boolean connected) { @@ -153,7 +221,7 @@ public void ping() { setPingOk(true); return; } - kbcClient.getCore().getLogger().trace("Attempting to PING."); + kbcClient.getCore().getLogger().trace("正在尝试 PING"); setPingOk(false); boolean queued = ws.send(String.format("{\"s\":2,\"sn\":%s}", kbcClient.getSession().getSN().get())); Validate.isTrue(queued, "Unable to queue ping request"); @@ -194,6 +262,10 @@ public boolean isConnected() { return connected; } + public ReconnectStrategy getReconnectStrategy() { + return reconnectStrategy; + } + protected class PingThread extends Thread { public PingThread() { diff --git a/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java b/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java new file mode 100644 index 00000000..0ceb0469 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java @@ -0,0 +1,361 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.network.ws; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.UnknownHostException; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 智能重连策略 + * + *

提供完整的断线重连机制,包括: + *

    + *
  • 指数退避算法 - 避免频繁重连
  • + *
  • 无限重试模式 - 持续重连直到成功或遇到不可恢复异常
  • + *
  • 异常分类处理 - 区分可恢复和不可恢复错误
  • + *
  • 统计监控 - 记录重连历史和性能指标
  • + *
+ * + *

指数退避序列(秒): + *

1, 2, 4, 8, 16, 32, 60, 60, 60...
+ * + *

异常处理策略: + *

    + *
  • 网络异常(DNS、连接超时、IO错误)- 允许无限重连
  • + *
  • 认证失败(401/403)- 不允许重连,需要用户介入
  • + *
  • 其他异常 - 记录后允许重连
  • + *
+ * + *

自动重置: + *

    + *
  • 连接保持稳定 5 分钟后自动重置重试计数器
  • + *
+ * + * @since KookBC 0.53.0 + */ +public class ReconnectStrategy { + private static final Logger logger = LoggerFactory.getLogger(ReconnectStrategy.class); + + // ===== 重连配置常量 ===== + + /** + * 指数退避延迟序列(秒) + * 1, 2, 4, 8, 16, 32, 60, 60... + */ + private static final int[] BACKOFF_DELAYS = {1, 2, 4, 8, 16, 32, 60}; + + /** + * 最大退避延迟(秒) + */ + private static final int MAX_BACKOFF_DELAY = 60; + + /** + * 成功连接后重置计数器的等待时间(秒) + * 如果连接保持稳定5分钟,则认为连接恢复正常,重置重试计数 + */ + private static final int RESET_THRESHOLD_SECONDS = 300; + + // ===== 重连状态 ===== + + private final AtomicInteger attemptCount = new AtomicInteger(0); + private final AtomicLong totalReconnects = new AtomicLong(0); + private final AtomicLong successfulReconnects = new AtomicLong(0); + private final AtomicLong failedReconnects = new AtomicLong(0); + + private volatile Instant lastSuccessfulConnection = null; + private volatile Instant lastAttemptTime = null; + private volatile Throwable lastException = null; + + /** + * 创建重连策略(无限重试) + */ + public ReconnectStrategy() { + // 无需初始化,重连将持续到连接成功或遇到不可恢复的异常 + } + + /** + * 检查是否应该重连 + * + * @param exception 导致断线的异常(可为 null) + * @return true 如果应该重连,false 如果应该放弃 + */ + public boolean shouldReconnect(Throwable exception) { + lastException = exception; + lastAttemptTime = Instant.now(); + + // 分析异常类型 + if (exception != null) { + if (isUnrecoverableException(exception)) { + logger.error("遇到不可恢复的异常,停止重连: {}", exception.getMessage()); + return false; + } + + // 记录可恢复的异常类型 + logRecoverableException(exception); + } + + // 无限重试,只要不是不可恢复的异常就继续重连 + return true; + } + + /** + * 判断异常是否不可恢复 + * + * @param exception 异常对象 + * @return true 如果是不可恢复的异常 + */ + private boolean isUnrecoverableException(Throwable exception) { + String message = exception.getMessage(); + if (message == null) { + return false; + } + + // 认证失败 - 需要用户更新 Token + if (message.contains("401") || message.contains("Unauthorized") || message.contains("Invalid token")) { + logger.error("认证失败,请检查 Bot Token 是否正确"); + return true; + } + + // 403 - Bot 被封禁 + if (message.contains("403") || message.contains("Forbidden")) { + logger.error("Bot 访问被拒绝,可能被封禁"); + return true; + } + + return false; + } + + /** + * 记录可恢复的异常信息 + */ + private void logRecoverableException(Throwable exception) { + String exceptionType = exception.getClass().getSimpleName(); + String message = exception.getMessage(); + + // DNS 解析失败 + if (exception instanceof UnknownHostException) { + logger.warn("DNS 解析失败: {},将在延迟后重试(可能是网络或 DNS 问题)", message); + return; + } + + // 连接超时 + if (exceptionType.contains("Timeout") || (message != null && message.contains("timeout"))) { + logger.warn("连接超时: {},将在延迟后重试", message); + return; + } + + // IO 异常 + if (exceptionType.contains("IOException")) { + logger.warn("网络 I/O 异常: {},将在延迟后重试", message); + return; + } + + // 其他可恢复异常 + logger.warn("遇到可恢复异常 ({}): {},将在延迟后重试", exceptionType, message); + } + + /** + * 计算下一次重连的延迟时间(秒) + * + * @return 延迟秒数 + */ + public int getNextDelay() { + int attempt = attemptCount.getAndIncrement(); + totalReconnects.incrementAndGet(); + + // 使用指数退避算法 + int delay; + if (attempt < BACKOFF_DELAYS.length) { + delay = BACKOFF_DELAYS[attempt]; + } else { + delay = MAX_BACKOFF_DELAY; + } + + logger.info("重连延迟: {} 秒 (第 {} 次重试,将持续重试直到连接成功)", delay, attempt + 1); + return delay; + } + + /** + * 执行延迟等待 + * + * @param delaySeconds 延迟秒数 + * @return true 如果成功等待,false 如果被中断 + */ + public boolean waitBeforeReconnect(int delaySeconds) { + try { + logger.debug("等待 {} 秒后重连...", delaySeconds); + TimeUnit.SECONDS.sleep(delaySeconds); + return true; + } catch (InterruptedException e) { + logger.warn("重连等待被中断"); + Thread.currentThread().interrupt(); + return false; + } + } + + /** + * 标记连接成功 + *

检查是否应该重置重试计数器 + */ + public void onConnectionSuccess() { + Instant now = Instant.now(); + + // 如果上次成功连接已经是 5 分钟前,重置计数器 + if (lastSuccessfulConnection != null) { + Duration stableTime = Duration.between(lastSuccessfulConnection, now); + if (stableTime.getSeconds() >= RESET_THRESHOLD_SECONDS) { + logger.info("连接保持稳定 {} 分钟,重置重连计数器", stableTime.toMinutes()); + reset(); + } + } + + lastSuccessfulConnection = now; + if (attemptCount.get() > 0) { + successfulReconnects.incrementAndGet(); + logger.info("重连成功!(共尝试 {} 次)", attemptCount.get()); + } + } + + /** + * 标记连接失败 + */ + public void onConnectionFailure() { + failedReconnects.incrementAndGet(); + } + + /** + * 重置重连状态 + */ + public void reset() { + attemptCount.set(0); + lastException = null; + } + + /** + * 完全重置所有统计信息 + */ + public void fullReset() { + reset(); + totalReconnects.set(0); + successfulReconnects.set(0); + failedReconnects.set(0); + lastSuccessfulConnection = null; + lastAttemptTime = null; + } + + // ===== 统计信息 ===== + + /** + * 获取当前重试次数 + */ + public int getCurrentAttempt() { + return attemptCount.get(); + } + + /** + * 获取总重连次数 + */ + public long getTotalReconnects() { + return totalReconnects.get(); + } + + /** + * 获取成功重连次数 + */ + public long getSuccessfulReconnects() { + return successfulReconnects.get(); + } + + /** + * 获取失败重连次数 + */ + public long getFailedReconnects() { + return failedReconnects.get(); + } + + /** + * 获取最后一次异常 + */ + public Throwable getLastException() { + return lastException; + } + + /** + * 获取上次成功连接时间 + */ + public Instant getLastSuccessfulConnection() { + return lastSuccessfulConnection; + } + + /** + * 获取上次尝试重连时间 + */ + public Instant getLastAttemptTime() { + return lastAttemptTime; + } + + /** + * 获取重连成功率 + */ + public double getSuccessRate() { + long total = totalReconnects.get(); + return total > 0 ? (double) successfulReconnects.get() / total : 0.0; + } + + /** + * 获取统计报告 + */ + public String getStatisticsReport() { + return String.format( + """ + 重连统计报告: + =========================================== + 当前重试: %d (无限重试模式) + 总重连次数: %d + 成功重连: %d + 失败重连: %d + 成功率: %.2f%% + 上次成功连接: %s + 上次尝试时间: %s + 上次异常: %s + """, + getCurrentAttempt(), + getTotalReconnects(), + getSuccessfulReconnects(), + getFailedReconnects(), + getSuccessRate() * 100, + lastSuccessfulConnection != null ? lastSuccessfulConnection.toString() : "N/A", + lastAttemptTime != null ? lastAttemptTime.toString() : "N/A", + lastException != null ? lastException.getMessage() : "N/A" + ); + } + + @Override + public String toString() { + return String.format("ReconnectStrategy[attempt=%d (unlimited), success=%.2f%%]", + getCurrentAttempt(), getSuccessRate() * 100); + } +} diff --git a/src/main/java/snw/kookbc/impl/network/ws/Reconnector.java b/src/main/java/snw/kookbc/impl/network/ws/Reconnector.java index 4f2c73fd..1b6e47af 100644 --- a/src/main/java/snw/kookbc/impl/network/ws/Reconnector.java +++ b/src/main/java/snw/kookbc/impl/network/ws/Reconnector.java @@ -37,22 +37,43 @@ public Reconnector(KBCClient client, Object lock, Connector connector) { public void run() { while (client.isRunning()) { synchronized (lock) { + // 等待重连请求 while (!connector.isRequireReconnect()) { try { lock.wait(); } catch (InterruptedException e) { + client.getCore().getLogger().debug("Reconnector 线程被中断"); + Thread.currentThread().interrupt(); return; } } + + // 再次检查客户端是否还在运行 if (!client.isRunning()) { + client.getCore().getLogger().debug("客户端已停止,Reconnector 退出"); return; } - if (connector.isConnected()) { - continue; + + // 在同一个锁内检查连接状态并执行重连,避免 TOCTOU 竞态条件 + if (!connector.isConnected()) { + try { + client.getCore().getLogger().info("Reconnector 检测到断线,开始重连流程"); + connector.restart(); + client.getCore().getLogger().info("Reconnector 重连流程完成"); + } catch (Exception e) { + client.getCore().getLogger().error("Reconnector 重连过程中发生未捕获异常", e); + // 异常已经在 connector.restart() 中处理,这里只是记录 + } finally { + // 无论成功或失败,都标记重连请求已处理 + connector.reconnectOk(); + } + } else { + // 已经连接上了,可能是其他地方已经处理了重连 + client.getCore().getLogger().debug("Reconnector 检测到连接已恢复,跳过重连"); + connector.reconnectOk(); } - connector.restart(); - connector.reconnectOk(); } } + client.getCore().getLogger().debug("Reconnector 线程退出"); } } diff --git a/src/main/java/snw/kookbc/impl/network/ws/WebSocketMessageProcessor.java b/src/main/java/snw/kookbc/impl/network/ws/WebSocketMessageProcessor.java index 072eec72..a89b459e 100644 --- a/src/main/java/snw/kookbc/impl/network/ws/WebSocketMessageProcessor.java +++ b/src/main/java/snw/kookbc/impl/network/ws/WebSocketMessageProcessor.java @@ -18,8 +18,7 @@ package snw.kookbc.impl.network.ws; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; @@ -30,13 +29,13 @@ import snw.kookbc.impl.network.Frame; import snw.kookbc.impl.network.ListenerFactory; import snw.kookbc.interfaces.network.FrameHandler; +import snw.kookbc.util.JacksonUtil; import java.io.IOException; import java.net.ProtocolException; +import java.net.UnknownHostException; import java.util.zip.DataFormatException; -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; import static snw.kookbc.util.Util.decompressDeflate; public class WebSocketMessageProcessor extends WebSocketListener { @@ -61,36 +60,92 @@ public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { @Override public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { super.onMessage(webSocket, text); - JsonObject object = JsonParser.parseString(text).getAsJsonObject(); - Frame frame = new Frame(get(object, "s").getAsInt(), has(object, "sn") ? get(object, "sn").getAsInt() : -1, object.getAsJsonObject("d")); - listener.executeEvent(frame); + try { + JsonNode object = JacksonUtil.parse(text); + JsonNode sNode = object.get("s"); + JsonNode snNode = object.get("sn"); + JsonNode dNode = object.get("d"); + + int s = sNode.asInt(); + int sn = snNode != null ? snNode.asInt() : -1; + + Frame frame = new Frame(s, sn, dNode); + listener.executeEvent(frame); + } catch (Exception e) { + client.getCore().getLogger().error("处理 WebSocket 消息时发生异常: {}, 原始消息: {}", e.getMessage(), text, e); + // 不触发重连,因为可能只是单个消息格式错误 + } } // for compressed messages, so we will extract it before processing @Override public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) { super.onMessage(webSocket, bytes); - String res; + String res = null; try { res = new String(decompressDeflate(bytes.toByteArray())); + JsonNode object = JacksonUtil.parse(res); + JsonNode sNode = object.get("s"); + JsonNode snNode = object.get("sn"); + JsonNode dNode = object.get("d"); + + int s = sNode.asInt(); + int sn = snNode != null ? snNode.asInt() : -1; + + Frame frame = new Frame(s, sn, dNode); + listener.executeEvent(frame); } catch (DataFormatException | IOException e) { - client.getCore().getLogger().error("Unable to decompress data", e); - return; + client.getCore().getLogger().error("解压缩 WebSocket 数据失败: {}, 数据长度: {} 字节", e.getMessage(), bytes.size(), e); + // 不触发重连,因为可能只是单个消息损坏 + } catch (Exception e) { + if (res != null) { + client.getCore().getLogger().error("处理压缩 WebSocket 消息时发生异常: {}, 原始消息: {}", e.getMessage(), res, e); + } else { + client.getCore().getLogger().error("处理压缩 WebSocket 消息时发生异常: {}", e.getMessage(), e); + } + // 不触发重连,因为可能只是单个消息格式错误 } - JsonObject object = JsonParser.parseString(res).getAsJsonObject(); - Frame frame = new Frame(get(object, "s").getAsInt(), has(object, "sn") ? get(object, "sn").getAsInt() : -1, object.getAsJsonObject("d")); - listener.executeEvent(frame); } @Override public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) { super.onFailure(webSocket, t, response); - if (!(t instanceof ProtocolException)) { - connector.getParent().getCore().getLogger().error("Unexpected failure occurred in the Network module. We will restart the Network module."); - connector.getParent().getCore().getLogger().error("Response is following: {}", response); - connector.getParent().getCore().getLogger().error("Stacktrace is following.", t); + + // 分类记录异常信息 + String exceptionType = t.getClass().getSimpleName(); + String message = t.getMessage(); + + if (t instanceof ProtocolException) { + // 协议异常,通常是正常的连接关闭 + client.getCore().getLogger().debug("WebSocket 协议异常(可能是正常关闭): {}", message); + } else if (t instanceof UnknownHostException) { + // DNS 解析失败 + client.getCore().getLogger().error("DNS 解析失败,无法连接到 Kook 服务器: {}", message); + client.getCore().getLogger().error("请检查:1) 网络连接是否正常 2) DNS 服务器配置 3) 域名 kookapp.cn 是否可访问"); + } else if (message != null && (message.contains("timeout") || message.contains("Timeout"))) { + // 连接超时 + client.getCore().getLogger().error("连接超时: {}", message); + client.getCore().getLogger().error("请检查网络连接是否稳定"); + } else if (exceptionType.contains("IOException") || exceptionType.contains("SocketException")) { + // I/O 或 Socket 异常 + client.getCore().getLogger().error("网络 I/O 异常 ({}): {}", exceptionType, message); + client.getCore().getLogger().error("这可能是由于网络不稳定或防火墙配置导致"); + } else { + // 其他未知异常 + client.getCore().getLogger().error("WebSocket 连接发生异常 ({})", exceptionType); + client.getCore().getLogger().error("响应信息: {}", response); + client.getCore().getLogger().error("异常堆栈:", t); + } + + // 关闭 WebSocket 连接 + try { + webSocket.close(1000, "Connection Failed - Reconnecting"); + } catch (Exception e) { + client.getCore().getLogger().debug("关闭 WebSocket 时发生异常(可以忽略): {}", e.getMessage()); } - webSocket.close(1000, "User Closed Service"); + + // 请求重连,并将异常传递给重连策略 + client.getCore().getLogger().info("将启动智能重连流程..."); connector.requestReconnect(); } diff --git a/src/main/java/snw/kookbc/impl/pageiter/ChannelInvitationIterator.java b/src/main/java/snw/kookbc/impl/pageiter/ChannelInvitationIterator.java index 0c88b4c8..20416b64 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/ChannelInvitationIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/ChannelInvitationIterator.java @@ -18,9 +18,7 @@ package snw.kookbc.impl.pageiter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.Invitation; import snw.jkook.entity.User; @@ -47,14 +45,17 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new HashSet<>(array.size()); - for (JsonElement element : array) { - JsonObject rawObj = element.getAsJsonObject(); - Guild guild = client.getStorage().getGuild(rawObj.get("guild_id").getAsString()); - String urlCode = rawObj.get("url_code").getAsString(); - String url = rawObj.get("url").getAsString(); - User master = client.getStorage().getUser(rawObj.getAsJsonObject("user").get("id").getAsString()); + protected void processElements(JsonNode node) { + object = new HashSet<>(node.size()); + for (JsonNode element : node) { + String guildId = element.get("guild_id").asText(); + Guild guild = client.getStorage().getGuild(guildId); + String urlCode = element.get("url_code").asText(); + String url = element.get("url").asText(); + JsonNode userNode = element.get("user"); + String userId = userNode.get("id").asText(); + // 使用完整的用户数据,避免额外的 HTTP 请求 + User master = client.getStorage().getUser(userId, userNode); object.add(new InvitationImpl( client, guild, channel, urlCode, url, master )); diff --git a/src/main/java/snw/kookbc/impl/pageiter/ChannelMessageIterator.java b/src/main/java/snw/kookbc/impl/pageiter/ChannelMessageIterator.java index bb501831..4597bafa 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/ChannelMessageIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/ChannelMessageIterator.java @@ -18,9 +18,7 @@ package snw.kookbc.impl.pageiter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.User; import snw.jkook.entity.channel.TextChannel; import snw.jkook.message.ChannelMessage; @@ -34,8 +32,6 @@ import java.util.Collections; import java.util.LinkedHashSet; -import static snw.kookbc.util.GsonUtil.get; - public class ChannelMessageIterator extends PageIteratorImpl> { private final TextChannel channel; private final String refer; @@ -56,30 +52,31 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new LinkedHashSet<>(array.size()); - for (JsonElement element : array) { - object.add(buildMessage(element.getAsJsonObject())); + protected void processElements(JsonNode node) { + object = new LinkedHashSet<>(node.size()); + for (JsonNode element : node) { + object.add(buildMessage(element)); } } + @Override public Collection next() { return Collections.unmodifiableCollection(super.next()); } - private ChannelMessage buildMessage(JsonObject object) { - String id = get(object, "id").getAsString(); + private ChannelMessage buildMessage(JsonNode node) { + String id = node.get("id").asText(); Message message = client.getStorage().getMessage(id); if (message != null) { - return (ChannelMessage) message; // if this throw ClassCastException, then we can know the message ID for pm and text channel message is in the same "space" + return (ChannelMessage) message; } - long timeStamp = get(object, "create_at").getAsLong(); - JsonObject authorObj = object.getAsJsonObject("author"); - User author = client.getStorage().getUser(authorObj.get("id").getAsString(), authorObj); - BaseComponent component = client.getMessageBuilder().buildComponent(object); - JsonElement quoteElement = object.get("quote"); - Message quote = quoteElement != null && !quoteElement.isJsonNull() ? client.getMessageBuilder().buildQuote(quoteElement.getAsJsonObject()) : null; + long timeStamp = node.get("create_at").asLong(); + JsonNode authorNode = node.get("author"); + User author = client.getStorage().getUser(authorNode.get("id").asText(), authorNode); + BaseComponent component = client.getMessageBuilder().buildComponent(node); + JsonNode quoteNode = node.get("quote"); + Message quote = quoteNode != null && !quoteNode.isNull() ? client.getMessageBuilder().buildQuote(quoteNode) : null; return new ChannelMessageImpl( client, id, diff --git a/src/main/java/snw/kookbc/impl/pageiter/GameIterator.java b/src/main/java/snw/kookbc/impl/pageiter/GameIterator.java index 3d6f16e8..76f06c46 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/GameIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/GameIterator.java @@ -18,9 +18,7 @@ package snw.kookbc.impl.pageiter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Game; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.GameImpl; @@ -48,25 +46,26 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new ArrayList<>(array.size()); + protected void processElements(JsonNode node) { + object = new ArrayList<>(node.size()); - for (JsonElement element : array) { - JsonObject rawObj = element.getAsJsonObject(); - int id = rawObj.get("id").getAsInt(); + for (JsonNode element : node) { + int id = element.get("id").asInt(); Game game = client.getStorage().getGame(id); if (game != null) { - ((GameImpl) game).update(rawObj); + ((GameImpl) game).update(element); } else { - game = client.getEntityBuilder().buildGame(rawObj); + game = client.getEntityBuilder().buildGame(element); client.getStorage().addGame(game); } object.add(game); } } + @Override public Collection next() { return Collections.unmodifiableCollection(super.next()); } + } diff --git a/src/main/java/snw/kookbc/impl/pageiter/GuildBannedUserIterator.java b/src/main/java/snw/kookbc/impl/pageiter/GuildBannedUserIterator.java index b0733991..a21dbe50 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/GuildBannedUserIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/GuildBannedUserIterator.java @@ -18,8 +18,7 @@ package snw.kookbc.impl.pageiter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.User; import snw.kookbc.impl.KBCClient; @@ -43,10 +42,13 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new HashSet<>(array.size()); - for (JsonElement element : array) { - object.add(client.getStorage().getUser(element.getAsJsonObject().getAsJsonObject("user").get("id").getAsString())); + protected void processElements(JsonNode node) { + object = new HashSet<>(node.size()); + for (JsonNode element : node) { + JsonNode userNode = element.get("user"); + String userId = userNode.get("id").asText(); + // 使用完整的用户数据,避免额外的 HTTP 请求 + object.add(client.getStorage().getUser(userId, userNode)); } } diff --git a/src/main/java/snw/kookbc/impl/pageiter/GuildChannelListIterator.java b/src/main/java/snw/kookbc/impl/pageiter/GuildChannelListIterator.java index 8da8fd9e..11b48a20 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/GuildChannelListIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/GuildChannelListIterator.java @@ -18,14 +18,11 @@ package snw.kookbc.impl.pageiter; -import static snw.kookbc.util.GsonUtil.getAsString; - import java.util.Collections; import java.util.HashSet; import java.util.Set; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.channel.Channel; import snw.kookbc.impl.KBCClient; @@ -45,10 +42,11 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new HashSet<>(array.size()); - for (JsonElement element : array) { - object.add(client.getStorage().getChannel(getAsString(element.getAsJsonObject(), "id"))); + protected void processElements(JsonNode node) { + object = new HashSet<>(node.size()); + for (JsonNode element : node) { + // 使用完整的频道数据,避免额外的 HTTP 请求 + object.add(client.getStorage().getChannel(element.get("id").asText(), element)); } } diff --git a/src/main/java/snw/kookbc/impl/pageiter/GuildEmojiListIterator.java b/src/main/java/snw/kookbc/impl/pageiter/GuildEmojiListIterator.java index 7c664d99..97f9a762 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/GuildEmojiListIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/GuildEmojiListIterator.java @@ -18,9 +18,7 @@ package snw.kookbc.impl.pageiter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.Guild; import snw.kookbc.impl.KBCClient; @@ -44,11 +42,11 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new HashSet<>(array.size()); - for (JsonElement element : array) { - JsonObject rawObj = element.getAsJsonObject(); - object.add(client.getStorage().getEmoji(rawObj.get("id").getAsString(), rawObj)); + protected void processElements(JsonNode node) { + object = new HashSet<>(node.size()); + for (JsonNode element : node) { + String id = element.get("id").asText(); + object.add(client.getStorage().getEmoji(id, element)); } } diff --git a/src/main/java/snw/kookbc/impl/pageiter/GuildInvitationsIterator.java b/src/main/java/snw/kookbc/impl/pageiter/GuildInvitationsIterator.java index a8e03649..67123b2e 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/GuildInvitationsIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/GuildInvitationsIterator.java @@ -18,9 +18,7 @@ package snw.kookbc.impl.pageiter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.Invitation; import snw.jkook.entity.User; @@ -48,14 +46,17 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new HashSet<>(array.size()); - for (JsonElement element : array) { - JsonObject rawObj = element.getAsJsonObject(); - Guild guild = client.getStorage().getGuild(rawObj.get("guild_id").getAsString()); - String urlCode = rawObj.get("url_code").getAsString(); - String url = rawObj.get("url").getAsString(); - User master = client.getStorage().getUser(rawObj.getAsJsonObject("user").get("id").getAsString()); + protected void processElements(JsonNode node) { + object = new HashSet<>(node.size()); + for (JsonNode element : node) { + String guildIdFromResponse = element.get("guild_id").asText(); + Guild guild = client.getStorage().getGuild(guildIdFromResponse); + String urlCode = element.get("url_code").asText(); + String url = element.get("url").asText(); + JsonNode userNode = element.get("user"); + String userId = userNode.get("id").asText(); + // 使用完整的用户数据,避免额外的 HTTP 请求 + User master = client.getStorage().getUser(userId, userNode); object.add(new InvitationImpl( client, guild, null, urlCode, url, master )); diff --git a/src/main/java/snw/kookbc/impl/pageiter/GuildRoleListIterator.java b/src/main/java/snw/kookbc/impl/pageiter/GuildRoleListIterator.java index b920b17e..394bad7b 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/GuildRoleListIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/GuildRoleListIterator.java @@ -18,9 +18,7 @@ package snw.kookbc.impl.pageiter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.Role; import snw.kookbc.impl.KBCClient; @@ -44,11 +42,11 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new HashSet<>(array.size()); - for (JsonElement element : array) { - JsonObject rawObj = element.getAsJsonObject(); - object.add(client.getStorage().getRole(guild, rawObj.get("role_id").getAsInt(), rawObj)); + protected void processElements(JsonNode node) { + object = new HashSet<>(node.size()); + for (JsonNode element : node) { + int roleId = element.get("role_id").asInt(); + object.add(client.getStorage().getRole(guild, roleId, element)); } } diff --git a/src/main/java/snw/kookbc/impl/pageiter/GuildUserListIterator.java b/src/main/java/snw/kookbc/impl/pageiter/GuildUserListIterator.java index ca086929..1324c987 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/GuildUserListIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/GuildUserListIterator.java @@ -18,9 +18,7 @@ package snw.kookbc.impl.pageiter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.User; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.network.HttpAPIRoute; @@ -73,14 +71,16 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new HashSet<>(array.size()); - for (JsonElement element : array) { - JsonObject rawObj = element.getAsJsonObject(); - object.add(client.getStorage().getUser(rawObj.get("id").getAsString())); + protected void processElements(JsonNode node) { + object = new HashSet<>(node.size()); + for (JsonNode element : node) { + String userId = element.get("id").asText(); + // 使用完整的用户数据,避免额外的 HTTP 请求 + object.add(client.getStorage().getUser(userId, element)); } } + @Override public Set next() { return Collections.unmodifiableSet(super.next()); diff --git a/src/main/java/snw/kookbc/impl/pageiter/JoinedGuildIterator.java b/src/main/java/snw/kookbc/impl/pageiter/JoinedGuildIterator.java index d171de6e..6d4fdb54 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/JoinedGuildIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/JoinedGuildIterator.java @@ -18,12 +18,11 @@ package snw.kookbc.impl.pageiter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.network.HttpAPIRoute; +import snw.kookbc.util.JacksonUtil; import java.util.Collection; import java.util.Collections; @@ -41,11 +40,11 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new HashSet<>(array.size()); - for (JsonElement element : array) { - JsonObject rawObj = element.getAsJsonObject(); - object.add(client.getStorage().getGuild(rawObj.get("id").getAsString(), rawObj)); + protected void processElements(JsonNode node) { + object = new HashSet<>(node.size()); + for (JsonNode element : node) { + String id = element.get("id").asText(); + object.add(client.getStorage().getGuild(id, element)); } } diff --git a/src/main/java/snw/kookbc/impl/pageiter/JoinedVoiceChannelsIterator.java b/src/main/java/snw/kookbc/impl/pageiter/JoinedVoiceChannelsIterator.java index 4e523ea1..58addebf 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/JoinedVoiceChannelsIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/JoinedVoiceChannelsIterator.java @@ -1,13 +1,9 @@ package snw.kookbc.impl.pageiter; -import static snw.kookbc.util.GsonUtil.getAsString; - import java.util.ArrayList; import java.util.Collection; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.channel.VoiceChannel; import snw.kookbc.impl.KBCClient; @@ -26,11 +22,10 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { - object = new ArrayList<>(array.size()); - for (JsonElement element : array) { - final JsonObject asObj = element.getAsJsonObject(); - final String id = getAsString(asObj, "id"); + protected void processElements(JsonNode node) { + object = new ArrayList<>(node.size()); + for (JsonNode element : node) { + final String id = element.get("id").asText(); final VoiceChannel channel = new VoiceChannelImpl(client, id); object.add(channel); } diff --git a/src/main/java/snw/kookbc/impl/pageiter/PageIteratorImpl.java b/src/main/java/snw/kookbc/impl/pageiter/PageIteratorImpl.java index 7a9847cc..da833ab1 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/PageIteratorImpl.java +++ b/src/main/java/snw/kookbc/impl/pageiter/PageIteratorImpl.java @@ -18,9 +18,7 @@ package snw.kookbc.impl.pageiter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import org.jetbrains.annotations.Range; import snw.jkook.util.Meta; import snw.jkook.util.PageIterator; @@ -54,24 +52,43 @@ public boolean hasNext() { executedOnce = true; } String reqUrl = getRequestURL(); - JsonObject object = client.getNetworkClient().get( + // 使用Jackson API获得更好的性能 + JsonNode object = client.getNetworkClient().get( reqUrl + (reqUrl.contains("?") ? "&" : "?") + "page=" + currentPage.get() + "&page_size=" + getPageSize() ); + JsonNode meta = object.get("meta"); + JsonNode items = object.get("items"); - JsonElement meta = object.get("meta"); - if (meta != null && !meta.isJsonNull()) { - JsonObject metaAsJsonObject = meta.getAsJsonObject(); - optionalMeta = Optional.of(new MetaImpl(metaAsJsonObject.get("page").getAsInt(), - metaAsJsonObject.get("page_total").getAsInt(), - metaAsJsonObject.get("page_size").getAsInt(), - metaAsJsonObject.get("total").getAsInt())); + // 先处理返回的数据项 + boolean hasData = false; + if (items != null && items.isArray() && items.size() > 0) { + processElements(items); + hasData = true; + } + + // 然后判断是否还有下一页 + if (meta != null && !meta.isNull()) { + // 有 meta 字段:使用分页信息判断是否有下一页 + optionalMeta = Optional.of(new MetaImpl(meta.get("page").asInt(), + meta.get("page_total").asInt(), + meta.get("page_size").asInt(), + meta.get("total").asInt())); next = currentPage.getAndAdd(1) <= optionalMeta.get().getPageTotal(); } else { - next = false; + // 无 meta 字段:根据返回的 items 数量判断是否有下一页 + // 如果返回的 items 数量等于 page_size,可能还有下一页 + if (items != null && items.isArray()) { + int itemCount = items.size(); + next = itemCount >= pageSizePerRequest; + currentPage.incrementAndGet(); + } else { + next = false; + } } - processElements(object.getAsJsonArray("items")); - return next; + + // 返回当前是否有数据(不是下一页是否有数据) + return hasData; } @Override @@ -102,5 +119,6 @@ public Optional getMeta() { protected abstract String getRequestURL(); - protected abstract void processElements(JsonArray array); + protected abstract void processElements(JsonNode node); + } diff --git a/src/main/java/snw/kookbc/impl/pageiter/ThreadPostIterator.java b/src/main/java/snw/kookbc/impl/pageiter/ThreadPostIterator.java new file mode 100644 index 00000000..77894271 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/pageiter/ThreadPostIterator.java @@ -0,0 +1,82 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.pageiter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import com.fasterxml.jackson.databind.JsonNode; + +import snw.jkook.entity.channel.ThreadChannel; +import snw.jkook.entity.thread.ThreadPost; +import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.entity.thread.ThreadPostImpl; +import snw.kookbc.impl.network.HttpAPIRoute; + +/** + * ThreadPost 分页迭代器 + * + *

用于获取帖子频道中的帖子列表 + * + * @since KookBC 0.33.0 + */ +public class ThreadPostIterator extends PageIteratorImpl> { + + private final ThreadChannel channel; + private final String categoryId; + + /** + * 构造 ThreadPost 迭代器 + * + * @param client KBCClient 实例 + * @param channel 帖子频道 + * @param categoryId 分类 ID (可为 null,表示获取所有分类) + */ + public ThreadPostIterator(KBCClient client, ThreadChannel channel, @Nullable String categoryId) { + super(client); + this.channel = channel; + this.categoryId = categoryId; + } + + @Override + protected String getRequestURL() { + String url = String.format("%s?channel_id=%s", + HttpAPIRoute.THREAD_LIST.toFullURL(), + channel.getId()); + + if (categoryId != null && !categoryId.isEmpty()) { + url += "&category_id=" + categoryId; + } + + return url; + } + + @Override + protected void processElements(JsonNode array) { + object = new ArrayList<>(array.size()); + + for (JsonNode element : array) { + ThreadPost threadPost = new ThreadPostImpl(client, channel, element); + object.add(threadPost); + } + } +} diff --git a/src/main/java/snw/kookbc/impl/pageiter/ThreadReplyIterator.java b/src/main/java/snw/kookbc/impl/pageiter/ThreadReplyIterator.java new file mode 100644 index 00000000..e69f9c12 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/pageiter/ThreadReplyIterator.java @@ -0,0 +1,66 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.pageiter; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.JsonNode; + +import snw.jkook.entity.thread.ThreadPost; +import snw.jkook.entity.thread.ThreadReply; +import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.entity.thread.ThreadReplyImpl; +import snw.kookbc.impl.network.HttpAPIRoute; + +/** + * ThreadReply 分页迭代器 + * + *

用于获取帖子的回复列表 + * + * @since KookBC 0.33.0 + */ +public class ThreadReplyIterator extends PageIteratorImpl> { + + private final ThreadPost threadPost; + + public ThreadReplyIterator(KBCClient client, ThreadPost threadPost) { + super(client); + this.threadPost = threadPost; + } + + @Override + protected String getRequestURL() { + return String.format("%s?channel_id=%s&thread_id=%s", + HttpAPIRoute.THREAD_POST_LIST.toFullURL(), + threadPost.getChannel().getId(), + threadPost.getId()); + } + + @Override + protected void processElements(JsonNode array) { + object = new java.util.ArrayList<>(array.size()); + + for (JsonNode element : array) { + ThreadReply reply = new ThreadReplyImpl(client, threadPost, element); + object.add(reply); + } + } +} diff --git a/src/main/java/snw/kookbc/impl/pageiter/UserJoinedVoiceChannelIterator.java b/src/main/java/snw/kookbc/impl/pageiter/UserJoinedVoiceChannelIterator.java index dbb35366..fa73ff92 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/UserJoinedVoiceChannelIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/UserJoinedVoiceChannelIterator.java @@ -18,13 +18,10 @@ package snw.kookbc.impl.pageiter; -import static snw.kookbc.util.GsonUtil.getAsString; - import java.util.Collection; import java.util.HashSet; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.User; @@ -50,10 +47,11 @@ protected String getRequestURL() { } @Override - protected void processElements(JsonArray array) { + protected void processElements(JsonNode node) { object = new HashSet<>(); - for (JsonElement element : array) { - object.add(new VoiceChannelImpl(client, getAsString(element.getAsJsonObject(), "id"))); + for (JsonNode element : node) { + String id = element.get("id").asText(); + object.add(new VoiceChannelImpl(client, id)); } } } diff --git a/src/main/java/snw/kookbc/impl/permissions/UserPermissionSaved.java b/src/main/java/snw/kookbc/impl/permissions/UserPermissionSaved.java index 54e4414a..a0342d9d 100644 --- a/src/main/java/snw/kookbc/impl/permissions/UserPermissionSaved.java +++ b/src/main/java/snw/kookbc/impl/permissions/UserPermissionSaved.java @@ -17,10 +17,11 @@ */ package snw.kookbc.impl.permissions; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import snw.jkook.permissions.PermissionAttachmentInfo; +import snw.kookbc.util.JacksonUtil; import java.util.*; @@ -49,18 +50,30 @@ public Map getPermissions() { return permissions; } - public static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); + // Jackson ObjectMapper - 高性能 JSON 处理 + private static final ObjectMapper MAPPER = JacksonUtil.createPrettyMapper(); public static String toString(UserPermissionSaved... array) { - return GSON.toJson(array); + try { + return MAPPER.writeValueAsString(array); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize UserPermissionSaved array", e); + } } public static UserPermissionSaved parse(String json) { - return GSON.fromJson(json, UserPermissionSaved.class); + try { + return MAPPER.readValue(json, UserPermissionSaved.class); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse UserPermissionSaved", e); + } } public static List parseList(String json) { - return GSON.fromJson(json, new TypeToken>() { - }); + try { + return MAPPER.readValue(json, new TypeReference>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse UserPermissionSaved list", e); + } } } diff --git a/src/main/java/snw/kookbc/impl/plugin/MixinPluginManager.java b/src/main/java/snw/kookbc/impl/plugin/MixinPluginManager.java index 5d80126f..aec6e63e 100644 --- a/src/main/java/snw/kookbc/impl/plugin/MixinPluginManager.java +++ b/src/main/java/snw/kookbc/impl/plugin/MixinPluginManager.java @@ -146,7 +146,7 @@ public void loadJarPlugin(AccessClassLoader classLoader, File file) { if (!confNameSet.isEmpty()) { if (!Util.isStartByLaunch()) { logger.warn( - "[{}] {} v{} plugin is using the Mixin framework. Please use 'Launch' mode to enable support for Mixin", + "[{}] {} v{} 插件正在使用 Mixin 框架。请使用 'Launch' 模式来启用 Mixin 支持", description.getName(), description.getName(), description.getVersion() diff --git a/src/main/java/snw/kookbc/impl/plugin/SimplePluginManager.java b/src/main/java/snw/kookbc/impl/plugin/SimplePluginManager.java index 23cc49b3..311bf2e8 100644 --- a/src/main/java/snw/kookbc/impl/plugin/SimplePluginManager.java +++ b/src/main/java/snw/kookbc/impl/plugin/SimplePluginManager.java @@ -29,14 +29,18 @@ import snw.kookbc.impl.launch.AccessClassLoader; import snw.kookbc.launcher.Launcher; import snw.kookbc.util.DependencyListBasedPluginDescriptionComparator; +import snw.kookbc.util.VirtualThreadUtil; import java.io.File; import java.io.IOException; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.jar.JarFile; +import java.util.stream.Collectors; import static snw.kookbc.util.Util.closeLoaderIfPossible; import static snw.kookbc.util.Util.getVersionDifference; @@ -138,11 +142,11 @@ protected Plugin loadPlugin0(File file, boolean failIfNoLoader) throws InvalidPl PluginDescription description = plugin.getDescription(); int diff = getVersionDifference(description.getApiVersion(), client.getCore().getAPIVersion()); if (diff == -1) { - plugin.getLogger().warn("The plugin is using old version of JKook API! We are using {}, got {}", client.getCore().getAPIVersion(), description.getApiVersion()); + plugin.getLogger().warn("该插件使用的 JKook API 版本过旧!我们使用的是 {},获取到的是 {}", client.getCore().getAPIVersion(), description.getApiVersion()); } if (diff == 1) { closeLoaderIfPossible(loader); // plugin won't be returned, so the loader should be closed to prevent resource leak - throw new InvalidPluginException(String.format("The plugin is using unsupported version of JKook API! We are using %s, got %s", client.getCore().getAPIVersion(), description.getApiVersion())); + throw new InvalidPluginException(String.format("该插件使用的 JKook API 版本不受支持!我们使用的是 %s,获取到的是 %s", client.getCore().getAPIVersion(), description.getApiVersion())); } return plugin; } @@ -171,7 +175,7 @@ protected Plugin loadPlugin0(File file, boolean failIfNoLoader) throws InvalidPl try { plugin = loadPlugin0(file, false); } catch (Throwable e) { - logger.error("Unable to load a plugin in the provided file {}", file, e); + logger.error("无法从指定文件 {} 中加载插件", file, e); continue; } if (plugin == null) { @@ -217,7 +221,7 @@ public void clearPlugins() { public void enablePlugin(Plugin plugin) throws UnknownDependencyException { if (isPluginEnabled(plugin)) return; PluginDescription description = plugin.getDescription(); - plugin.getLogger().info("Enabling {} version {}", description.getName(), description.getVersion()); + plugin.getLogger().info("正在启用 {} 版本 {}", description.getName(), description.getVersion()); if (!plugin.getDataFolder().exists()) { //noinspection ResultOfMethodCallIgnored plugin.getDataFolder().mkdir(); @@ -230,7 +234,7 @@ public void enablePlugin(Plugin plugin) throws UnknownDependencyException { try { plugin.setEnabled(true); } catch (Throwable e) { - plugin.getLogger().error("Unable to enable this plugin", e); + plugin.getLogger().error("无法启用此插件", e); disablePlugin(plugin); // make sure the plugin is still disabled } } @@ -239,7 +243,7 @@ public void enablePlugin(Plugin plugin) throws UnknownDependencyException { public void disablePlugin(Plugin plugin) { if (!isPluginEnabled(plugin)) return; PluginDescription description = plugin.getDescription(); - plugin.getLogger().info("Disabling {} version {}", description.getName(), description.getVersion()); + plugin.getLogger().info("正在禁用 {} 版本 {}", description.getName(), description.getVersion()); // cancel tasks client.getCore().getScheduler().cancelTasks(plugin); client.getCore().getEventManager().unregisterAllHandlers(plugin); @@ -249,13 +253,13 @@ public void disablePlugin(Plugin plugin) { ((CommandManagerImpl) client.getCore().getCommandManager()).getCommandMap().unregisterAll(plugin); plugin.setEnabled(false); } catch (Throwable e) { - plugin.getLogger().error("Exception occurred when we are disabling this plugin", e); + plugin.getLogger().error("禁用此插件时发生异常", e); } if (plugin.getClass().getClassLoader() instanceof SimplePluginClassLoader) { try { ((SimplePluginClassLoader) plugin.getClass().getClassLoader()).close(); } catch (IOException e) { - logger.error("Unexpected IOException while we're attempting to close the PluginClassLoader.", e); + logger.error("在尝试关闭 PluginClassLoader 时发生意外的 IOException。", e); } } } @@ -316,4 +320,230 @@ protected PluginLoader createPluginLoader(@Nullable ClassLoader parent) { public Map, Function> getLoaderProviders() { return Collections.unmodifiableMap(loaderMap); } + + // ===== 虚拟线程异步 API ===== + + /** + * 异步加载插件 - 使用虚拟线程 + * + *

在虚拟线程中执行插件加载操作,避免阻塞主线程, + * 特别适合加载大型插件或多个插件的场景。 + * + * @param file 插件文件 + * @return 异步插件加载结果 + */ + public CompletableFuture loadPluginAsync(File file) { + return CompletableFuture.supplyAsync(() -> { + try { + return loadPlugin(file); + } catch (InvalidPluginException e) { + throw new RuntimeException("异步加载插件失败: " + file.getName(), e); + } + }, VirtualThreadUtil.getPluginExecutor()); + } + + /** + * 异步批量加载插件 - 使用虚拟线程 + * + *

并行加载目录中的所有插件,显著提升多插件加载性能 + * + * @param directory 插件目录 + * @return 异步插件数组结果 + */ + public CompletableFuture loadPluginsAsync(File directory) { + return CompletableFuture.supplyAsync(() -> loadPlugins(directory), VirtualThreadUtil.getPluginExecutor()); + } + + /** + * 异步启用插件 - 使用虚拟线程 + * + *

在虚拟线程中执行插件启用操作,避免阻塞主线程 + * + * @param plugin 要启用的插件 + * @return 异步启用结果 + */ + public CompletableFuture enablePluginAsync(Plugin plugin) { + return CompletableFuture.runAsync(() -> { + try { + enablePlugin(plugin); + } catch (UnknownDependencyException e) { + throw new RuntimeException("异步启用插件失败: " + plugin.getDescription().getName(), e); + } + }, VirtualThreadUtil.getPluginExecutor()); + } + + /** + * 异步禁用插件 - 使用虚拟线程 + * + *

在虚拟线程中执行插件禁用操作,包括资源清理 + * + * @param plugin 要禁用的插件 + * @return 异步禁用结果 + */ + public CompletableFuture disablePluginAsync(Plugin plugin) { + return CompletableFuture.runAsync(() -> disablePlugin(plugin), VirtualThreadUtil.getPluginExecutor()); + } + + /** + * 异步禁用所有插件 - 使用虚拟线程 + * + *

并行禁用所有插件,提升关闭速度 + * + * @return 异步禁用结果 + */ + public CompletableFuture disablePluginsAsync() { + return CompletableFuture.runAsync(this::disablePlugins, VirtualThreadUtil.getPluginExecutor()); + } + + /** + * 批量异步启用插件 - 使用虚拟线程 + * + *

并行启用多个插件,考虑依赖关系顺序 + * + * @param plugins 要启用的插件列表 + * @return 异步启用结果 + */ + public CompletableFuture batchEnablePluginsAsync(List plugins) { + return CompletableFuture.runAsync(() -> { + // 按依赖关系排序 + List sortedPlugins = plugins.stream() + .sorted((p1, p2) -> DependencyListBasedPluginDescriptionComparator.INSTANCE + .compare(p1.getDescription(), p2.getDescription())) + .collect(Collectors.toList()); + + // 按序启用插件 + for (Plugin plugin : sortedPlugins) { + try { + enablePlugin(plugin); + } catch (UnknownDependencyException e) { + logger.error("批量启用插件失败: {}", plugin.getDescription().getName(), e); + // 继续处理其他插件 + } + } + }, VirtualThreadUtil.getPluginExecutor()); + } + + /** + * 批量异步禁用插件 - 使用虚拟线程 + * + *

并行禁用多个插件,提升性能 + * + * @param plugins 要禁用的插件列表 + * @return 异步禁用结果 + */ + public CompletableFuture batchDisablePluginsAsync(List plugins) { + List> futures = plugins.stream() + .map(this::disablePluginAsync) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } + + /** + * 异步重载插件 - 使用虚拟线程 + * + *

先禁用再重新加载启用插件,在虚拟线程中执行避免阻塞 + * + * @param plugin 要重载的插件 + * @return 异步重载结果 + */ + public CompletableFuture reloadPluginAsync(Plugin plugin) { + return CompletableFuture.supplyAsync(() -> { + String pluginName = plugin.getDescription().getName(); + File pluginFile = plugin.getFile(); + + // 先禁用插件 + disablePlugin(plugin); + removePlugin(plugin); + + // 重新加载插件 + try { + Plugin newPlugin = loadPlugin(pluginFile); + addPlugin(newPlugin); + enablePlugin(newPlugin); + return newPlugin; + } catch (InvalidPluginException | UnknownDependencyException e) { + throw new RuntimeException("异步重载插件失败: " + pluginName, e); + } + }, VirtualThreadUtil.getPluginExecutor()); + } + + /** + * 异步扫描并加载新插件 - 使用虚拟线程 + * + *

扫描插件目录,加载新发现的插件文件 + * + * @param directory 插件目录 + * @return 新加载的插件列表 + */ + public CompletableFuture> scanAndLoadNewPluginsAsync(File directory) { + return CompletableFuture.supplyAsync(() -> { + Validate.isTrue(directory.isDirectory(), "The provided file object is not a directory."); + File[] files = directory.listFiles(File::isFile); + if (files == null) { + return Collections.emptyList(); + } + + List newPlugins = new ArrayList<>(); + Set existingPluginNames = plugins.stream() + .map(p -> p.getDescription().getName()) + .collect(Collectors.toSet()); + + for (File file : files) { + final PluginDescriptionResolver resolver = lookUpPluginDescriptionResolverForFile(file); + if (resolver == null) { + continue; + } + + try { + final PluginDescription description = resolver.resolve(file); + // 检查是否是新插件 + if (!existingPluginNames.contains(description.getName())) { + Plugin plugin = loadPlugin0(file, false); + if (plugin != null) { + newPlugins.add(plugin); + addPlugin(plugin); + } + } + } catch (Throwable e) { + logger.error("扫描加载新插件失败: {}", file.getName(), e); + } + } + + return newPlugins; + }, VirtualThreadUtil.getPluginExecutor()); + } + + /** + * 异步插件热重载 - 使用虚拟线程 + * + *

监控插件文件变化,自动重载已修改的插件 + * + * @param pluginFile 插件文件 + * @return 异步重载结果 + */ + public CompletableFuture hotReloadPluginAsync(File pluginFile) { + return CompletableFuture.supplyAsync(() -> { + // 查找现有插件 + Plugin existingPlugin = plugins.stream() + .filter(p -> p.getFile().equals(pluginFile)) + .findFirst() + .orElse(null); + + if (existingPlugin != null) { + // 重载现有插件 + return reloadPluginAsync(existingPlugin).join(); + } else { + // 加载新插件 + try { + Plugin newPlugin = loadPlugin(pluginFile); + addPlugin(newPlugin); + enablePlugin(newPlugin); + return newPlugin; + } catch (InvalidPluginException | UnknownDependencyException e) { + throw new RuntimeException("热重载插件失败: " + pluginFile.getName(), e); + } + } + }, VirtualThreadUtil.getPluginExecutor()); + } } diff --git a/src/main/java/snw/kookbc/impl/scheduler/AfterPluginInitTask.java b/src/main/java/snw/kookbc/impl/scheduler/AfterPluginInitTask.java index 98fae6fa..640a0be9 100644 --- a/src/main/java/snw/kookbc/impl/scheduler/AfterPluginInitTask.java +++ b/src/main/java/snw/kookbc/impl/scheduler/AfterPluginInitTask.java @@ -45,7 +45,7 @@ public void run() { try { runnable.run(); } catch (Throwable e) { - plugin.getLogger().warn("Exception occurred while running the after-plugin-init task", e); + plugin.getLogger().warn("运行插件初始化后任务时发生异常", e); } executed = true; } diff --git a/src/main/java/snw/kookbc/impl/scheduler/SchedulerImpl.java b/src/main/java/snw/kookbc/impl/scheduler/SchedulerImpl.java index 68c74bdc..2cbb9b70 100644 --- a/src/main/java/snw/kookbc/impl/scheduler/SchedulerImpl.java +++ b/src/main/java/snw/kookbc/impl/scheduler/SchedulerImpl.java @@ -23,7 +23,6 @@ import snw.jkook.scheduler.Task; import snw.jkook.util.Validate; import snw.kookbc.impl.KBCClient; -import snw.kookbc.util.PrefixThreadFactory; import java.util.HashMap; import java.util.Map; @@ -32,6 +31,7 @@ import static snw.kookbc.util.Util.ensurePluginEnabled; import static snw.kookbc.util.Util.pluginNotNull; +import static snw.kookbc.util.VirtualThreadUtil.newVirtualThreadScheduledExecutor; public class SchedulerImpl implements Scheduler { private final KBCClient client; @@ -42,7 +42,8 @@ public class SchedulerImpl implements Scheduler { private final Map scheduledAfterPluginInitTasks = new HashMap<>(); public SchedulerImpl(KBCClient client) { - this(client, new PrefixThreadFactory("Scheduler Thread #")); + this.client = client; + this.pool = newVirtualThreadScheduledExecutor("Scheduler-Thread"); } public SchedulerImpl(KBCClient client, ThreadFactory factory) { @@ -51,7 +52,7 @@ public SchedulerImpl(KBCClient client, ThreadFactory factory) { public SchedulerImpl(KBCClient client, int corePoolSize, ThreadFactory factory) { this.client = client; - pool = Executors.newScheduledThreadPool(corePoolSize, factory); + this.pool = newVirtualThreadScheduledExecutor(corePoolSize, "Scheduler-Thread-#"); } @@ -59,7 +60,7 @@ public SchedulerImpl(KBCClient client, int corePoolSize, ThreadFactory factory) public Task runTask(Plugin plugin, Runnable runnable) { ensurePluginEnabled(plugin); int id = nextId(); - TaskImpl task = new TaskImpl(this, pool.submit(runnable), id, plugin); + TaskImpl task = new TaskImpl(this, pool.submit(wrap(runnable, id, false)), id, plugin); scheduledTasks.put(id, task); return task; } @@ -74,10 +75,10 @@ public Task runTaskLater(Plugin plugin, Runnable runnable, long delay) { } @Override - public Task runTaskTimer(Plugin plugin, Runnable runnable, long period, long delay) { + public Task runTaskTimer(Plugin plugin, Runnable runnable, long delay, long period) { ensurePluginEnabled(plugin); int id = nextId(); - TaskImpl task = new TaskImpl(this, pool.scheduleAtFixedRate(wrap(runnable, id, true), period, delay, TimeUnit.MILLISECONDS), id, plugin); + TaskImpl task = new TaskImpl(this, pool.scheduleAtFixedRate(wrap(runnable, id, true), delay, period, TimeUnit.MILLISECONDS), id, plugin); scheduledTasks.put(id, task); return task; } @@ -134,7 +135,7 @@ private Runnable wrap(Runnable runnable, int id, boolean isRepeat) { try { runnable.run(); } catch (Throwable e) { - client.getCore().getLogger().warn("Unexpected exception thrown from task #{}", id, e); + client.getCore().getLogger().warn("任务 #{} 抛出意外异常", id, e); } finally { if (!isRepeat) { // if this task should be repeated until it cancel itself... scheduledTasks.remove(id); @@ -162,7 +163,7 @@ public void shutdown() { //noinspection ResultOfMethodCallIgnored pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); } catch (InterruptedException e) { - client.getCore().getLogger().error("Unexpected interrupt happened while we waiting the scheduler got fully stopped.", e); + client.getCore().getLogger().error("等待调度器完全停止时发生意外中断", e); } } } diff --git a/src/main/java/snw/kookbc/impl/serializer/component/TemplateMessageSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/TemplateMessageSerializer.java deleted file mode 100644 index b051a41b..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/TemplateMessageSerializer.java +++ /dev/null @@ -1,17 +0,0 @@ -package snw.kookbc.impl.serializer.component; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import snw.jkook.message.component.TemplateMessage; - -import java.lang.reflect.Type; - -public class TemplateMessageSerializer implements JsonSerializer { - - @Override - public JsonElement serialize(TemplateMessage templateMessage, Type type, JsonSerializationContext jsonSerializationContext) { - return new JsonPrimitive(templateMessage.getContent()); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/CardComponentSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/CardComponentSerializer.java deleted file mode 100644 index c64ee591..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/CardComponentSerializer.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card; - -import com.google.gson.*; -import snw.jkook.message.component.card.CardComponent; -import snw.jkook.message.component.card.Size; -import snw.jkook.message.component.card.Theme; -import snw.jkook.message.component.card.module.*; - -import java.lang.reflect.Type; -import java.util.*; - -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; - -public class CardComponentSerializer implements JsonSerializer, JsonDeserializer { - public static final Map> MODULE_MAP; - - static { - Map> mutableMap = new HashMap<>(); - mutableMap.put("action-group", ActionGroupModule.class); - mutableMap.put("container", ContainerModule.class); - mutableMap.put("context", ContextModule.class); - mutableMap.put("countdown", CountdownModule.class); - mutableMap.put("divider", DividerModule.class); - mutableMap.put("file", FileModule.class); - mutableMap.put("audio", FileModule.class); - mutableMap.put("video", FileModule.class); - mutableMap.put("header", HeaderModule.class); - mutableMap.put("image-group", ImageGroupModule.class); - mutableMap.put("invite", InviteModule.class); - mutableMap.put("section", SectionModule.class); - MODULE_MAP = Collections.unmodifiableMap(mutableMap); - } - - @Override - public JsonElement serialize(CardComponent component, Type typeOfSrc, JsonSerializationContext context) { - JsonObject object = new JsonObject(); - object.addProperty("type", "card"); - object.addProperty("size", component.getSize().getValue()); - if (component.getColor() != null && !component.getColor().isEmpty()) { - object.addProperty("color", component.getColor()); - } else { - object.addProperty("theme", component.getTheme().getValue()); - } - JsonArray modules = context.serialize(component.getModules()).getAsJsonArray(); - object.add("modules", modules); - return object; - } - - @Override - public CardComponent deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - String theme = get(jsonObject, "theme").getAsString(); - String size = get(jsonObject, "size").getAsString(); - String color = has(jsonObject, "color") ? get(jsonObject, "color").getAsString() : null; - if (color != null && color.isEmpty()) { - color = null; - } - JsonArray modules = jsonObject.getAsJsonArray("modules"); - List list = new ArrayList<>(modules.size()); - modules.forEach(jsonElement -> { - JsonObject json = jsonElement.getAsJsonObject(); - String type = json.getAsJsonPrimitive("type").getAsString(); - processModule(context, list, json, type); - }); - return new CardComponent(list, Size.value(size), Theme.value(theme), color); - } - - private static void processModule(JsonDeserializationContext context, List list, JsonObject json, String type) { - if (!MODULE_MAP.containsKey(type)) { - throw new IllegalArgumentException("Unsupported module type: " + type); - } - list.add(context.deserialize(json, MODULE_MAP.get(type))); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/MultipleCardComponentSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/MultipleCardComponentSerializer.java deleted file mode 100644 index 8b2255e5..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/MultipleCardComponentSerializer.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package snw.kookbc.impl.serializer.component.card; - -import com.google.gson.*; -import snw.jkook.message.component.card.CardComponent; -import snw.jkook.message.component.card.MultipleCardComponent; - -import java.lang.reflect.Type; - -import static snw.kookbc.util.GsonUtil.createListType; - -public class MultipleCardComponentSerializer implements JsonSerializer, JsonDeserializer { - private static final Type LIST_CARDCOMPONENT = createListType(CardComponent.class); - - @Override - public JsonElement serialize(MultipleCardComponent src, Type typeOfSrc, JsonSerializationContext context) { - return context.serialize(src.getComponents()); - } - - @Override - public MultipleCardComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonArray array = json.getAsJsonArray(); - return new MultipleCardComponent(context.deserialize(array, LIST_CARDCOMPONENT)); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/element/ButtonElementSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/element/ButtonElementSerializer.java deleted file mode 100644 index bee5316e..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/element/ButtonElementSerializer.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.element; - -import com.google.gson.*; -import snw.jkook.message.component.card.Theme; -import snw.jkook.message.component.card.element.BaseElement; -import snw.jkook.message.component.card.element.ButtonElement; -import snw.jkook.message.component.card.element.MarkdownElement; -import snw.jkook.message.component.card.element.PlainTextElement; - -import java.lang.reflect.Type; - -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; - -public class ButtonElementSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(ButtonElement element, Type typeOfSrc, JsonSerializationContext context) { - ButtonElement.EventType eventType = element.getEventType(); - String value = element.getValue(); - JsonObject accessoryJson = new JsonObject(); - accessoryJson.addProperty("type", "button"); - accessoryJson.addProperty("theme", element.getTheme().getValue()); - - BaseElement textModule = element.getText(); - if (textModule != null) { - accessoryJson.add("text", context.serialize(textModule)); - } else { - accessoryJson.addProperty("text", ""); - } - if (eventType != null) { - accessoryJson.addProperty("click", eventType.getValue()); - } else { - accessoryJson.addProperty("click", ""); - } - if (value != null) { - accessoryJson.addProperty("value", value); - } else { - accessoryJson.addProperty("value", ""); - } - return accessoryJson; - } - - @Override - public ButtonElement deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - String theme = get(jsonObject, "theme").getAsString(); - - JsonObject textObj = jsonObject.getAsJsonObject("text"); - BaseElement text = context.deserialize(textObj, - "kmarkdown".equals(textObj.getAsJsonPrimitive("type").getAsString()) ? MarkdownElement.class - : PlainTextElement.class); - - String click = has(jsonObject, "click") ? get(jsonObject, "click").getAsString() : ""; - String value = has(jsonObject, "value") ? get(jsonObject, "value").getAsString() : ""; - - return new ButtonElement(Theme.value(theme), value, ButtonElement.EventType.value(click), text); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/element/ContentElementSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/element/ContentElementSerializer.java deleted file mode 100644 index 7afb508f..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/element/ContentElementSerializer.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.element; - -import com.google.gson.*; - -import java.lang.reflect.Type; -import java.util.function.Function; - -import static snw.kookbc.util.GsonUtil.get; - -public class ContentElementSerializer implements JsonSerializer, JsonDeserializer { - private final String type; - private final Function contentFunc; - private final Function parseFunc; - - public ContentElementSerializer(String type, Function contentFunc, Function parseFunc) { - this.type = type; - this.contentFunc = contentFunc; - this.parseFunc = parseFunc; - } - - @Override - public JsonElement serialize(T element, Type typeOfSrc, JsonSerializationContext context) { - JsonObject rawText = new JsonObject(); - rawText.addProperty("type", type); - rawText.addProperty("content", contentFunc.apply(element)); - return rawText; - } - - @Override - public T deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - String content = get(jsonObject, "content").getAsString(); - return parseFunc.apply(content); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/element/ImageElementSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/element/ImageElementSerializer.java deleted file mode 100644 index ef9cb30b..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/element/ImageElementSerializer.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.element; - -import com.google.gson.*; -import snw.jkook.message.component.card.Size; -import snw.jkook.message.component.card.element.ImageElement; - -import java.lang.reflect.Type; - -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; - -public class ImageElementSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(ImageElement element, Type typeOfSrc, JsonSerializationContext context) { - JsonObject accessoryJson = new JsonObject(); - accessoryJson.addProperty("type", "image"); - accessoryJson.addProperty("src", element.getSource()); - accessoryJson.addProperty("size", element.getSize().getValue()); - accessoryJson.addProperty("alt", element.getAlt()); - accessoryJson.addProperty("circle", element.isCircled()); - return accessoryJson; - } - - @Override - public ImageElement deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - String src = get(jsonObject, "src").getAsString(); - String size = has(jsonObject, "size") ? get(jsonObject, "size").getAsString() : Size.LG.getValue(); - String alt = has(jsonObject, "alt") ? get(jsonObject, "alt").getAsString() : ""; - boolean circle = has(jsonObject, "circle") && get(jsonObject, "circle").getAsBoolean(); - return new ImageElement(src, alt, Size.value(size), circle); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/module/ActionGroupModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/module/ActionGroupModuleSerializer.java deleted file mode 100644 index 08b7b9b7..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/module/ActionGroupModuleSerializer.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.module; - -import com.google.gson.*; -import snw.jkook.message.component.card.element.ButtonElement; -import snw.jkook.message.component.card.element.InteractElement; -import snw.jkook.message.component.card.module.ActionGroupModule; -import snw.jkook.util.Validate; -import snw.kookbc.SharedConstants; - -import java.lang.reflect.Type; -import java.util.List; - -import static snw.kookbc.util.GsonUtil.createListType; - -public class ActionGroupModuleSerializer implements JsonSerializer, JsonDeserializer { - private static final Type LIST_BUTTONELEMENT = createListType(ButtonElement.class); - - @Override - public JsonElement serialize(ActionGroupModule module, Type typeOfSrc, JsonSerializationContext context) { - JsonObject moduleObj = new JsonObject(); - Validate.isTrue( - module.getButtons().stream().allMatch(button -> button instanceof ButtonElement), - "If this has error, please tell the author of " + SharedConstants.SPEC_NAME + "! Maybe Kook updated the action module?" - ); - moduleObj.addProperty("type", "action-group"); - moduleObj.add("elements", context.serialize(module.getButtons())); - return moduleObj; - } - - @Override - public ActionGroupModule deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - List list = context.deserialize(jsonObject.get("elements"), LIST_BUTTONELEMENT); - return new ActionGroupModule(list); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/module/ContainerModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/module/ContainerModuleSerializer.java deleted file mode 100644 index cbb2c5b6..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/module/ContainerModuleSerializer.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.module; - -import com.google.gson.*; -import snw.jkook.message.component.card.element.ImageElement; -import snw.jkook.message.component.card.module.ContainerModule; - -import java.lang.reflect.Type; -import java.util.List; - -import static snw.kookbc.util.GsonUtil.createListType; - -public class ContainerModuleSerializer implements JsonSerializer, JsonDeserializer { - static Type LIST_IMAGEELEMENT = createListType(ImageElement.class); - - @Override - public JsonElement serialize(ContainerModule module, Type typeOfSrc, JsonSerializationContext context) { - JsonObject moduleObj = new JsonObject(); - // This will include the size attribute - JsonElement elements = context.serialize(module.getImages()); - moduleObj.addProperty("type", "container"); - moduleObj.add("elements", elements); - return moduleObj; - } - - @Override - public ContainerModule deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - List list = context.deserialize(jsonObject.get("elements"), LIST_IMAGEELEMENT); - return new ContainerModule(list); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/module/ContextModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/module/ContextModuleSerializer.java deleted file mode 100644 index a2dc2ae7..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/module/ContextModuleSerializer.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.module; - -import com.google.gson.*; -import snw.jkook.message.component.card.element.BaseElement; -import snw.jkook.message.component.card.element.ImageElement; -import snw.jkook.message.component.card.element.MarkdownElement; -import snw.jkook.message.component.card.element.PlainTextElement; -import snw.jkook.message.component.card.module.ContextModule; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -public class ContextModuleSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(ContextModule module, Type typeOfSrc, JsonSerializationContext context) { - JsonObject moduleObj = new JsonObject(); - JsonArray elements = new JsonArray(); - for (BaseElement base : module.getModules()) { - if (base instanceof PlainTextElement || base instanceof MarkdownElement || base instanceof ImageElement) { - JsonElement raw = context.serialize(base); - elements.add(raw); - } else { - throw new IllegalArgumentException("Invalid element in context module"); - } - } - moduleObj.addProperty("type", "context"); - moduleObj.add("elements", elements); - return moduleObj; - } - - @Override - public ContextModule deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - JsonArray elements = jsonObject.getAsJsonArray("elements"); - List list = new ArrayList<>(); - for (JsonElement element1 : elements) { - JsonObject obj = element1.getAsJsonObject(); - String type = obj.getAsJsonPrimitive("type").getAsString(); - switch (type) { - case "plain-text": { - list.add(context.deserialize(obj, PlainTextElement.class)); - break; - } - case "kmarkdown": { - list.add(context.deserialize(obj, MarkdownElement.class)); - break; - } - case "image": - list.add(context.deserialize(obj, ImageElement.class)); - break; - } - } - return new ContextModule(list); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/module/CountdownModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/module/CountdownModuleSerializer.java deleted file mode 100644 index b8ed7b3e..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/module/CountdownModuleSerializer.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.module; - -import com.google.gson.*; -import snw.jkook.message.component.card.module.CountdownModule; - -import java.lang.reflect.Type; - -import static snw.kookbc.util.GsonUtil.get; - -public class CountdownModuleSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(CountdownModule module, Type typeOfSrc, JsonSerializationContext context) { - JsonObject moduleObj = new JsonObject(); - moduleObj.addProperty("type", "countdown"); - moduleObj.addProperty("mode", module.getType().getValue()); - if (module.getType() == CountdownModule.Type.SECOND) { - moduleObj.addProperty("startTime", module.getStartTime()); - } - moduleObj.addProperty("endTime", module.getEndTime()); - return moduleObj; - } - - @Override - public CountdownModule deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - String mode = get(jsonObject, "mode").getAsString(); - long startTime = get(jsonObject, "startTime").getAsLong(); - long endTime = get(jsonObject, "endTime").getAsLong(); - return new CountdownModule(CountdownModule.Type.value(mode), startTime, endTime); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/module/DividerModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/module/DividerModuleSerializer.java deleted file mode 100644 index b9e2bc87..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/module/DividerModuleSerializer.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.module; - -import com.google.gson.*; -import snw.jkook.message.component.card.module.DividerModule; - -import java.lang.reflect.Type; - -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; - -public class DividerModuleSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(DividerModule src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject moduleObj = new JsonObject(); - moduleObj.addProperty("type", "divider"); - return moduleObj; - } - - @Override - public DividerModule deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - if (has(jsonObject, "type") && get(jsonObject, "type").getAsString().equals("divider")) { - return DividerModule.INSTANCE; - } - throw new JsonParseException("Invalid divider module"); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/module/FileModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/module/FileModuleSerializer.java deleted file mode 100644 index 7256ac8b..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/module/FileModuleSerializer.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.module; - -import com.google.gson.*; -import snw.jkook.message.component.FileComponent; -import snw.jkook.message.component.card.module.FileModule; - -import java.lang.reflect.Type; - -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; - -public class FileModuleSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(FileModule module, Type typeOfSrc, JsonSerializationContext context) { - JsonObject moduleObj = new JsonObject(); - moduleObj.addProperty("type", module.getType().getValue()); - moduleObj.addProperty("title", (module.getTitle())); - moduleObj.addProperty("src", module.getSource()); - if (module.getType() == FileComponent.Type.AUDIO) { - moduleObj.addProperty("cover", module.getCover()); - } - return moduleObj; - } - - @Override - public FileModule deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - String type = get(jsonObject, "type").getAsString(); - String title = get(jsonObject, "title").getAsString(); - String src = get(jsonObject, "src").getAsString(); - String cover = null; - if (has(jsonObject, "cover")) { - cover = get(jsonObject, "cover").getAsString(); - } - return new FileModule(FileComponent.Type.value(type), src, title, cover); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/module/HeaderModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/module/HeaderModuleSerializer.java deleted file mode 100644 index dd540032..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/module/HeaderModuleSerializer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.module; - -import com.google.gson.*; -import snw.jkook.message.component.card.element.PlainTextElement; -import snw.jkook.message.component.card.module.HeaderModule; - -import java.lang.reflect.Type; - -public class HeaderModuleSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(HeaderModule module, Type typeOfSrc, JsonSerializationContext context) { - JsonObject moduleObj = new JsonObject(); - JsonObject textObj = new JsonObject(); - textObj.addProperty("type", "plain-text"); - textObj.addProperty("content", module.getElement().getContent()); - moduleObj.addProperty("type", "header"); - moduleObj.add("text", textObj); - return moduleObj; - } - - @Override - public HeaderModule deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - JsonObject text = jsonObject.getAsJsonObject("text"); - String content = text.getAsJsonPrimitive("content").getAsString(); - return new HeaderModule(new PlainTextElement(content)); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/module/ImageGroupModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/module/ImageGroupModuleSerializer.java deleted file mode 100644 index b9bab4e7..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/module/ImageGroupModuleSerializer.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.module; - -import com.google.gson.*; -import snw.jkook.message.component.card.module.ImageGroupModule; - -import java.lang.reflect.Type; - -import static snw.kookbc.impl.serializer.component.card.module.ContainerModuleSerializer.LIST_IMAGEELEMENT; - -public class ImageGroupModuleSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(ImageGroupModule module, Type typeOfSrc, JsonSerializationContext context) { - JsonObject moduleObj = new JsonObject(); - JsonElement elements = context.serialize(module.getImages()); - moduleObj.addProperty("type", "image-group"); - moduleObj.add("elements", elements); - return moduleObj; - } - - @Override - public ImageGroupModule deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - JsonElement elements = jsonObject.get("elements"); - return new ImageGroupModule(context.deserialize(elements, LIST_IMAGEELEMENT)); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/module/InviteModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/module/InviteModuleSerializer.java deleted file mode 100644 index d92a6549..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/module/InviteModuleSerializer.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.module; - -import com.google.gson.*; -import snw.jkook.message.component.card.module.InviteModule; - -import java.lang.reflect.Type; - -import static snw.kookbc.util.GsonUtil.get; - -public class InviteModuleSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(InviteModule module, Type typeOfSrc, JsonSerializationContext context) { - JsonObject moduleObj = new JsonObject(); - moduleObj.addProperty("type", "invite"); - moduleObj.addProperty("code", module.getCode()); - return moduleObj; - } - - @Override - public InviteModule deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - return new InviteModule(get(jsonObject, "code").getAsString()); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/module/SectionModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/module/SectionModuleSerializer.java deleted file mode 100644 index cd9976ac..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/module/SectionModuleSerializer.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.module; - -import com.google.gson.*; -import snw.jkook.entity.abilities.Accessory; -import snw.jkook.message.component.card.CardScopeElement; -import snw.jkook.message.component.card.element.ButtonElement; -import snw.jkook.message.component.card.element.ImageElement; -import snw.jkook.message.component.card.element.MarkdownElement; -import snw.jkook.message.component.card.element.PlainTextElement; -import snw.jkook.message.component.card.module.SectionModule; -import snw.jkook.message.component.card.structure.Paragraph; - -import java.lang.reflect.Type; - -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; - -public class SectionModuleSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(SectionModule sectionModule, Type typeOfSrc, JsonSerializationContext context) { - JsonObject moduleObj = new JsonObject(); - JsonElement rawText = context.serialize(sectionModule.getText()); - moduleObj.addProperty("type", "section"); - moduleObj.add("text", rawText); - Accessory.Mode mode = sectionModule.getMode(); - if (mode != null) { - moduleObj.addProperty("mode", mode.getValue()); - } - if (sectionModule.getAccessory() != null) { - moduleObj.add("accessory", context.serialize(sectionModule.getAccessory())); - } - return moduleObj; - } - - @Override - public SectionModule deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - JsonObject text = jsonObject.get("text").getAsJsonObject(); - boolean hasModeField = has(jsonObject, "mode"); - Accessory.Mode mode = hasModeField ? Accessory.Mode.value(get(jsonObject, "mode").getAsString()) : null; - Accessory accessory = null; - if (has(jsonObject, "accessory")) { - JsonObject accessoryJson = jsonObject.get("accessory").getAsJsonObject(); - String accessoryType = accessoryJson.getAsJsonPrimitive("type").getAsString(); - if (accessoryType.equals("image")) { - accessory = context.deserialize(accessoryJson, ImageElement.class); - } else if (accessoryType.equals("button")) { - accessory = context.deserialize(accessoryJson, ButtonElement.class); - } - } - CardScopeElement cardElement = null; - String type = text.getAsJsonPrimitive("type").getAsString(); - switch (type) { - case "plain-text": { - cardElement = context.deserialize(text, PlainTextElement.class); - break; - } - case "kmarkdown": { - cardElement = context.deserialize(text, MarkdownElement.class); - break; - } - case "paragraph": { - cardElement = context.deserialize(text, Paragraph.class); - break; - } - } - return new SectionModule(cardElement, accessory, mode); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/card/structure/ParagraphSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/card/structure/ParagraphSerializer.java deleted file mode 100644 index 5a995e74..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/component/card/structure/ParagraphSerializer.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.component.card.structure; - -import com.google.gson.*; -import snw.jkook.message.component.card.element.BaseElement; -import snw.jkook.message.component.card.element.MarkdownElement; -import snw.jkook.message.component.card.element.PlainTextElement; -import snw.jkook.message.component.card.structure.Paragraph; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.has; - -public class ParagraphSerializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(Paragraph element, Type typeOfSrc, JsonSerializationContext context) { - JsonObject rawText = new JsonObject(); - rawText.addProperty("type", "paragraph"); - rawText.addProperty("cols", element.getColumns()); - rawText.add("fields", context.serialize(element.getFields())); - return rawText; - } - - @Override - public Paragraph deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = element.getAsJsonObject(); - if (has(jsonObject, "type") && get(jsonObject, "type").getAsString().equals("paragraph")) { - int cols = get(jsonObject, "cols").getAsInt(); - JsonArray fieldArray = jsonObject.getAsJsonArray("fields"); - - List fields = new ArrayList<>(fieldArray.size()); - fieldArray.forEach(json -> { - JsonObject object = json.getAsJsonObject(); - String type = object.getAsJsonPrimitive("type").getAsString(); - String content = object.getAsJsonPrimitive("content").getAsString(); - if (type.equals("kmarkdown")) { - fields.add(new MarkdownElement(content)); - } else { - fields.add(new PlainTextElement(content)); - } - }); - - return new Paragraph(cols, fields); - } - throw new JsonParseException("Invalid paragraph"); - } -} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/JacksonTemplateMessageDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/JacksonTemplateMessageDeserializer.java new file mode 100644 index 00000000..47b909a2 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/JacksonTemplateMessageDeserializer.java @@ -0,0 +1,60 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.TemplateMessage; + +import java.io.IOException; + +/** + * TemplateMessage Jackson 序列化器 + * 处理模板消息的序列化和反序列化 + * TemplateMessage 通常只是一个字符串内容 + */ +public class JacksonTemplateMessageDeserializer extends JsonDeserializer { + + @Override + public TemplateMessage deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String content = p.getValueAsString(); + if (content == null) { + content = ""; + } + // 根据代码分析,TemplateMessage构造器需要3个参数 (long id, String content, int type) + // 由于我们从字符串反序列化,使用默认值 + return new TemplateMessage(0L, content, 1); + } + + /** + * TemplateMessage 序列化器 + */ + public static class TemplateMessageSerializer extends JsonSerializer { + + @Override + public void serialize(TemplateMessage value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + String content = value.getContent(); + gen.writeString(content != null ? content : ""); + } + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/JacksonCardComponentDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/JacksonCardComponentDeserializer.java new file mode 100644 index 00000000..887e84a6 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/JacksonCardComponentDeserializer.java @@ -0,0 +1,146 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.CardComponent; +import snw.jkook.message.component.card.Size; +import snw.jkook.message.component.card.Theme; +import snw.jkook.message.component.card.module.*; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; +import java.util.*; + +/** + * CardComponent Jackson 序列化器 + * 处理卡片组件的序列化和反序列化 + */ +public class JacksonCardComponentDeserializer extends JsonDeserializer { + + public static final Map> MODULE_MAP; + + static { + Map> mutableMap = new HashMap<>(); + mutableMap.put("action-group", ActionGroupModule.class); + mutableMap.put("container", ContainerModule.class); + mutableMap.put("context", ContextModule.class); + mutableMap.put("countdown", CountdownModule.class); + mutableMap.put("divider", DividerModule.class); + mutableMap.put("file", FileModule.class); + mutableMap.put("audio", FileModule.class); + mutableMap.put("video", FileModule.class); + mutableMap.put("header", HeaderModule.class); + mutableMap.put("image-group", ImageGroupModule.class); + mutableMap.put("invite", InviteModule.class); + mutableMap.put("section", SectionModule.class); + MODULE_MAP = Collections.unmodifiableMap(mutableMap); + } + + @Override + public CardComponent deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // 解析基本属性 + String size = JacksonCardUtil.getRequiredString(node, "size"); + String theme = JacksonCardUtil.getStringOrDefault(node, "theme", null); + String color = JacksonCardUtil.getStringOrDefault(node, "color", null); + + // 如果 color 是空字符串,设置为 null + if (color != null && color.trim().isEmpty()) { + color = null; + } + + // 解析模块列表 + List modules = new ArrayList<>(); + if (JacksonCardUtil.has(node, "modules")) { + JsonNode modulesNode = node.get("modules"); + if (modulesNode.isArray()) { + for (JsonNode moduleNode : modulesNode) { + try { + String moduleType = JacksonCardUtil.getRequiredString(moduleNode, "type"); + BaseModule module = processModule(moduleNode, moduleType); + if (module != null) { + modules.add(module); + } + } catch (Exception e) { + // 日志记录但不中断处理,跳过无效模块 + } + } + } + } + + try { + Size cardSize = Size.value(size); + Theme cardTheme = theme != null ? Theme.value(theme) : null; + return new CardComponent(modules, cardSize, cardTheme, color); + } catch (Exception e) { + throw new IOException("Failed to create CardComponent: " + e.getMessage(), e); + } + } + + private BaseModule processModule(JsonNode moduleNode, String type) { + Class moduleClass = MODULE_MAP.get(type); + if (moduleClass == null) { + throw new IllegalArgumentException("Unsupported module type: " + type); + } + return JacksonCardUtil.fromJson(moduleNode, moduleClass); + } + + /** + * CardComponent 序列化器 + */ + public static class CardComponentSerializer extends JsonSerializer { + + @Override + public void serialize(CardComponent component, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + + gen.writeStringField("type", "card"); + + // 序列化尺寸 + Size size = component.getSize(); + if (size != null) { + gen.writeStringField("size", size.getValue()); + } + + // 序列化主题或颜色 + String color = component.getColor(); + if (color != null && !color.isEmpty()) { + gen.writeStringField("color", color); + } else { + Theme theme = component.getTheme(); + if (theme != null) { + gen.writeStringField("theme", theme.getValue()); + } + } + + // 序列化模块列表 + gen.writeObjectField("modules", component.getModules()); + + gen.writeEndObject(); + } + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/JacksonMultipleCardComponentDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/JacksonMultipleCardComponentDeserializer.java new file mode 100644 index 00000000..e26df7a4 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/JacksonMultipleCardComponentDeserializer.java @@ -0,0 +1,77 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.CardComponent; +import snw.jkook.message.component.card.MultipleCardComponent; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * MultipleCardComponent Jackson 序列化器 + * 处理多卡片组件的序列化和反序列化 + */ +public class JacksonMultipleCardComponentDeserializer extends JsonDeserializer { + + @Override + public MultipleCardComponent deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + List components = new ArrayList<>(); + + if (node.isArray()) { + for (JsonNode cardNode : node) { + try { + CardComponent card = JacksonCardUtil.fromJson(cardNode, CardComponent.class); + if (card != null) { + components.add(card); + } + } catch (Exception e) { + // 日志记录但不中断处理,跳过无效卡片 + } + } + } else { + throw new IOException("MultipleCardComponent must be a JSON array"); + } + + return new MultipleCardComponent(components); + } + + /** + * MultipleCardComponent 序列化器 + */ + public static class MultipleCardComponentSerializer extends JsonSerializer { + + @Override + public void serialize(MultipleCardComponent value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // 直接序列化为卡片数组 + gen.writeObject(value.getComponents()); + } + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonBaseElementDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonBaseElementDeserializer.java new file mode 100644 index 00000000..8789c3d1 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonBaseElementDeserializer.java @@ -0,0 +1,99 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.element; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import snw.jkook.message.component.card.element.*; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * BaseElement 多态反序列化器 + * 根据 JSON 中的 type 字段选择正确的 Element 实现类进行反序列化 + */ +public class JacksonBaseElementDeserializer extends JsonDeserializer { + + private static final Map> ELEMENT_TYPE_MAP = new HashMap<>(); + + static { + // 注册各种元素类型映射 + // Paragraph不是BaseElement的子类,应该移除 + ELEMENT_TYPE_MAP.put("button", ButtonElement.class); + ELEMENT_TYPE_MAP.put("image", ImageElement.class); + ELEMENT_TYPE_MAP.put("plain-text", PlainTextElement.class); + ELEMENT_TYPE_MAP.put("kmarkdown", MarkdownElement.class); + // paragraph已移除,因为Paragraph是BaseStructure而不是BaseElement + } + + @Override + public BaseElement deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // 获取 type 字段,确定具体的元素类型 + if (!node.has("type")) { + throw new IllegalArgumentException("Missing required 'type' field in element JSON"); + } + + String type = node.get("type").asText(); + if (type == null || type.trim().isEmpty()) { + throw new IllegalArgumentException("Element type cannot be null or empty"); + } + + Class elementClass = ELEMENT_TYPE_MAP.get(type); + if (elementClass == null) { + throw new IllegalArgumentException("Unknown element type: " + type); + } + + try { + // 使用 JacksonCardUtil 的 mapper 进行反序列化,确保使用正确的序列化器 + return JacksonCardUtil.fromJson(node, elementClass); + } catch (Exception e) { + throw new IOException("Failed to deserialize element of type: " + type, e); + } + } + + /** + * 获取支持的元素类型列表 + * @return 元素类型到类的映射 + */ + public static Map> getSupportedTypes() { + return new HashMap<>(ELEMENT_TYPE_MAP); + } + + /** + * 注册新的元素类型(用于扩展) + * @param type 元素类型字符串 + * @param elementClass 对应的元素类 + */ + public static void registerElementType(String type, Class elementClass) { + if (type == null || type.trim().isEmpty()) { + throw new IllegalArgumentException("Element type cannot be null or empty"); + } + if (elementClass == null) { + throw new IllegalArgumentException("Element class cannot be null"); + } + ELEMENT_TYPE_MAP.put(type, elementClass); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonButtonElementDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonButtonElementDeserializer.java new file mode 100644 index 00000000..4a55da35 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonButtonElementDeserializer.java @@ -0,0 +1,124 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.element; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.Theme; +import snw.jkook.message.component.card.element.BaseElement; +import snw.jkook.message.component.card.element.ButtonElement; +import snw.jkook.message.component.card.element.MarkdownElement; +import snw.jkook.message.component.card.element.PlainTextElement; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; + +/** + * ButtonElement Jackson 序列化器 + * 处理按钮元素的序列化和反序列化 + */ +public class JacksonButtonElementDeserializer extends JsonDeserializer { + + @Override + public ButtonElement deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // 验证必需字段 + String theme = JacksonCardUtil.getRequiredString(node, "theme"); + + // 解析按钮文本 + BaseElement text = null; + if (JacksonCardUtil.has(node, "text")) { + JsonNode textNode = node.get("text"); + if (textNode.isObject()) { + String textType = JacksonCardUtil.getStringOrDefault(textNode, "type", "plain-text"); + if ("kmarkdown".equals(textType)) { + text = JacksonCardUtil.fromJson(textNode, MarkdownElement.class); + } else { + text = JacksonCardUtil.fromJson(textNode, PlainTextElement.class); + } + } else if (textNode.isTextual()) { + // 如果 text 是字符串,创建 PlainTextElement + text = new PlainTextElement(textNode.asText()); + } + } + + // 如果没有文本,使用空字符串 + if (text == null) { + text = new PlainTextElement(""); + } + + // 解析事件类型和值 + String click = JacksonCardUtil.getStringOrDefault(node, "click", ""); + String value = JacksonCardUtil.getStringOrDefault(node, "value", ""); + + try { + Theme buttonTheme = Theme.value(theme); + ButtonElement.EventType eventType = ButtonElement.EventType.value(click); + return new ButtonElement(buttonTheme, value, eventType, text); + } catch (Exception e) { + throw new IOException("Failed to create ButtonElement: " + e.getMessage(), e); + } + } + + /** + * ButtonElement 序列化器 + */ + public static class ButtonElementSerializer extends JsonSerializer { + + @Override + public void serialize(ButtonElement element, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + + gen.writeStringField("type", "button"); + gen.writeStringField("theme", element.getTheme().getValue()); + + // 序列化按钮文本 + BaseElement textElement = element.getText(); + if (textElement != null) { + gen.writeObjectField("text", textElement); + } else { + gen.writeStringField("text", ""); + } + + // 序列化事件类型 + ButtonElement.EventType eventType = element.getEventType(); + if (eventType != null) { + gen.writeStringField("click", eventType.getValue()); + } else { + gen.writeStringField("click", ""); + } + + // 序列化值 + String value = element.getValue(); + if (value != null) { + gen.writeStringField("value", value); + } else { + gen.writeStringField("value", ""); + } + + gen.writeEndObject(); + } + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonButtonElementSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonButtonElementSerializer.java new file mode 100644 index 00000000..2629dc84 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonButtonElementSerializer.java @@ -0,0 +1,62 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.element; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.element.ButtonElement; + +import java.io.IOException; + +/** + * ButtonElement Jackson 序列化器 + * 将按钮元素序列化为标准的Kook卡片JSON格式 + */ +public class JacksonButtonElementSerializer extends JsonSerializer { + + @Override + public void serialize(ButtonElement value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", "button"); + + // 序列化theme字段 + if (value.getTheme() != null) { + gen.writeStringField("theme", value.getTheme().getValue()); + } + + // 序列化value字段 + if (value.getValue() != null) { + gen.writeStringField("value", value.getValue()); + } + + // 序列化click字段 + if (value.getEventType() != null) { + gen.writeStringField("click", value.getEventType().getValue()); + } + + // 序列化text字段 + if (value.getText() != null) { + gen.writeFieldName("text"); + serializers.defaultSerializeValue(value.getText(), gen); + } + + gen.writeEndObject(); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonContentElementDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonContentElementDeserializer.java new file mode 100644 index 00000000..626b06b6 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonContentElementDeserializer.java @@ -0,0 +1,116 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.element; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; +import java.util.function.Function; + +/** + * 泛型内容元素序列化器 + * 用于处理只包含 type 和 content 字段的简单元素,如 PlainTextElement 和 MarkdownElement + * + * @param 元素类型 + */ +public class JacksonContentElementDeserializer extends JsonDeserializer { + + private final Function constructor; + + /** + * 创建内容元素反序列化器 + * @param constructor 构造函数,接受字符串内容并返回对应的元素对象 + */ + public JacksonContentElementDeserializer(Function constructor) { + this.constructor = constructor; + if (constructor == null) { + throw new IllegalArgumentException("Constructor function cannot be null"); + } + } + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // 验证必需字段 + if (!node.has("type")) { + throw new IllegalArgumentException("Missing required 'type' field in content element JSON"); + } + + if (!node.has("content")) { + throw new IllegalArgumentException("Missing required 'content' field in content element JSON"); + } + + // 获取内容并创建对象 + String content = JacksonCardUtil.getStringOrDefault(node, "content", ""); + + try { + return constructor.apply(content); + } catch (Exception e) { + String type = JacksonCardUtil.getStringOrDefault(node, "type", "unknown"); + throw new IOException("Failed to create content element of type '" + type + "' with content: " + content, e); + } + } + + /** + * 创建一个新的内容元素序列化器(静态工厂方法) + * @param constructor 构造函数 + * @param 元素类型 + * @return 新的序列化器实例 + */ + public static JacksonContentElementDeserializer create(Function constructor) { + return new JacksonContentElementDeserializer<>(constructor); + } + + /** + * 内容元素序列化器(支持序列化和反序列化) + * @param 元素类型 + */ + public static class ContentElementSerializer extends JsonSerializer { + + private final String type; + private final Function contentExtractor; + + public ContentElementSerializer(String type, Function contentExtractor) { + this.type = type; + this.contentExtractor = contentExtractor; + if (type == null || type.trim().isEmpty()) { + throw new IllegalArgumentException("Type cannot be null or empty"); + } + if (contentExtractor == null) { + throw new IllegalArgumentException("Content extractor cannot be null"); + } + } + + @Override + public void serialize(T value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", type); + gen.writeStringField("content", contentExtractor.apply(value)); + gen.writeEndObject(); + } + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonImageElementDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonImageElementDeserializer.java new file mode 100644 index 00000000..e613a4f6 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonImageElementDeserializer.java @@ -0,0 +1,90 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.element; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.Size; +import snw.jkook.message.component.card.element.ImageElement; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; + +/** + * ImageElement Jackson 序列化器 + * 处理图片元素的序列化和反序列化 + */ +public class JacksonImageElementDeserializer extends JsonDeserializer { + + @Override + public ImageElement deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // 获取必需字段 + String src = JacksonCardUtil.getRequiredString(node, "src"); + + // 获取可选字段,使用默认值 + String size = JacksonCardUtil.getStringOrDefault(node, "size", Size.LG.getValue()); + String alt = JacksonCardUtil.getStringOrDefault(node, "alt", ""); + boolean circle = JacksonCardUtil.getBooleanOrDefault(node, "circle", false); + + try { + Size imageSize = Size.value(size); + return new ImageElement(src, alt, imageSize, circle); + } catch (Exception e) { + throw new IOException("Failed to create ImageElement with src='" + src + "', size='" + size + "': " + e.getMessage(), e); + } + } + + /** + * ImageElement 序列化器 + */ + public static class ImageElementSerializer extends JsonSerializer { + + @Override + public void serialize(ImageElement element, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + + gen.writeStringField("type", "image"); + gen.writeStringField("src", element.getSource()); + + // 序列化尺寸 + Size size = element.getSize(); + if (size != null) { + gen.writeStringField("size", size.getValue()); + } else { + gen.writeStringField("size", Size.LG.getValue()); + } + + // 序列化 alt 文本 + String alt = element.getAlt(); + gen.writeStringField("alt", alt != null ? alt : ""); + + // 序列化圆形标识 + gen.writeBooleanField("circle", element.isCircled()); + + gen.writeEndObject(); + } + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonMarkdownElementSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonMarkdownElementSerializer.java new file mode 100644 index 00000000..75bf64a5 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonMarkdownElementSerializer.java @@ -0,0 +1,46 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.element; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.element.MarkdownElement; + +import java.io.IOException; + +/** + * MarkdownElement Jackson 序列化器 + * 将Markdown元素序列化为标准的Kook卡片JSON格式 + */ +public class JacksonMarkdownElementSerializer extends JsonSerializer { + + @Override + public void serialize(MarkdownElement value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", "kmarkdown"); + + // 序列化content字段 + if (value.getContent() != null) { + gen.writeStringField("content", value.getContent()); + } + + gen.writeEndObject(); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonPlainTextElementSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonPlainTextElementSerializer.java new file mode 100644 index 00000000..922da28f --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonPlainTextElementSerializer.java @@ -0,0 +1,49 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.element; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.element.PlainTextElement; + +import java.io.IOException; + +/** + * PlainTextElement Jackson 序列化器 + * 将纯文本元素序列化为标准的Kook卡片JSON格式 + */ +public class JacksonPlainTextElementSerializer extends JsonSerializer { + + @Override + public void serialize(PlainTextElement value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", "plain-text"); + + // 序列化content字段 + if (value.getContent() != null) { + gen.writeStringField("content", value.getContent()); + } + + // PlainTextElement默认支持emoji + gen.writeBooleanField("emoji", true); + + gen.writeEndObject(); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonActionGroupModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonActionGroupModuleDeserializer.java new file mode 100644 index 00000000..c202cd15 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonActionGroupModuleDeserializer.java @@ -0,0 +1,66 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import snw.jkook.message.component.card.element.ButtonElement; +import snw.jkook.message.component.card.element.InteractElement; +import snw.jkook.message.component.card.module.ActionGroupModule; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * ActionGroupModule Jackson 反序列化器 + * 处理动作组模块的反序列化 + */ +public class JacksonActionGroupModuleDeserializer extends JsonDeserializer { + + @Override + public ActionGroupModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // 解析按钮元素列表,ActionGroupModule需要List + List elements = new ArrayList<>(); + + if (JacksonCardUtil.has(node, "elements")) { + JsonNode elementsNode = node.get("elements"); + if (elementsNode.isArray()) { + for (JsonNode elementNode : elementsNode) { + try { + // ButtonElement实现了InteractElement接口 + ButtonElement button = JacksonCardUtil.fromJson(elementNode, ButtonElement.class); + if (button != null) { + elements.add(button); + } + } catch (Exception e) { + // 日志记录但不中断处理,跳过无效的按钮 + } + } + } + } + + return new ActionGroupModule(elements); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonActionGroupModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonActionGroupModuleSerializer.java new file mode 100644 index 00000000..e2ac0d14 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonActionGroupModuleSerializer.java @@ -0,0 +1,47 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.module.ActionGroupModule; + +import java.io.IOException; + +/** + * ActionGroupModule Jackson 序列化器 + * 将操作组模块序列化为标准的Kook卡片JSON格式 + */ +public class JacksonActionGroupModuleSerializer extends JsonSerializer { + + @Override + public void serialize(ActionGroupModule value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", "action-group"); + + // 序列化elements字段 + if (value.getButtons() != null && !value.getButtons().isEmpty()) { + gen.writeFieldName("elements"); + serializers.defaultSerializeValue(value.getButtons(), gen); + } + + gen.writeEndObject(); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonBaseModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonBaseModuleDeserializer.java new file mode 100644 index 00000000..75375ec9 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonBaseModuleDeserializer.java @@ -0,0 +1,113 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import snw.jkook.message.component.card.module.*; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * BaseModule 多态反序列化器 + * 根据 JSON 中的 type 字段选择正确的 Module 实现类进行反序列化 + */ +public class JacksonBaseModuleDeserializer extends JsonDeserializer { + + private static final Map> MODULE_TYPE_MAP = new HashMap<>(); + + static { + // 注册各种模块类型映射 + MODULE_TYPE_MAP.put("section", SectionModule.class); + MODULE_TYPE_MAP.put("action-group", ActionGroupModule.class); + MODULE_TYPE_MAP.put("container", ContainerModule.class); + MODULE_TYPE_MAP.put("context", ContextModule.class); + MODULE_TYPE_MAP.put("countdown", CountdownModule.class); + MODULE_TYPE_MAP.put("divider", DividerModule.class); + MODULE_TYPE_MAP.put("file", FileModule.class); + MODULE_TYPE_MAP.put("header", HeaderModule.class); + MODULE_TYPE_MAP.put("image-group", ImageGroupModule.class); + MODULE_TYPE_MAP.put("invite", InviteModule.class); + } + + @Override + public BaseModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // 获取 type 字段,确定具体的模块类型 + if (!node.has("type")) { + throw new IllegalArgumentException("Missing required 'type' field in module JSON"); + } + + String type = node.get("type").asText(); + if (type == null || type.trim().isEmpty()) { + throw new IllegalArgumentException("Module type cannot be null or empty"); + } + + Class moduleClass = MODULE_TYPE_MAP.get(type); + if (moduleClass == null) { + throw new IllegalArgumentException("Unknown module type: " + type + ". Supported types: " + + String.join(", ", MODULE_TYPE_MAP.keySet())); + } + + try { + // 使用 JacksonCardUtil 的 mapper 进行反序列化,确保使用正确的序列化器 + return JacksonCardUtil.fromJson(node, moduleClass); + } catch (Exception e) { + throw new IOException("Failed to deserialize module of type '" + type + "': " + e.getMessage(), e); + } + } + + /** + * 获取支持的模块类型列表 + * @return 模块类型到类的映射 + */ + public static Map> getSupportedTypes() { + return new HashMap<>(MODULE_TYPE_MAP); + } + + /** + * 注册新的模块类型(用于扩展) + * @param type 模块类型字符串 + * @param moduleClass 对应的模块类 + */ + public static void registerModuleType(String type, Class moduleClass) { + if (type == null || type.trim().isEmpty()) { + throw new IllegalArgumentException("Module type cannot be null or empty"); + } + if (moduleClass == null) { + throw new IllegalArgumentException("Module class cannot be null"); + } + MODULE_TYPE_MAP.put(type, moduleClass); + } + + /** + * 检查是否支持指定的模块类型 + * @param type 模块类型 + * @return true 如果支持 + */ + public static boolean isSupported(String type) { + return type != null && MODULE_TYPE_MAP.containsKey(type); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContainerModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContainerModuleDeserializer.java new file mode 100644 index 00000000..b2f0791e --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContainerModuleDeserializer.java @@ -0,0 +1,64 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import snw.jkook.message.component.card.element.ImageElement; +import snw.jkook.message.component.card.module.ContainerModule; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * ContainerModule Jackson 反序列化器 + * 处理容器模块的反序列化 + */ +public class JacksonContainerModuleDeserializer extends JsonDeserializer { + + @Override + public ContainerModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // 解析图片列表 + List elements = new ArrayList<>(); + + if (JacksonCardUtil.has(node, "elements")) { + JsonNode elementsNode = node.get("elements"); + if (elementsNode.isArray()) { + for (JsonNode elementNode : elementsNode) { + try { + ImageElement image = JacksonCardUtil.fromJson(elementNode, ImageElement.class); + if (image != null) { + elements.add(image); + } + } catch (Exception e) { + // 日志记录但不中断处理,跳过无效的图片 + } + } + } + } + + return new ContainerModule(elements); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContextModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContextModuleDeserializer.java new file mode 100644 index 00000000..71ae1c66 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContextModuleDeserializer.java @@ -0,0 +1,67 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import snw.jkook.message.component.card.element.BaseElement; +import snw.jkook.message.component.card.element.MarkdownElement; +import snw.jkook.message.component.card.element.PlainTextElement; +import snw.jkook.message.component.card.module.ContextModule; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * ContextModule Jackson 反序列化器 + */ +public class JacksonContextModuleDeserializer extends JsonDeserializer { + + @Override + public ContextModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + List elements = new ArrayList<>(); + + if (JacksonCardUtil.has(node, "elements")) { + JsonNode elementsNode = node.get("elements"); + if (elementsNode.isArray()) { + for (JsonNode elementNode : elementsNode) { + try { + String type = JacksonCardUtil.getStringOrDefault(elementNode, "type", "plain-text"); + BaseElement element = "kmarkdown".equals(type) ? + JacksonCardUtil.fromJson(elementNode, MarkdownElement.class) : + JacksonCardUtil.fromJson(elementNode, PlainTextElement.class); + if (element != null) { + elements.add(element); + } + } catch (Exception e) { + // 跳过无效元素 + } + } + } + } + + return new ContextModule(elements); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContextModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContextModuleSerializer.java new file mode 100644 index 00000000..81fdee48 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContextModuleSerializer.java @@ -0,0 +1,47 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.module.ContextModule; + +import java.io.IOException; + +/** + * ContextModule Jackson 序列化器 + * 将上下文模块序列化为标准的Kook卡片JSON格式 + */ +public class JacksonContextModuleSerializer extends JsonSerializer { + + @Override + public void serialize(ContextModule value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", "context"); + + // 序列化elements字段 + if (value.getModules() != null && !value.getModules().isEmpty()) { + gen.writeFieldName("elements"); + serializers.defaultSerializeValue(value.getModules(), gen); + } + + gen.writeEndObject(); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonCountdownModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonCountdownModuleDeserializer.java new file mode 100644 index 00000000..192c8a5f --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonCountdownModuleDeserializer.java @@ -0,0 +1,30 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; + +import snw.jkook.message.component.card.module.CountdownModule; +import snw.kookbc.util.JacksonCardUtil; + +public class JacksonCountdownModuleDeserializer extends JsonDeserializer { + @Override + public CountdownModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // CountdownModule构造器需要(CountdownModule.Type, long startTime, long endTime) + String modeStr = JacksonCardUtil.getStringOrDefault(node, "mode", "day"); + long startTime = JacksonCardUtil.getLongOrDefault(node, "startTime", 0L); + long endTime = JacksonCardUtil.getLongOrDefault(node, "endTime", System.currentTimeMillis() + 86400000L); // 默认24小时后 + + CountdownModule.Type type = CountdownModule.Type.value(modeStr); + return new CountdownModule(type, startTime, endTime); + } +} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonDividerModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonDividerModuleDeserializer.java new file mode 100644 index 00000000..6bf719bb --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonDividerModuleDeserializer.java @@ -0,0 +1,41 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import snw.jkook.message.component.card.module.DividerModule; + +import java.io.IOException; + +/** + * DividerModule Jackson 反序列化器 + * 处理分割线模块的反序列化(简单模块,无额外参数) + */ +public class JacksonDividerModuleDeserializer extends JsonDeserializer { + + @Override + public DividerModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // DividerModule 没有额外的参数,使用静态实例 + // 还是要读取 JSON 以保持解析器的一致性 + p.getCodec().readTree(p); + return DividerModule.INSTANCE; + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonDividerModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonDividerModuleSerializer.java new file mode 100644 index 00000000..fce16a5d --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonDividerModuleSerializer.java @@ -0,0 +1,40 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.module.DividerModule; + +import java.io.IOException; + +/** + * DividerModule Jackson 序列化器 + * 将分割线模块序列化为标准的Kook卡片JSON格式 + */ +public class JacksonDividerModuleSerializer extends JsonSerializer { + + @Override + public void serialize(DividerModule value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", "divider"); + gen.writeEndObject(); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonFileModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonFileModuleDeserializer.java new file mode 100644 index 00000000..12b7546c --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonFileModuleDeserializer.java @@ -0,0 +1,32 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; + +import snw.jkook.message.component.FileComponent; +import snw.jkook.message.component.card.module.FileModule; +import snw.kookbc.util.JacksonCardUtil; + +public class JacksonFileModuleDeserializer extends JsonDeserializer { + @Override + public FileModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // FileModule构造器需要(FileComponent.Type, String src, String title, String cover) + String typeStr = JacksonCardUtil.getStringOrDefault(node, "type", "file"); + String src = JacksonCardUtil.getStringOrDefault(node, "src", ""); + String title = JacksonCardUtil.getStringOrDefault(node, "title", ""); + String cover = JacksonCardUtil.getStringOrDefault(node, "cover", null); + + FileComponent.Type type = FileComponent.Type.value(typeStr); + return new FileModule(type, src, title, cover); + } +} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonHeaderModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonHeaderModuleDeserializer.java new file mode 100644 index 00000000..9a7005e5 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonHeaderModuleDeserializer.java @@ -0,0 +1,37 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; + +import snw.jkook.message.component.card.element.PlainTextElement; +import snw.jkook.message.component.card.module.HeaderModule; +import snw.kookbc.util.JacksonCardUtil; + +public class JacksonHeaderModuleDeserializer extends JsonDeserializer { + @Override + public HeaderModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // HeaderModule构造器需要PlainTextElement参数 + PlainTextElement textElement; + + if (JacksonCardUtil.has(node, "text")) { + JsonNode textNode = node.get("text"); + String content = JacksonCardUtil.getStringOrDefault(textNode, "content", ""); + textElement = new PlainTextElement(content); + } else { + // 如果没有text字段,使用空内容创建PlainTextElement + textElement = new PlainTextElement(""); + } + + return new HeaderModule(textElement); + } +} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonHeaderModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonHeaderModuleSerializer.java new file mode 100644 index 00000000..cfd9c433 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonHeaderModuleSerializer.java @@ -0,0 +1,45 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.module.HeaderModule; + +import java.io.IOException; + +/** + * HeaderModule Jackson 序列化器 + * 将标题模块序列化为标准的Kook卡片JSON格式 + */ +public class JacksonHeaderModuleSerializer extends JsonSerializer { + + @Override + public void serialize(HeaderModule value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", "header"); + + // 序列化text字段 + gen.writeFieldName("text"); + serializers.defaultSerializeValue(value.getElement(), gen); + + gen.writeEndObject(); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonImageGroupModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonImageGroupModuleDeserializer.java new file mode 100644 index 00000000..aebee730 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonImageGroupModuleDeserializer.java @@ -0,0 +1,45 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import snw.jkook.message.component.card.element.ImageElement; +import snw.jkook.message.component.card.module.ImageGroupModule; +import snw.kookbc.util.JacksonCardUtil; + +public class JacksonImageGroupModuleDeserializer extends JsonDeserializer { + @Override + public ImageGroupModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // ImageGroupModule构造器需要List参数 + List images = new ArrayList<>(); + + // 尝试解析elements字段 + if (JacksonCardUtil.has(node, "elements")) { + JsonNode elementsNode = node.get("elements"); + if (elementsNode.isArray()) { + for (JsonNode elementNode : elementsNode) { + try { + ImageElement imageElement = JacksonCardUtil.fromJson(elementNode, ImageElement.class); + images.add(imageElement); + } catch (Exception e) { + // 忽略无法解析的元素,继续处理其他元素 + } + } + } + } + + return new ImageGroupModule(images); + } +} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonInviteModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonInviteModuleDeserializer.java new file mode 100644 index 00000000..53cf87a7 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonInviteModuleDeserializer.java @@ -0,0 +1,26 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; + +import snw.jkook.message.component.card.module.InviteModule; +import snw.kookbc.util.JacksonCardUtil; + +public class JacksonInviteModuleDeserializer extends JsonDeserializer { + @Override + public InviteModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // InviteModule构造器需要String code参数 + String code = JacksonCardUtil.getStringOrDefault(node, "code", ""); + return new InviteModule(code); + } +} diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonSectionModuleDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonSectionModuleDeserializer.java new file mode 100644 index 00000000..f320a016 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonSectionModuleDeserializer.java @@ -0,0 +1,133 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.entity.abilities.Accessory; +import snw.jkook.message.component.card.CardScopeElement; +import snw.jkook.message.component.card.element.*; +import snw.jkook.message.component.card.module.SectionModule; +import snw.jkook.message.component.card.structure.Paragraph; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; + +/** + * SectionModule Jackson 序列化器 + * 处理章节模块的序列化和反序列化 + */ +public class JacksonSectionModuleDeserializer extends JsonDeserializer { + + @Override + public SectionModule deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // 解析文本内容 + CardScopeElement text = null; + if (JacksonCardUtil.has(node, "text")) { + JsonNode textNode = node.get("text"); + String textType = JacksonCardUtil.getStringOrDefault(textNode, "type", "plain-text"); + + switch (textType) { + case "plain-text": + text = JacksonCardUtil.fromJson(textNode, PlainTextElement.class); + break; + case "kmarkdown": + text = JacksonCardUtil.fromJson(textNode, MarkdownElement.class); + break; + case "paragraph": + text = JacksonCardUtil.fromJson(textNode, Paragraph.class); + break; + default: + throw new IOException("Unsupported text type in SectionModule: " + textType); + } + } + + // 解析模式 + Accessory.Mode mode = null; + if (JacksonCardUtil.has(node, "mode")) { + String modeValue = node.get("mode").asText(); + try { + mode = Accessory.Mode.value(modeValue); + } catch (Exception e) { + // 日志记录但不抛出异常,使用 null 值 + } + } + + // 解析附件 + Accessory accessory = null; + if (JacksonCardUtil.has(node, "accessory")) { + JsonNode accessoryNode = node.get("accessory"); + String accessoryType = JacksonCardUtil.getStringOrDefault(accessoryNode, "type", ""); + + switch (accessoryType) { + case "image": + accessory = JacksonCardUtil.fromJson(accessoryNode, ImageElement.class); + break; + case "button": + accessory = JacksonCardUtil.fromJson(accessoryNode, ButtonElement.class); + break; + default: + // 日志记录但不抛出异常,忽略不支持的附件类型 + break; + } + } + + return new SectionModule(text, accessory, mode); + } + + /** + * SectionModule 序列化器 + */ + public static class SectionModuleSerializer extends JsonSerializer { + + @Override + public void serialize(SectionModule module, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + + gen.writeStringField("type", "section"); + + // 序列化文本 + CardScopeElement text = module.getText(); + if (text != null) { + gen.writeObjectField("text", text); + } + + // 序列化模式 + Accessory.Mode mode = module.getMode(); + if (mode != null) { + gen.writeStringField("mode", mode.getValue()); + } + + // 序列化附件 + Accessory accessory = module.getAccessory(); + if (accessory != null) { + gen.writeObjectField("accessory", accessory); + } + + gen.writeEndObject(); + } + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonSectionModuleSerializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonSectionModuleSerializer.java new file mode 100644 index 00000000..08fea345 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonSectionModuleSerializer.java @@ -0,0 +1,58 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.module; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import snw.jkook.message.component.card.module.SectionModule; + +import java.io.IOException; + +/** + * SectionModule Jackson 序列化器 + * 将节模块序列化为标准的Kook卡片JSON格式 + */ +public class JacksonSectionModuleSerializer extends JsonSerializer { + + @Override + public void serialize(SectionModule value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", "section"); + + // 序列化text字段 + if (value.getText() != null) { + gen.writeFieldName("text"); + serializers.defaultSerializeValue(value.getText(), gen); + } + + // 序列化accessory字段(如果存在) + if (value.getAccessory() != null) { + gen.writeFieldName("accessory"); + serializers.defaultSerializeValue(value.getAccessory(), gen); + } + + // 序列化mode字段 + if (value.getMode() != null) { + gen.writeStringField("mode", value.getMode().getValue()); + } + + gen.writeEndObject(); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/structure/JacksonParagraphDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/structure/JacksonParagraphDeserializer.java new file mode 100644 index 00000000..38d8ddb8 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/component/jackson/card/structure/JacksonParagraphDeserializer.java @@ -0,0 +1,72 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.component.jackson.card.structure; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import snw.jkook.message.component.card.element.BaseElement; +import snw.jkook.message.component.card.element.MarkdownElement; +import snw.jkook.message.component.card.element.PlainTextElement; +import snw.jkook.message.component.card.structure.Paragraph; +import snw.kookbc.util.JacksonCardUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Paragraph Jackson 反序列化器 + * 处理段落结构的反序列化 + */ +public class JacksonParagraphDeserializer extends JsonDeserializer { + + @Override + public Paragraph deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + // 解析字段列表 + List fields = new ArrayList<>(); + + if (JacksonCardUtil.has(node, "fields")) { + JsonNode fieldsNode = node.get("fields"); + if (fieldsNode.isArray()) { + for (JsonNode fieldNode : fieldsNode) { + try { + String type = JacksonCardUtil.getStringOrDefault(fieldNode, "type", "plain-text"); + BaseElement field = "kmarkdown".equals(type) ? + JacksonCardUtil.fromJson(fieldNode, MarkdownElement.class) : + JacksonCardUtil.fromJson(fieldNode, PlainTextElement.class); + if (field != null) { + fields.add(field); + } + } catch (Exception e) { + // 跳过无效字段 + } + } + } + } + + // 解析列数 + int cols = JacksonCardUtil.getIntOrDefault(node, "cols", 1); + + return new Paragraph(cols, fields); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/impl/serializer/event/BaseEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/BaseEventDeserializer.java deleted file mode 100644 index 0e11b736..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/event/BaseEventDeserializer.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.event; - -import com.google.gson.*; -import snw.jkook.event.Event; -import snw.kookbc.impl.KBCClient; - -import java.lang.reflect.Type; - -public abstract class BaseEventDeserializer implements JsonDeserializer { - protected final KBCClient client; - - protected BaseEventDeserializer(KBCClient client) { - this.client = client; - } - - @Override - public final T deserialize(JsonElement element, Type type, JsonDeserializationContext ctx) throws JsonParseException { - final JsonObject object = element.getAsJsonObject(); - T t = deserialize(object, type, ctx); - beforeReturn(t); - return t; - } - - protected abstract T deserialize(JsonObject object, Type type, JsonDeserializationContext ctx) throws JsonParseException; - - // override it if you want to do something before we returning the final result. - protected void beforeReturn(T event) { - } - -} diff --git a/src/main/java/snw/kookbc/impl/serializer/event/NormalEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/NormalEventDeserializer.java deleted file mode 100644 index a9dfae00..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/event/NormalEventDeserializer.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.event; - -import static snw.kookbc.util.GsonUtil.getAsJsonObject; -import static snw.kookbc.util.GsonUtil.getAsLong; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; - -import snw.jkook.event.Event; -import snw.kookbc.impl.KBCClient; - -public abstract class NormalEventDeserializer extends BaseEventDeserializer { - - protected NormalEventDeserializer(KBCClient client) { - super(client); - } - - @Override - protected T deserialize(JsonObject object, Type type, JsonDeserializationContext ctx) throws JsonParseException { - final long timeStamp = getAsLong(object, "msg_timestamp"); - final JsonObject body = getAsJsonObject(getAsJsonObject(object, "extra"), "body"); - return deserialize(object, type, ctx, timeStamp, body); - } - - protected abstract T deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, long timeStamp, - JsonObject body) throws JsonParseException; - -} diff --git a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildBanUserEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildBanUserEventDeserializer.java deleted file mode 100644 index ef7b11ff..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildBanUserEventDeserializer.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.event.guild; - -import static java.util.Collections.unmodifiableList; -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; -import java.util.List; -import java.util.stream.Collectors; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; - -import snw.jkook.entity.Guild; -import snw.jkook.entity.User; -import snw.jkook.event.guild.GuildBanUserEvent; -import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; -import snw.kookbc.impl.storage.EntityStorage; - -public class GuildBanUserEventDeserializer extends NormalEventDeserializer { - - public GuildBanUserEventDeserializer(KBCClient client) { - super(client); - } - - @Override - protected GuildBanUserEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final EntityStorage entityStorage = client.getStorage(); - final Guild guild = entityStorage.getGuild(getAsString(object, "target_id")); - final User operator = entityStorage.getUser(getAsString(body, "operator_id")); - final List banned = unmodifiableList( - body.getAsJsonArray("user_id") - .asList() - .stream() - .map(JsonElement::getAsString) - .map(entityStorage::getUser) - .collect(Collectors.toList())); - final String reason = getAsString(body, "remark"); - return new GuildBanUserEvent(timeStamp, guild, banned, operator, reason); - } - -} diff --git a/src/main/java/snw/kookbc/impl/serializer/event/item/ItemConsumedEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/item/ItemConsumedEventDeserializer.java deleted file mode 100644 index 43243e8c..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/event/item/ItemConsumedEventDeserializer.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.event.item; - -import static com.google.gson.JsonParser.parseString; -import static snw.kookbc.util.GsonUtil.getAsInt; -import static snw.kookbc.util.GsonUtil.getAsJsonObject; -import static snw.kookbc.util.GsonUtil.getAsLong; -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; - -import snw.jkook.entity.User; -import snw.jkook.event.item.ItemConsumedEvent; -import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.BaseEventDeserializer; -import snw.kookbc.impl.storage.EntityStorage; - -public class ItemConsumedEventDeserializer extends BaseEventDeserializer { - - public ItemConsumedEventDeserializer(KBCClient client) { - super(client); - } - - @Override - protected ItemConsumedEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx) - throws JsonParseException { - final EntityStorage storage = client.getStorage(); - final JsonObject content = parseString(getAsString(object, "content")).getAsJsonObject(); - final JsonObject data = getAsJsonObject(content, "data"); - final long timeStamp = getAsLong(object, "msg_timestamp"); - final User consumer = storage.getUser(getAsString(data, "user_id")); - final User affected = storage.getUser(getAsString(data, "target_id")); - final int itemId = getAsInt(data, "item_id"); - return new ItemConsumedEvent(timeStamp, consumer, affected, itemId); - } - -} diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/BaseJacksonEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/BaseJacksonEventDeserializer.java new file mode 100644 index 00000000..1c008910 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/BaseJacksonEventDeserializer.java @@ -0,0 +1,98 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.event.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import snw.jkook.event.Event; +import snw.kookbc.impl.KBCClient; + +import java.io.IOException; + +import static snw.kookbc.util.JacksonUtil.get; + +/** + * Jackson 事件反序列化器基类 + * + *

提供事件反序列化的通用逻辑和工具方法。 + * + * @param 事件类型 + * @since KookBC 0.33.0 + */ +public abstract class BaseJacksonEventDeserializer extends JsonDeserializer { + + protected final KBCClient client; + + protected BaseJacksonEventDeserializer(KBCClient client) { + this.client = client; + } + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + T event = deserialize(node); + + // 调用后处理钩子 + if (event != null) { + beforeReturn(event); + } + + return event; + } + + /** + * 从 JsonNode 反序列化事件对象 + * + * @param node JSON 数据节点 + * @return 事件对象 + */ + protected abstract T deserialize(JsonNode node); + + /** + * 事件返回前的处理钩子 + *

子类可以重写此方法进行额外处理,例如更新缓存 + * + * @param event 事件对象 + */ + protected void beforeReturn(T event) { + // 默认不做处理,子类可以重写 + } + + /** + * 提取时间戳 + * + * @param node JSON 节点 + * @return 时间戳(毫秒) + */ + protected long extractTimeStamp(JsonNode node) { + return get(node, "msg_timestamp").asLong(); + } + + /** + * 提取 extra.body 节点 + * + * @param node JSON 节点 + * @return body 节点 + */ + protected JsonNode extractBody(JsonNode node) { + return get(get(node, "extra"), "body"); + } +} diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/JKookEventModule.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/JKookEventModule.java new file mode 100644 index 00000000..2ca56982 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/JKookEventModule.java @@ -0,0 +1,130 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.event.jackson; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import snw.jkook.event.channel.*; +import snw.jkook.event.guild.*; +import snw.jkook.event.item.ItemConsumedEvent; +import snw.jkook.event.pm.PrivateMessageDeleteEvent; +import snw.jkook.event.pm.PrivateMessageReceivedEvent; +import snw.jkook.event.pm.PrivateMessageUpdateEvent; +import snw.jkook.event.role.RoleCreateEvent; +import snw.jkook.event.role.RoleDeleteEvent; +import snw.jkook.event.role.RoleInfoUpdateEvent; +import snw.jkook.event.user.*; +import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.serializer.event.jackson.channel.*; +import snw.kookbc.impl.serializer.event.jackson.guild.*; +import snw.kookbc.impl.serializer.event.jackson.item.ItemConsumedEventJacksonDeserializer; +import snw.kookbc.impl.serializer.event.jackson.pm.PrivateMessageDeleteEventJacksonDeserializer; +import snw.kookbc.impl.serializer.event.jackson.pm.PrivateMessageReceivedEventJacksonDeserializer; +import snw.kookbc.impl.serializer.event.jackson.pm.PrivateMessageUpdateEventJacksonDeserializer; +import snw.kookbc.impl.serializer.event.jackson.role.RoleCreateEventJacksonDeserializer; +import snw.kookbc.impl.serializer.event.jackson.role.RoleDeleteEventJacksonDeserializer; +import snw.kookbc.impl.serializer.event.jackson.role.RoleInfoUpdateEventJacksonDeserializer; +import snw.kookbc.impl.serializer.event.jackson.user.*; + +/** + * Jackson 自定义模块 - JKook 事件反序列化 + * + *

该模块为所有 JKook 事件类注册了 Jackson 自定义反序列化器, + * 使得 Jackson 能够正确反序列化没有无参构造函数的事件对象。 + * + *

架构设计: + *

    + *
  • 每个事件类型对应一个专用的 Jackson Deserializer
  • + *
  • 反序列化器从 JsonNode 提取数据,然后调用事件类构造函数
  • + *
  • 提供更好的 null-safe 处理和性能优化
  • + *
  • 完全替代 GSON 反序列化器
  • + *
+ * + * @since KookBC 0.33.0 + * @see com.fasterxml.jackson.databind.module.SimpleModule + */ +public class JKookEventModule extends SimpleModule { + + private final KBCClient client; + + /** + * 创建 JKook 事件反序列化模块 + * + * @param client KBCClient 实例,用于传递给反序列化器 + */ + public JKookEventModule(KBCClient client) { + super("JKookEventModule"); + this.client = client; + registerDeserializers(); + } + + /** + * 注册所有事件反序列化器 + */ + private void registerDeserializers() { + // === Channel Events === + addDeserializer(ChannelMessageEvent.class, new ChannelMessageEventJacksonDeserializer(client)); + addDeserializer(ChannelCreateEvent.class, new ChannelCreateEventJacksonDeserializer(client)); + addDeserializer(ChannelDeleteEvent.class, new ChannelDeleteEventJacksonDeserializer(client)); + addDeserializer(ChannelInfoUpdateEvent.class, new ChannelInfoUpdateEventJacksonDeserializer(client)); + addDeserializer(ChannelMessageDeleteEvent.class, new ChannelMessageDeleteEventJacksonDeserializer(client)); + addDeserializer(ChannelMessagePinEvent.class, new ChannelMessagePinEventJacksonDeserializer(client)); + addDeserializer(ChannelMessageUnpinEvent.class, new ChannelMessageUnpinEventJacksonDeserializer(client)); + addDeserializer(ChannelMessageUpdateEvent.class, new ChannelMessageUpdateEventJacksonDeserializer(client)); + + // === Guild Events === + addDeserializer(GuildAddEmojiEvent.class, new GuildAddEmojiEventJacksonDeserializer(client)); + addDeserializer(GuildBanUserEvent.class, new GuildBanUserEventJacksonDeserializer(client)); + addDeserializer(GuildDeleteEvent.class, new GuildDeleteEventJacksonDeserializer(client)); + addDeserializer(GuildInfoUpdateEvent.class, new GuildInfoUpdateEventJacksonDeserializer(client)); + addDeserializer(GuildRemoveEmojiEvent.class, new GuildRemoveEmojiEventJacksonDeserializer(client)); + addDeserializer(GuildUnbanUserEvent.class, new GuildUnbanUserEventJacksonDeserializer(client)); + addDeserializer(GuildUpdateEmojiEvent.class, new GuildUpdateEmojiEventJacksonDeserializer(client)); + addDeserializer(GuildUserNickNameUpdateEvent.class, new GuildUserNickNameUpdateEventJacksonDeserializer(client)); + + // === Private Message Events === + addDeserializer(PrivateMessageReceivedEvent.class, new PrivateMessageReceivedEventJacksonDeserializer(client)); + addDeserializer(PrivateMessageDeleteEvent.class, new PrivateMessageDeleteEventJacksonDeserializer(client)); + addDeserializer(PrivateMessageUpdateEvent.class, new PrivateMessageUpdateEventJacksonDeserializer(client)); + + // === Role Events === + addDeserializer(RoleCreateEvent.class, new RoleCreateEventJacksonDeserializer(client)); + addDeserializer(RoleDeleteEvent.class, new RoleDeleteEventJacksonDeserializer(client)); + addDeserializer(RoleInfoUpdateEvent.class, new RoleInfoUpdateEventJacksonDeserializer(client)); + + // === User Events === + addDeserializer(UserAddReactionEvent.class, new UserAddReactionEventJacksonDeserializer(client)); + addDeserializer(UserClickButtonEvent.class, new UserClickButtonEventJacksonDeserializer(client)); + addDeserializer(UserInfoUpdateEvent.class, new UserInfoUpdateEventJacksonDeserializer(client)); + addDeserializer(UserJoinGuildEvent.class, new UserJoinGuildEventJacksonDeserializer(client)); + addDeserializer(UserJoinVoiceChannelEvent.class, new UserJoinVoiceChannelEventJacksonDeserializer(client)); + addDeserializer(UserLeaveGuildEvent.class, new UserLeaveGuildEventJacksonDeserializer(client)); + addDeserializer(UserLeaveVoiceChannelEvent.class, new UserLeaveVoiceChannelEventJacksonDeserializer(client)); + addDeserializer(UserOfflineEvent.class, new UserOfflineEventJacksonDeserializer(client)); + addDeserializer(UserOnlineEvent.class, new UserOnlineEventJacksonDeserializer(client)); + addDeserializer(UserRemoveReactionEvent.class, new UserRemoveReactionEventJacksonDeserializer(client)); + + // === Item Events === + addDeserializer(ItemConsumedEvent.class, new ItemConsumedEventJacksonDeserializer(client)); + } + + @Override + public String getModuleName() { + return "JKookEventModule"; + } +} diff --git a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelCreateEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelCreateEventJacksonDeserializer.java similarity index 68% rename from src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelCreateEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelCreateEventJacksonDeserializer.java index 0b2588d4..526d5e04 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelCreateEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelCreateEventJacksonDeserializer.java @@ -16,28 +16,30 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.channel; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.channel; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.channel.Channel; import snw.jkook.event.channel.ChannelCreateEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class ChannelCreateEventDeserializer extends NormalEventDeserializer { +/** + * ChannelCreateEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class ChannelCreateEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public ChannelCreateEventDeserializer(KBCClient client) { + public ChannelCreateEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected ChannelCreateEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { + protected ChannelCreateEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + final Channel channel = client.getEntityBuilder().buildChannel(body); return new ChannelCreateEvent(timeStamp, channel); } @@ -46,5 +48,4 @@ protected ChannelCreateEvent deserialize(JsonObject object, Type type, JsonDeser protected void beforeReturn(ChannelCreateEvent event) { client.getStorage().addChannel(event.getChannel()); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelDeleteEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelDeleteEventJacksonDeserializer.java similarity index 60% rename from src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelDeleteEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelDeleteEventJacksonDeserializer.java index 7678229b..96390156 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelDeleteEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelDeleteEventJacksonDeserializer.java @@ -16,32 +16,32 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.channel; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.channel; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.event.channel.ChannelDeleteEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class ChannelDeleteEventDeserializer extends NormalEventDeserializer { +/** + * ChannelDeleteEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class ChannelDeleteEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public ChannelDeleteEventDeserializer(KBCClient client) { + public ChannelDeleteEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected ChannelDeleteEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final String id = getAsString(body, "id"); - final Guild guild = client.getStorage().getGuild(getAsString(object, "target_id")); + protected ChannelDeleteEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String id = body.get("id").asText(); + final Guild guild = client.getStorage().getGuild(node.get("target_id").asText()); return new ChannelDeleteEvent(timeStamp, id, guild); } @@ -49,5 +49,4 @@ protected ChannelDeleteEvent deserialize(JsonObject object, Type type, JsonDeser protected void beforeReturn(ChannelDeleteEvent event) { client.getStorage().removeChannel(event.getChannelId()); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelInfoUpdateEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelInfoUpdateEventJacksonDeserializer.java similarity index 53% rename from src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelInfoUpdateEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelInfoUpdateEventJacksonDeserializer.java index 74630c6a..9a51db52 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelInfoUpdateEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelInfoUpdateEventJacksonDeserializer.java @@ -16,36 +16,46 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.channel; +package snw.kookbc.impl.serializer.event.jackson.channel; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.event.channel.ChannelInfoUpdateEvent; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.channel.ChannelImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; - -import java.lang.reflect.Type; -import java.util.Objects; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; +import snw.kookbc.util.JacksonUtil; import static snw.kookbc.impl.entity.builder.EntityBuildUtil.parseChannel; -import static snw.kookbc.util.GsonUtil.getAsInt; -import static snw.kookbc.util.GsonUtil.getAsString; -public class ChannelInfoUpdateEventDeserializer extends NormalEventDeserializer { +/** + * ChannelInfoUpdateEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class ChannelInfoUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public ChannelInfoUpdateEventDeserializer(KBCClient client) { + public ChannelInfoUpdateEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected ChannelInfoUpdateEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, long timeStamp, JsonObject body) throws JsonParseException { - final String id = getAsString(body, "id"); - final int channelType = getAsInt(body, "type"); + protected ChannelInfoUpdateEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String id = JacksonUtil.getStringOrDefault(body, "id", null); + final int channelType = JacksonUtil.getIntOrDefault(body, "type", 0); + + if (id == null) { + throw new RuntimeException("Missing required field 'id' in channel update event"); + } + final ChannelImpl channel = (ChannelImpl) parseChannel(client, id, channelType); - Objects.requireNonNull(channel).update(body); + if (channel == null) { + throw new RuntimeException("Unable to parse channel with id: " + id + ", type: " + channelType); + } + + channel.update(body); return new ChannelInfoUpdateEvent(timeStamp, channel); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageDeleteEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageDeleteEventJacksonDeserializer.java similarity index 58% rename from src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageDeleteEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageDeleteEventJacksonDeserializer.java index 83e7cec2..e1a71fff 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageDeleteEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageDeleteEventJacksonDeserializer.java @@ -16,33 +16,32 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.channel; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.channel; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.channel.Channel; import snw.jkook.event.channel.ChannelMessageDeleteEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class ChannelMessageDeleteEventDeserializer extends NormalEventDeserializer { +/** + * ChannelMessageDeleteEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class ChannelMessageDeleteEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public ChannelMessageDeleteEventDeserializer(KBCClient client) { + public ChannelMessageDeleteEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected ChannelMessageDeleteEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - // if this error, we can regard it as internal error - final Channel channel = client.getStorage().getChannel(getAsString(body, "channel_id")); - final String messageId = getAsString(body, "msg_id"); + protected ChannelMessageDeleteEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final Channel channel = client.getStorage().getChannel(body.get("channel_id").asText()); + final String messageId = body.get("msg_id").asText(); return new ChannelMessageDeleteEvent(timeStamp, channel, messageId); } @@ -50,5 +49,4 @@ protected ChannelMessageDeleteEvent deserialize(JsonObject object, Type type, Js protected void beforeReturn(ChannelMessageDeleteEvent event) { client.getStorage().removeMessage(event.getMessageId()); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageEventJacksonDeserializer.java similarity index 70% rename from src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageEventJacksonDeserializer.java index 58975e03..529178e4 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageEventJacksonDeserializer.java @@ -16,30 +16,29 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.channel; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.channel; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.channel.NonCategoryChannel; import snw.jkook.event.channel.ChannelMessageEvent; import snw.jkook.message.ChannelMessage; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.BaseEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class ChannelMessageEventDeserializer extends BaseEventDeserializer { +/** + * ChannelMessageEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class ChannelMessageEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public ChannelMessageEventDeserializer(KBCClient client) { + public ChannelMessageEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected ChannelMessageEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx) - throws JsonParseException { - final ChannelMessage msg = client.getMessageBuilder().buildChannelMessage(object); + protected ChannelMessageEvent deserialize(JsonNode node) { + final ChannelMessage msg = client.getMessageBuilder().buildChannelMessage(node); final long timeStamp = msg.getTimeStamp(); final NonCategoryChannel channel = msg.getChannel(); return new ChannelMessageEvent(timeStamp, channel, msg); @@ -49,5 +48,4 @@ protected ChannelMessageEvent deserialize(JsonObject object, Type type, JsonDese protected void beforeReturn(ChannelMessageEvent event) { client.getStorage().addMessage(event.getMessage()); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessagePinEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessagePinEventJacksonDeserializer.java similarity index 57% rename from src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessagePinEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessagePinEventJacksonDeserializer.java index 68534cfb..584f8f18 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessagePinEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessagePinEventJacksonDeserializer.java @@ -16,41 +16,39 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.channel; - -import static snw.kookbc.util.GsonUtil.getAsInt; -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.channel; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.User; import snw.jkook.entity.channel.Channel; import snw.jkook.event.channel.ChannelMessagePinEvent; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.channel.TextChannelImpl; import snw.kookbc.impl.entity.channel.VoiceChannelImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class ChannelMessagePinEventDeserializer extends NormalEventDeserializer { +/** + * ChannelMessagePinEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class ChannelMessagePinEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public ChannelMessagePinEventDeserializer(KBCClient client) { + public ChannelMessagePinEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected ChannelMessagePinEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final String id = getAsString(body, "channel_id"); - final Channel channel = getAsInt(body, "channel_type") == 1 + protected ChannelMessagePinEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String id = body.get("channel_id").asText(); + final Channel channel = body.get("channel_type").asInt() == 1 ? new TextChannelImpl(client, id) : new VoiceChannelImpl(client, id); - final String msgId = getAsString(body, "msg_id"); - final User operator = client.getStorage().getUser(getAsString(body, "operator_id")); + final String msgId = body.get("msg_id").asText(); + final User operator = client.getStorage().getUser(body.get("operator_id").asText()); return new ChannelMessagePinEvent(timeStamp, channel, msgId, operator); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageUnpinEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUnpinEventJacksonDeserializer.java similarity index 57% rename from src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageUnpinEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUnpinEventJacksonDeserializer.java index d80f1471..4957599b 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageUnpinEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUnpinEventJacksonDeserializer.java @@ -16,41 +16,39 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.channel; - -import static snw.kookbc.util.GsonUtil.getAsInt; -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.channel; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.User; import snw.jkook.entity.channel.Channel; import snw.jkook.event.channel.ChannelMessageUnpinEvent; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.channel.TextChannelImpl; import snw.kookbc.impl.entity.channel.VoiceChannelImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class ChannelMessageUnpinEventDeserializer extends NormalEventDeserializer { +/** + * ChannelMessageUnpinEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class ChannelMessageUnpinEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public ChannelMessageUnpinEventDeserializer(KBCClient client) { + public ChannelMessageUnpinEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected ChannelMessageUnpinEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final String id = getAsString(body, "channel_id"); - final Channel channel = getAsInt(body, "channel_type") == 1 + protected ChannelMessageUnpinEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String id = body.get("channel_id").asText(); + final Channel channel = body.get("channel_type").asInt() == 1 ? new TextChannelImpl(client, id) : new VoiceChannelImpl(client, id); - final String msgId = getAsString(body, "msg_id"); - final User operator = client.getStorage().getUser(getAsString(body, "operator_id")); + final String msgId = body.get("msg_id").asText(); + final User operator = client.getStorage().getUser(body.get("operator_id").asText()); return new ChannelMessageUnpinEvent(timeStamp, channel, msgId, operator); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageUpdateEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUpdateEventJacksonDeserializer.java similarity index 64% rename from src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageUpdateEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUpdateEventJacksonDeserializer.java index 198717b7..89b6bc95 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/channel/ChannelMessageUpdateEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUpdateEventJacksonDeserializer.java @@ -16,17 +16,9 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.channel; - -import static snw.kookbc.util.GsonUtil.getAsInt; -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.channel; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.channel.Channel; import snw.jkook.event.channel.ChannelMessageUpdateEvent; import snw.jkook.message.Message; @@ -35,23 +27,30 @@ import snw.kookbc.impl.entity.channel.TextChannelImpl; import snw.kookbc.impl.entity.channel.VoiceChannelImpl; import snw.kookbc.impl.message.MessageImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class ChannelMessageUpdateEventDeserializer extends NormalEventDeserializer { +/** + * ChannelMessageUpdateEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class ChannelMessageUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public ChannelMessageUpdateEventDeserializer(KBCClient client) { + public ChannelMessageUpdateEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected ChannelMessageUpdateEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final String id = getAsString(body, "channel_id"); - final Channel channel = getAsInt(body, "channel_type") == 1 + protected ChannelMessageUpdateEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String id = body.get("channel_id").asText(); + final Channel channel = body.get("channel_type").asInt() == 1 ? new TextChannelImpl(client, id) : new VoiceChannelImpl(client, id); - final String msgId = getAsString(body, "msg_id"); - final String content = getAsString(object, "content"); + final String msgId = body.get("msg_id").asText(); + final String content = node.get("content").asText(); return new ChannelMessageUpdateEvent(timeStamp, channel, msgId, content); } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildAddEmojiEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildAddEmojiEventJacksonDeserializer.java similarity index 67% rename from src/main/java/snw/kookbc/impl/serializer/event/guild/GuildAddEmojiEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildAddEmojiEventJacksonDeserializer.java index 5d25d3e7..d2c71409 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildAddEmojiEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildAddEmojiEventJacksonDeserializer.java @@ -16,32 +16,33 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.guild; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.guild; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.Guild; import snw.jkook.event.guild.GuildAddEmojiEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class GuildAddEmojiEventDeserializer extends NormalEventDeserializer { +/** + * GuildAddEmojiEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class GuildAddEmojiEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public GuildAddEmojiEventDeserializer(KBCClient client) { + public GuildAddEmojiEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected GuildAddEmojiEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { + protected GuildAddEmojiEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + final CustomEmoji customEmoji = client.getEntityBuilder().buildEmoji(body); final Guild guild = customEmoji.getGuild(); return new GuildAddEmojiEvent(timeStamp, guild, customEmoji); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildBanUserEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildBanUserEventJacksonDeserializer.java new file mode 100644 index 00000000..a85cf805 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildBanUserEventJacksonDeserializer.java @@ -0,0 +1,66 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.event.jackson.guild; + +import com.fasterxml.jackson.databind.JsonNode; +import snw.jkook.entity.Guild; +import snw.jkook.entity.User; +import snw.jkook.event.guild.GuildBanUserEvent; +import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; +import snw.kookbc.impl.storage.EntityStorage; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * GuildBanUserEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class GuildBanUserEventJacksonDeserializer extends BaseJacksonEventDeserializer { + + public GuildBanUserEventJacksonDeserializer(KBCClient client) { + super(client); + } + + @Override + protected GuildBanUserEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final EntityStorage entityStorage = client.getStorage(); + final Guild guild = entityStorage.getGuild(node.get("target_id").asText()); + final User operator = entityStorage.getUser(body.get("operator_id").asText()); + + // 转换 JsonNode 数组为 List + List bannedList = new ArrayList<>(); + JsonNode userIdArray = body.get("user_id"); + if (userIdArray != null && userIdArray.isArray()) { + for (JsonNode userIdNode : userIdArray) { + bannedList.add(entityStorage.getUser(userIdNode.asText())); + } + } + final List banned = Collections.unmodifiableList(bannedList); + + final String reason = body.get("remark").asText(); + return new GuildBanUserEvent(timeStamp, guild, banned, operator, reason); + } +} diff --git a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildDeleteEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildDeleteEventJacksonDeserializer.java similarity index 60% rename from src/main/java/snw/kookbc/impl/serializer/event/guild/GuildDeleteEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildDeleteEventJacksonDeserializer.java index 16913b17..78f4124d 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildDeleteEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildDeleteEventJacksonDeserializer.java @@ -16,32 +16,31 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.guild; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.guild; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.event.guild.GuildDeleteEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class GuildDeleteEventDeserializer extends NormalEventDeserializer { +/** + * GuildDeleteEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class GuildDeleteEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public GuildDeleteEventDeserializer(KBCClient client) { + public GuildDeleteEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected GuildDeleteEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, long timeStamp, - JsonObject body) throws JsonParseException { - final String id = getAsString(body, "id"); + protected GuildDeleteEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String id = body.get("id").asText(); client.getStorage().removeGuild(id); return new GuildDeleteEvent(timeStamp, id); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildInfoUpdateEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildInfoUpdateEventJacksonDeserializer.java similarity index 60% rename from src/main/java/snw/kookbc/impl/serializer/event/guild/GuildInfoUpdateEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildInfoUpdateEventJacksonDeserializer.java index 6cf98a42..13505cb4 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildInfoUpdateEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildInfoUpdateEventJacksonDeserializer.java @@ -16,34 +16,33 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.guild; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.guild; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.event.guild.GuildInfoUpdateEvent; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.GuildImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class GuildInfoUpdateEventDeserializer extends NormalEventDeserializer { +/** + * GuildInfoUpdateEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class GuildInfoUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public GuildInfoUpdateEventDeserializer(KBCClient client) { + public GuildInfoUpdateEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected GuildInfoUpdateEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final Guild guild = client.getStorage().getGuild(getAsString(body, "id")); + protected GuildInfoUpdateEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final Guild guild = client.getStorage().getGuild(body.get("id").asText()); ((GuildImpl) guild).update(body); return new GuildInfoUpdateEvent(timeStamp, guild); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildRemoveEmojiEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildRemoveEmojiEventJacksonDeserializer.java similarity index 66% rename from src/main/java/snw/kookbc/impl/serializer/event/guild/GuildRemoveEmojiEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildRemoveEmojiEventJacksonDeserializer.java index f4ed0c69..c9d862ad 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildRemoveEmojiEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildRemoveEmojiEventJacksonDeserializer.java @@ -16,32 +16,32 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.guild; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.guild; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.Guild; import snw.jkook.event.guild.GuildRemoveEmojiEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class GuildRemoveEmojiEventDeserializer extends NormalEventDeserializer { +/** + * GuildRemoveEmojiEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class GuildRemoveEmojiEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public GuildRemoveEmojiEventDeserializer(KBCClient client) { + public GuildRemoveEmojiEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected GuildRemoveEmojiEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final CustomEmoji customEmoji = client.getStorage().getEmoji(getAsString(body, "id"), body); + protected GuildRemoveEmojiEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final CustomEmoji customEmoji = client.getStorage().getEmoji(body.get("id").asText(), body); final Guild guild = customEmoji.getGuild(); return new GuildRemoveEmojiEvent(timeStamp, guild, customEmoji); } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildUnbanUserEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUnbanUserEventJacksonDeserializer.java similarity index 53% rename from src/main/java/snw/kookbc/impl/serializer/event/guild/GuildUnbanUserEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUnbanUserEventJacksonDeserializer.java index ade2a970..935a4e83 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildUnbanUserEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUnbanUserEventJacksonDeserializer.java @@ -16,47 +16,50 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.guild; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.guild; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.User; import snw.jkook.event.guild.GuildUnbanUserEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; import snw.kookbc.impl.storage.EntityStorage; -public class GuildUnbanUserEventDeserializer extends NormalEventDeserializer { +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * GuildUnbanUserEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class GuildUnbanUserEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public GuildUnbanUserEventDeserializer(KBCClient client) { + public GuildUnbanUserEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected GuildUnbanUserEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { + protected GuildUnbanUserEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + final EntityStorage storage = client.getStorage(); - final Guild guild = storage.getGuild(getAsString(object, "target_id")); - final List unbanned = Collections.unmodifiableList( - body.getAsJsonArray("user_id") - .asList() - .stream() - .map(JsonElement::getAsString) - .map(storage::getUser) - .collect(Collectors.toList())); - final User operator = storage.getUser(getAsString(body, "operator_id")); + final Guild guild = storage.getGuild(node.get("target_id").asText()); + + // 转换 JsonNode 数组为 List + List unbannedList = new ArrayList<>(); + JsonNode userIdArray = body.get("user_id"); + if (userIdArray != null && userIdArray.isArray()) { + for (JsonNode userIdNode : userIdArray) { + unbannedList.add(storage.getUser(userIdNode.asText())); + } + } + final List unbanned = Collections.unmodifiableList(unbannedList); + + final User operator = storage.getUser(body.get("operator_id").asText()); return new GuildUnbanUserEvent(timeStamp, guild, unbanned, operator); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildUpdateEmojiEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUpdateEmojiEventJacksonDeserializer.java similarity index 65% rename from src/main/java/snw/kookbc/impl/serializer/event/guild/GuildUpdateEmojiEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUpdateEmojiEventJacksonDeserializer.java index a7735306..67b93b77 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildUpdateEmojiEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUpdateEmojiEventJacksonDeserializer.java @@ -16,36 +16,35 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.guild; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.guild; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.Guild; import snw.jkook.event.guild.GuildUpdateEmojiEvent; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.CustomEmojiImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class GuildUpdateEmojiEventDeserializer extends NormalEventDeserializer { +/** + * GuildUpdateEmojiEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class GuildUpdateEmojiEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public GuildUpdateEmojiEventDeserializer(KBCClient client) { + public GuildUpdateEmojiEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected GuildUpdateEmojiEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final CustomEmoji customEmoji = client.getStorage().getEmoji(getAsString(body, "id"), body); + protected GuildUpdateEmojiEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final CustomEmoji customEmoji = client.getStorage().getEmoji(body.get("id").asText(), body); final Guild guild = customEmoji.getGuild(); ((CustomEmojiImpl) customEmoji).update(body); return new GuildUpdateEmojiEvent(timeStamp, guild, customEmoji); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildUserNickNameUpdateEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUserNickNameUpdateEventJacksonDeserializer.java similarity index 50% rename from src/main/java/snw/kookbc/impl/serializer/event/guild/GuildUserNickNameUpdateEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUserNickNameUpdateEventJacksonDeserializer.java index 23997235..c1ccbe3c 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/guild/GuildUserNickNameUpdateEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUserNickNameUpdateEventJacksonDeserializer.java @@ -16,48 +16,57 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.guild; - -import static snw.kookbc.util.GsonUtil.getAsString; -import static snw.kookbc.util.GsonUtil.has; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.guild; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.User; import snw.jkook.event.guild.GuildUserNickNameUpdateEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; import snw.kookbc.impl.storage.EntityStorage; +import snw.kookbc.util.JacksonUtil; -public class GuildUserNickNameUpdateEventDeserializer extends NormalEventDeserializer { +/** + * GuildUserNickNameUpdateEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class GuildUserNickNameUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public GuildUserNickNameUpdateEventDeserializer(KBCClient client) { + public GuildUserNickNameUpdateEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected GuildUserNickNameUpdateEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { + protected GuildUserNickNameUpdateEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + String guildId; User user; String nickname; final EntityStorage entityStorage = client.getStorage(); - if (has(body, "my_nickname")) { // it is from GuildInfoUpdateEvent body... - guildId = getAsString(body, "id"); + + if (JacksonUtil.hasNonNull(body, "my_nickname")) { + // GuildInfoUpdateEvent 中的昵称更新 + guildId = JacksonUtil.getRequiredString(body, "id"); user = client.getCore().getUser(); - nickname = getAsString(body, "my_nickname"); + nickname = JacksonUtil.getStringOrDefault(body, "my_nickname", ""); } else { - guildId = getAsString(object, "target_id"); - user = entityStorage.getUser(getAsString(body, "user_id")); - nickname = getAsString(body, "nickname"); + // 普通用户昵称更新事件 + guildId = JacksonUtil.getRequiredString(node, "target_id"); + + String userId = JacksonUtil.getStringOrDefault(body, "user_id", null); + if (userId == null) { + throw new RuntimeException("Missing required field 'user_id' in guild member update event"); + } + user = entityStorage.getUser(userId); + + nickname = JacksonUtil.getStringOrDefault(body, "nickname", ""); } + final Guild guild = entityStorage.getGuild(guildId); return new GuildUserNickNameUpdateEvent(timeStamp, guild, user, nickname); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/item/ItemConsumedEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/item/ItemConsumedEventJacksonDeserializer.java new file mode 100644 index 00000000..a5e0a8e1 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/item/ItemConsumedEventJacksonDeserializer.java @@ -0,0 +1,51 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.event.jackson.item; + +import com.fasterxml.jackson.databind.JsonNode; +import snw.jkook.entity.User; +import snw.jkook.event.item.ItemConsumedEvent; +import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; +import snw.kookbc.impl.storage.EntityStorage; +import snw.kookbc.util.JacksonUtil; + +/** + * ItemConsumedEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class ItemConsumedEventJacksonDeserializer extends BaseJacksonEventDeserializer { + + public ItemConsumedEventJacksonDeserializer(KBCClient client) { + super(client); + } + + @Override + protected ItemConsumedEvent deserialize(JsonNode node) { + final EntityStorage storage = client.getStorage(); + final JsonNode content = JacksonUtil.parse(node.get("content").asText()); + final JsonNode data = content.get("data"); + final long timeStamp = node.get("msg_timestamp").asLong(); + final User consumer = storage.getUser(data.get("user_id").asText()); + final User affected = storage.getUser(data.get("target_id").asText()); + final int itemId = data.get("item_id").asInt(); + return new ItemConsumedEvent(timeStamp, consumer, affected, itemId); + } +} diff --git a/src/main/java/snw/kookbc/impl/serializer/event/pm/PrivateMessageDeleteEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageDeleteEventJacksonDeserializer.java similarity index 61% rename from src/main/java/snw/kookbc/impl/serializer/event/pm/PrivateMessageDeleteEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageDeleteEventJacksonDeserializer.java index 942a213c..ef6de85b 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/pm/PrivateMessageDeleteEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageDeleteEventJacksonDeserializer.java @@ -16,30 +16,30 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.pm; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.pm; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.event.pm.PrivateMessageDeleteEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class PrivateMessageDeleteEventDeserializer extends NormalEventDeserializer { +/** + * PrivateMessageDeleteEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class PrivateMessageDeleteEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public PrivateMessageDeleteEventDeserializer(KBCClient client) { + public PrivateMessageDeleteEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected PrivateMessageDeleteEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final String messageId = getAsString(body, "msg_id"); + protected PrivateMessageDeleteEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String messageId = body.get("msg_id").asText(); return new PrivateMessageDeleteEvent(timeStamp, messageId); } @@ -47,5 +47,4 @@ protected PrivateMessageDeleteEvent deserialize(JsonObject object, Type type, Js protected void beforeReturn(PrivateMessageDeleteEvent event) { client.getStorage().removeMessage(event.getMessageId()); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/pm/PrivateMessageReceivedEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageReceivedEventJacksonDeserializer.java similarity index 69% rename from src/main/java/snw/kookbc/impl/serializer/event/pm/PrivateMessageReceivedEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageReceivedEventJacksonDeserializer.java index 7be452bf..da83c799 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/pm/PrivateMessageReceivedEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageReceivedEventJacksonDeserializer.java @@ -16,30 +16,29 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.pm; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.pm; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.User; import snw.jkook.event.pm.PrivateMessageReceivedEvent; import snw.jkook.message.PrivateMessage; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.BaseEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class PrivateMessageReceivedEventDeserializer extends BaseEventDeserializer { +/** + * PrivateMessageReceivedEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class PrivateMessageReceivedEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public PrivateMessageReceivedEventDeserializer(KBCClient client) { + public PrivateMessageReceivedEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected PrivateMessageReceivedEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx) - throws JsonParseException { - final PrivateMessage pm = client.getMessageBuilder().buildPrivateMessage(object); + protected PrivateMessageReceivedEvent deserialize(JsonNode node) { + final PrivateMessage pm = client.getMessageBuilder().buildPrivateMessage(node); final long timeStamp = pm.getTimeStamp(); final User user = pm.getSender(); return new PrivateMessageReceivedEvent(timeStamp, user, pm); @@ -49,5 +48,4 @@ protected PrivateMessageReceivedEvent deserialize(JsonObject object, Type type, protected void beforeReturn(PrivateMessageReceivedEvent event) { client.getStorage().addMessage(event.getMessage()); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/pm/PrivateMessageUpdateEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageUpdateEventJacksonDeserializer.java similarity index 56% rename from src/main/java/snw/kookbc/impl/serializer/event/pm/PrivateMessageUpdateEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageUpdateEventJacksonDeserializer.java index 0ba899a8..361527f6 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/pm/PrivateMessageUpdateEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageUpdateEventJacksonDeserializer.java @@ -16,32 +16,31 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.pm; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.pm; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.event.pm.PrivateMessageUpdateEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class PrivateMessageUpdateEventDeserializer extends NormalEventDeserializer { +/** + * PrivateMessageUpdateEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class PrivateMessageUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public PrivateMessageUpdateEventDeserializer(KBCClient client) { + public PrivateMessageUpdateEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected PrivateMessageUpdateEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final String messageId = getAsString(body, "msg_id"); - final String content = getAsString(body, "content"); + protected PrivateMessageUpdateEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String messageId = body.get("msg_id").asText(); + final String content = body.get("content").asText(); return new PrivateMessageUpdateEvent(timeStamp, messageId, content); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/role/RoleCreateEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleCreateEventJacksonDeserializer.java similarity index 65% rename from src/main/java/snw/kookbc/impl/serializer/event/role/RoleCreateEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleCreateEventJacksonDeserializer.java index 89eeefba..1bf92a71 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/role/RoleCreateEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleCreateEventJacksonDeserializer.java @@ -16,32 +16,32 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.role; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.role; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.Role; import snw.jkook.event.role.RoleCreateEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class RoleCreateEventDeserializer extends NormalEventDeserializer { +/** + * RoleCreateEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class RoleCreateEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public RoleCreateEventDeserializer(KBCClient client) { + public RoleCreateEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected RoleCreateEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, long timeStamp, - JsonObject body) throws JsonParseException { - final Guild guild = client.getStorage().getGuild(getAsString(object, "target_id")); + protected RoleCreateEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final Guild guild = client.getStorage().getGuild(node.get("target_id").asText()); final Role role = client.getEntityBuilder().buildRole(guild, body); return new RoleCreateEvent(timeStamp, role); } @@ -52,5 +52,4 @@ protected void beforeReturn(RoleCreateEvent event) { final Role role = event.getRole(); client.getStorage().addRole(guild, role); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/role/RoleDeleteEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleDeleteEventJacksonDeserializer.java similarity index 60% rename from src/main/java/snw/kookbc/impl/serializer/event/role/RoleDeleteEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleDeleteEventJacksonDeserializer.java index 030a1f7d..67624eb4 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/role/RoleDeleteEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleDeleteEventJacksonDeserializer.java @@ -16,33 +16,33 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.role; - -import static snw.kookbc.util.GsonUtil.getAsInt; -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.role; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.Role; import snw.jkook.event.role.RoleDeleteEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class RoleDeleteEventDeserializer extends NormalEventDeserializer { - public RoleDeleteEventDeserializer(KBCClient client) { +/** + * RoleDeleteEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class RoleDeleteEventJacksonDeserializer extends BaseJacksonEventDeserializer { + + public RoleDeleteEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected RoleDeleteEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, long timeStamp, - JsonObject body) throws JsonParseException { - final Guild guild = client.getStorage().getGuild(getAsString(object, "target_id")); - final int roleId = getAsInt(body, "role_id"); + protected RoleDeleteEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final Guild guild = client.getStorage().getGuild(node.get("target_id").asText()); + final int roleId = body.get("role_id").asInt(); final Role role = client.getStorage().getRole(guild, roleId, body); return new RoleDeleteEvent(timeStamp, role); } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/role/RoleInfoUpdateEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleInfoUpdateEventJacksonDeserializer.java similarity index 61% rename from src/main/java/snw/kookbc/impl/serializer/event/role/RoleInfoUpdateEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleInfoUpdateEventJacksonDeserializer.java index 2ad44b9d..2df7a0af 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/role/RoleInfoUpdateEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleInfoUpdateEventJacksonDeserializer.java @@ -16,36 +16,36 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.role; +package snw.kookbc.impl.serializer.event.jackson.role; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.Role; import snw.jkook.event.role.RoleInfoUpdateEvent; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.RoleImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -import java.lang.reflect.Type; - -import static snw.kookbc.util.GsonUtil.get; - -public class RoleInfoUpdateEventDeserializer extends NormalEventDeserializer { +/** + * RoleInfoUpdateEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class RoleInfoUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public RoleInfoUpdateEventDeserializer(KBCClient client) { + public RoleInfoUpdateEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected RoleInfoUpdateEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final Guild guild = client.getStorage().getGuild(get(object, "target_id").getAsString()); - final int roleId = get(body, "role_id").getAsInt(); + protected RoleInfoUpdateEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final Guild guild = client.getStorage().getGuild(node.get("target_id").asText()); + final int roleId = body.get("role_id").asInt(); ((RoleImpl) client.getStorage().getRole(guild, roleId, body)).update(body); final Role role = client.getStorage().getRole(guild, roleId); return new RoleInfoUpdateEvent(timeStamp, role); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/user/UserAddReactionEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserAddReactionEventJacksonDeserializer.java similarity index 55% rename from src/main/java/snw/kookbc/impl/serializer/event/user/UserAddReactionEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserAddReactionEventJacksonDeserializer.java index 4c64b485..9dc3b534 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/user/UserAddReactionEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserAddReactionEventJacksonDeserializer.java @@ -16,44 +16,43 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.user; - -import static snw.kookbc.util.GsonUtil.getAsJsonObject; -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.user; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.User; import snw.jkook.event.user.UserAddReactionEvent; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.ReactionImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class UserAddReactionEventDeserializer extends NormalEventDeserializer { +/** + * UserAddReactionEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class UserAddReactionEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public UserAddReactionEventDeserializer(KBCClient client) { + public UserAddReactionEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected UserAddReactionEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final String messageId = getAsString(body, "msg_id"); - final User user = client.getStorage().getUser(getAsString(body, "user_id")); - final JsonObject rawEmoji = getAsJsonObject(body, "emoji"); - final CustomEmoji emoji = client.getStorage().getEmoji(getAsString(rawEmoji, "id"), rawEmoji); + protected UserAddReactionEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String messageId = body.get("msg_id").asText(); + final User user = client.getStorage().getUser(body.get("user_id").asText()); + final JsonNode rawEmoji = body.get("emoji"); + final CustomEmoji emoji = client.getStorage().getEmoji(rawEmoji.get("id").asText(), rawEmoji); final ReactionImpl reaction = new ReactionImpl(client, messageId, emoji, user, timeStamp); + return new UserAddReactionEvent(timeStamp, user, messageId, reaction); } @Override - public void beforeReturn(UserAddReactionEvent event) { + protected void beforeReturn(UserAddReactionEvent event) { client.getStorage().addReaction(event.getReaction()); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/user/UserClickButtonEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserClickButtonEventJacksonDeserializer.java similarity index 61% rename from src/main/java/snw/kookbc/impl/serializer/event/user/UserClickButtonEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserClickButtonEventJacksonDeserializer.java index ee83e263..ffe28065 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/user/UserClickButtonEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserClickButtonEventJacksonDeserializer.java @@ -16,42 +16,42 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.user; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; -import java.util.Objects; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.user; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.User; import snw.jkook.entity.channel.NonCategoryChannel; import snw.jkook.event.user.UserClickButtonEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; + +import java.util.Objects; -public class UserClickButtonEventDeserializer extends NormalEventDeserializer { +/** + * UserClickButtonEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class UserClickButtonEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public UserClickButtonEventDeserializer(KBCClient client) { + public UserClickButtonEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected UserClickButtonEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final String userId = getAsString(body, "user_id"); - final String targetId = getAsString(body, "target_id"); + protected UserClickButtonEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String userId = body.get("user_id").asText(); + final String targetId = body.get("target_id").asText(); final Boolean needChannel = Objects.equals(userId, targetId); final User user = client.getStorage().getUser(userId); - final String messageId = getAsString(body, "msg_id"); - final String value = getAsString(body, "value"); + final String messageId = body.get("msg_id").asText(); + final String value = body.get("value").asText(); final NonCategoryChannel channel = needChannel ? null : (NonCategoryChannel) client.getStorage().getChannel(targetId); return new UserClickButtonEvent(timeStamp, user, messageId, value, channel); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserInfoUpdateEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserInfoUpdateEventJacksonDeserializer.java new file mode 100644 index 00000000..5e1918ef --- /dev/null +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserInfoUpdateEventJacksonDeserializer.java @@ -0,0 +1,64 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.impl.serializer.event.jackson.user; + +import com.fasterxml.jackson.databind.JsonNode; +import snw.jkook.event.user.UserInfoUpdateEvent; +import snw.kookbc.impl.KBCClient; +import snw.kookbc.impl.entity.UserImpl; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; +import snw.kookbc.util.JacksonUtil; + +/** + * UserInfoUpdateEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class UserInfoUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { + + public UserInfoUpdateEventJacksonDeserializer(KBCClient client) { + super(client); + } + + @Override + protected UserInfoUpdateEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + String userId = JacksonUtil.getStringOrDefault(body, "user_id", null); + if (userId == null) { + userId = JacksonUtil.getStringOrDefault(body, "body_id", null); + } + + if (userId == null) { + throw new RuntimeException("Missing required field 'user_id' or 'body_id' in user update event"); + } + + UserImpl user = ((UserImpl) client.getStorage().getUser(userId)); + + if (JacksonUtil.hasNonNull(body, "username")) { + user.setName(JacksonUtil.getStringOrDefault(body, "username", user.getName())); + } + if (JacksonUtil.hasNonNull(body, "avatar")) { + user.setAvatarUrl(JacksonUtil.getStringOrDefault(body, "avatar", user.getAvatarUrl(false))); + } + + return new UserInfoUpdateEvent(timeStamp, user); + } +} diff --git a/src/main/java/snw/kookbc/impl/serializer/event/user/UserJoinGuildEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinGuildEventJacksonDeserializer.java similarity index 56% rename from src/main/java/snw/kookbc/impl/serializer/event/user/UserJoinGuildEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinGuildEventJacksonDeserializer.java index 72f4586a..fa76ded7 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/user/UserJoinGuildEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinGuildEventJacksonDeserializer.java @@ -16,45 +16,44 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.user; - -import static snw.kookbc.util.GsonUtil.get; -import static snw.kookbc.util.GsonUtil.getAsJsonObject; -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.user; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.User; import snw.jkook.event.user.UserJoinGuildEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; + +import static snw.kookbc.util.JacksonUtil.get; -public class UserJoinGuildEventDeserializer extends NormalEventDeserializer { +/** + * UserJoinGuildEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class UserJoinGuildEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public UserJoinGuildEventDeserializer(KBCClient client) { + public UserJoinGuildEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected UserJoinGuildEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final String realType = getAsString(getAsJsonObject(object, "extra"), "type"); + protected UserJoinGuildEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final String realType = get(get(node, "extra"), "type").asText(); User user; String guildId; if ("self_joined_guild".equals(realType)) { user = client.getCore().getUser(); - guildId = get(body, "guild_id").getAsString(); + guildId = body.get("guild_id").asText(); } else { - user = client.getStorage().getUser(get(body, "user_id").getAsString()); - guildId = get(object, "target_id").getAsString(); + user = client.getStorage().getUser(body.get("user_id").asText()); + guildId = node.get("target_id").asText(); } final Guild guild = client.getStorage().getGuild(guildId); return new UserJoinGuildEvent(timeStamp, user, guild); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/user/UserJoinVoiceChannelEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinVoiceChannelEventJacksonDeserializer.java similarity index 60% rename from src/main/java/snw/kookbc/impl/serializer/event/user/UserJoinVoiceChannelEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinVoiceChannelEventJacksonDeserializer.java index 92aa2d09..709652fe 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/user/UserJoinVoiceChannelEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinVoiceChannelEventJacksonDeserializer.java @@ -16,35 +16,34 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.user; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.user; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.User; import snw.jkook.entity.channel.VoiceChannel; import snw.jkook.event.user.UserJoinVoiceChannelEvent; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.channel.VoiceChannelImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class UserJoinVoiceChannelEventDeserializer extends NormalEventDeserializer { +/** + * UserJoinVoiceChannelEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class UserJoinVoiceChannelEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public UserJoinVoiceChannelEventDeserializer(KBCClient client) { + public UserJoinVoiceChannelEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected UserJoinVoiceChannelEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final User user = client.getStorage().getUser(getAsString(body, "user_id")); - final VoiceChannel channel = new VoiceChannelImpl(client, getAsString(body, "channel_id")); + protected UserJoinVoiceChannelEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final User user = client.getStorage().getUser(body.get("user_id").asText()); + final VoiceChannel channel = new VoiceChannelImpl(client, body.get("channel_id").asText()); return new UserJoinVoiceChannelEvent(timeStamp, user, channel); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/user/UserLeaveGuildEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveGuildEventJacksonDeserializer.java similarity index 60% rename from src/main/java/snw/kookbc/impl/serializer/event/user/UserLeaveGuildEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveGuildEventJacksonDeserializer.java index 739a81a5..4b7d78f8 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/user/UserLeaveGuildEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveGuildEventJacksonDeserializer.java @@ -16,40 +16,42 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.user; - -import static snw.kookbc.util.GsonUtil.get; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.user; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.Guild; import snw.jkook.entity.User; import snw.jkook.event.user.UserLeaveGuildEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; + +import static snw.kookbc.util.JacksonUtil.get; -public class UserLeaveGuildEventDeserializer extends NormalEventDeserializer { +/** + * UserLeaveGuildEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class UserLeaveGuildEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public UserLeaveGuildEventDeserializer(KBCClient client) { + public UserLeaveGuildEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected UserLeaveGuildEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - String realType = get(get(object, "extra").getAsJsonObject(), "type").getAsString(); + protected UserLeaveGuildEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + String realType = get(get(node, "extra"), "type").asText(); User user; String guildId; if ("self_exited_guild".equals(realType)) { user = client.getCore().getUser(); - guildId = get(body, "guild_id").getAsString(); + guildId = body.get("guild_id").asText(); } else { - user = client.getStorage().getUser(get(body, "user_id").getAsString()); - guildId = get(object, "target_id").getAsString(); + user = client.getStorage().getUser(body.get("user_id").asText()); + guildId = node.get("target_id").asText(); } Guild guild = client.getStorage().getGuild(guildId); if (guild == null) { @@ -57,5 +59,4 @@ protected UserLeaveGuildEvent deserialize(JsonObject object, Type type, JsonDese } return new UserLeaveGuildEvent(timeStamp, user, guild); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/user/UserLeaveVoiceChannelEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveVoiceChannelEventJacksonDeserializer.java similarity index 60% rename from src/main/java/snw/kookbc/impl/serializer/event/user/UserLeaveVoiceChannelEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveVoiceChannelEventJacksonDeserializer.java index 5601c1a3..d7d7b720 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/user/UserLeaveVoiceChannelEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveVoiceChannelEventJacksonDeserializer.java @@ -16,35 +16,34 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.user; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.user; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.User; import snw.jkook.entity.channel.VoiceChannel; import snw.jkook.event.user.UserLeaveVoiceChannelEvent; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.channel.VoiceChannelImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class UserLeaveVoiceChannelEventDeserializer extends NormalEventDeserializer { +/** + * UserLeaveVoiceChannelEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class UserLeaveVoiceChannelEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public UserLeaveVoiceChannelEventDeserializer(KBCClient client) { + public UserLeaveVoiceChannelEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected UserLeaveVoiceChannelEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final User user = client.getStorage().getUser(getAsString(body, "user_id")); - final VoiceChannel channel = new VoiceChannelImpl(client, getAsString(body, "channel_id")); + protected UserLeaveVoiceChannelEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final User user = client.getStorage().getUser(body.get("user_id").asText()); + final VoiceChannel channel = new VoiceChannelImpl(client, body.get("channel_id").asText()); return new UserLeaveVoiceChannelEvent(timeStamp, user, channel); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/user/UserOfflineEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOfflineEventJacksonDeserializer.java similarity index 59% rename from src/main/java/snw/kookbc/impl/serializer/event/user/UserOfflineEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOfflineEventJacksonDeserializer.java index 42b10d22..0247a409 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/user/UserOfflineEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOfflineEventJacksonDeserializer.java @@ -16,32 +16,31 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.user; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.user; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.User; import snw.jkook.event.user.UserOfflineEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class UserOfflineEventDeserializer extends NormalEventDeserializer { +/** + * UserOfflineEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class UserOfflineEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public UserOfflineEventDeserializer(KBCClient client) { + public UserOfflineEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected UserOfflineEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, long timeStamp, - JsonObject body) throws JsonParseException { - final User user = client.getStorage().getUser(getAsString(body, "user_id")); + protected UserOfflineEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final User user = client.getStorage().getUser(body.get("user_id").asText()); return new UserOfflineEvent(timeStamp, user); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/user/UserOnlineEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOnlineEventJacksonDeserializer.java similarity index 59% rename from src/main/java/snw/kookbc/impl/serializer/event/user/UserOnlineEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOnlineEventJacksonDeserializer.java index 63e1f949..372f5265 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/user/UserOnlineEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOnlineEventJacksonDeserializer.java @@ -16,32 +16,31 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.user; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.user; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.User; import snw.jkook.event.user.UserOnlineEvent; import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class UserOnlineEventDeserializer extends NormalEventDeserializer { +/** + * UserOnlineEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class UserOnlineEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public UserOnlineEventDeserializer(KBCClient client) { + public UserOnlineEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected UserOnlineEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, long timeStamp, - JsonObject body) throws JsonParseException { - final User user = client.getStorage().getUser(getAsString(body, "user_id")); + protected UserOnlineEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final User user = client.getStorage().getUser(body.get("user_id").asText()); return new UserOnlineEvent(timeStamp, user); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/user/UserRemoveReactionEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserRemoveReactionEventJacksonDeserializer.java similarity index 61% rename from src/main/java/snw/kookbc/impl/serializer/event/user/UserRemoveReactionEventDeserializer.java rename to src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserRemoveReactionEventJacksonDeserializer.java index b3551c7e..cae79772 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/user/UserRemoveReactionEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserRemoveReactionEventJacksonDeserializer.java @@ -16,38 +16,37 @@ * along with this program. If not, see . */ -package snw.kookbc.impl.serializer.event.user; - -import static snw.kookbc.util.GsonUtil.getAsJsonObject; -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; +package snw.kookbc.impl.serializer.event.jackson.user; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.CustomEmoji; import snw.jkook.entity.Reaction; import snw.jkook.entity.User; import snw.jkook.event.user.UserRemoveReactionEvent; import snw.kookbc.impl.KBCClient; import snw.kookbc.impl.entity.ReactionImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; +import snw.kookbc.impl.serializer.event.jackson.BaseJacksonEventDeserializer; -public class UserRemoveReactionEventDeserializer extends NormalEventDeserializer { +/** + * UserRemoveReactionEvent 的 Jackson 反序列化器 + * + * @since KookBC 0.33.0 + */ +public class UserRemoveReactionEventJacksonDeserializer extends BaseJacksonEventDeserializer { - public UserRemoveReactionEventDeserializer(KBCClient client) { + public UserRemoveReactionEventJacksonDeserializer(KBCClient client) { super(client); } @Override - protected UserRemoveReactionEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - final JsonObject emojiObject = getAsJsonObject(body, "emoji"); - final CustomEmoji customEmoji = client.getStorage().getEmoji(getAsString(emojiObject, "id"), emojiObject); - final User user = client.getStorage().getUser(getAsString(body, "user_id")); - final String messageId = getAsString(body, "msg_id"); + protected UserRemoveReactionEvent deserialize(JsonNode node) { + final long timeStamp = extractTimeStamp(node); + final JsonNode body = extractBody(node); + + final JsonNode emojiObject = body.get("emoji"); + final CustomEmoji customEmoji = client.getStorage().getEmoji(emojiObject.get("id").asText(), emojiObject); + final User user = client.getStorage().getUser(body.get("user_id").asText()); + final String messageId = body.get("msg_id").asText(); Reaction reaction = client.getStorage().getReaction(messageId, customEmoji, user); if (reaction != null) { client.getStorage().removeReaction(reaction); @@ -56,5 +55,4 @@ protected UserRemoveReactionEvent deserialize(JsonObject object, Type type, Json } return new UserRemoveReactionEvent(timeStamp, user, messageId, reaction); } - } diff --git a/src/main/java/snw/kookbc/impl/serializer/event/user/UserInfoUpdateEventDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/user/UserInfoUpdateEventDeserializer.java deleted file mode 100644 index 0ff6a52f..00000000 --- a/src/main/java/snw/kookbc/impl/serializer/event/user/UserInfoUpdateEventDeserializer.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package snw.kookbc.impl.serializer.event.user; - -import static snw.kookbc.util.GsonUtil.getAsString; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; - -import snw.jkook.event.user.UserInfoUpdateEvent; -import snw.kookbc.impl.KBCClient; -import snw.kookbc.impl.entity.UserImpl; -import snw.kookbc.impl.serializer.event.NormalEventDeserializer; - -public class UserInfoUpdateEventDeserializer extends NormalEventDeserializer { - - public UserInfoUpdateEventDeserializer(KBCClient client) { - super(client); - } - - @Override - protected UserInfoUpdateEvent deserialize(JsonObject object, Type type, JsonDeserializationContext ctx, - long timeStamp, JsonObject body) throws JsonParseException { - UserImpl user = ((UserImpl) client.getStorage().getUser(getAsString(body, "body_id"))); - user.setName(getAsString(body, "username")); - user.setAvatarUrl(getAsString(body, "avatar")); - return new UserInfoUpdateEvent(timeStamp, user); - } - -} diff --git a/src/main/java/snw/kookbc/impl/storage/EntityStorage.java b/src/main/java/snw/kookbc/impl/storage/EntityStorage.java index 63ff03c9..0019c455 100644 --- a/src/main/java/snw/kookbc/impl/storage/EntityStorage.java +++ b/src/main/java/snw/kookbc/impl/storage/EntityStorage.java @@ -21,7 +21,7 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.jkook.entity.*; import snw.jkook.entity.channel.Channel; import snw.jkook.message.Message; @@ -35,9 +35,12 @@ import java.util.List; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import snw.kookbc.util.VirtualThreadUtil; + public class EntityStorage { private static final int RETRY_TIMES = 1; @@ -128,7 +131,9 @@ public CustomEmoji getEmoji(String id) { return emojis.getIfPresent(id); } - public User getUser(String id, JsonObject def) { + // ===== Jackson API - 高性能版本 ===== + + public User getUser(String id, JsonNode def) { // use getIfPresent, because the def should not be wasted User result = users.getIfPresent(id); if (result == null) { @@ -140,7 +145,7 @@ public User getUser(String id, JsonObject def) { return result; } - public Guild getGuild(String id, JsonObject def) { + public Guild getGuild(String id, JsonNode def) { Guild result = guilds.getIfPresent(id); if (result == null) { result = client.getEntityBuilder().buildGuild(def); @@ -151,7 +156,7 @@ public Guild getGuild(String id, JsonObject def) { return result; } - public Channel getChannel(String id, JsonObject def) { + public Channel getChannel(String id, JsonNode def) { Channel result = channels.getIfPresent(id); if (result == null) { result = client.getEntityBuilder().buildChannel(def); @@ -162,7 +167,7 @@ public Channel getChannel(String id, JsonObject def) { return result; } - public Role getRole(Guild guild, int id, JsonObject def) { + public Role getRole(Guild guild, int id, JsonNode def) { // getRole is Nullable Role result = getRole(guild, id); if (result == null) { @@ -174,7 +179,7 @@ public Role getRole(Guild guild, int id, JsonObject def) { return result; } - public CustomEmoji getEmoji(String id, JsonObject def) { + public CustomEmoji getEmoji(String id, JsonNode def) { CustomEmoji emoji = getEmoji(id); if (emoji == null) { emoji = client.getEntityBuilder().buildEmoji(def); @@ -281,6 +286,192 @@ public void cleanUpUserPermissionOverwrite(Guild guild, User user) { .map(i -> ((ChannelImpl) i).getOverwrittenUserPermissions0()) .forEach(i -> i.removeIf(o -> o.getUser() == user)); } + + // ===== 虚拟线程异步 API ===== + + /** + * 异步获取用户 - 使用虚拟线程 + * + *

在虚拟线程中执行用户获取操作,适合需要从网络获取用户信息的场景 + * + * @param id 用户ID + * @return 异步用户对象 + */ + public CompletableFuture getUserAsync(String id) { + return CompletableFuture.supplyAsync(() -> getUser(id), VirtualThreadUtil.getCacheExecutor()); + } + + /** + * 异步获取服务器 - 使用虚拟线程 + * + *

在虚拟线程中执行服务器获取操作,适合需要从网络获取服务器信息的场景 + * + * @param id 服务器ID + * @return 异步服务器对象 + */ + public CompletableFuture getGuildAsync(String id) { + return CompletableFuture.supplyAsync(() -> getGuild(id), VirtualThreadUtil.getCacheExecutor()); + } + + /** + * 异步获取频道 - 使用虚拟线程 + * + *

在虚拟线程中执行频道获取操作,避免阻塞主线程 + * + * @param id 频道ID + * @return 异步频道对象 + */ + public CompletableFuture getChannelAsync(String id) { + return CompletableFuture.supplyAsync(() -> getChannel(id), VirtualThreadUtil.getCacheExecutor()); + } + + /** + * 异步预热缓存 - 使用虚拟线程 + * + *

并行预加载常用实体,提升后续访问性能 + * + * @param userIds 要预加载的用户ID列表 + * @param guildIds 要预加载的服务器ID列表 + * @return 异步预热结果 + */ + public CompletableFuture preloadCacheAsync(List userIds, List guildIds) { + return CompletableFuture.runAsync(() -> { + // 并行预加载用户 + List> userFutures = userIds.stream() + .map(this::getUserAsync) + .collect(Collectors.toList()); + + // 并行预加载服务器 + List> guildFutures = guildIds.stream() + .map(this::getGuildAsync) + .collect(Collectors.toList()); + + // 等待所有预加载完成 + CompletableFuture.allOf(userFutures.toArray(new CompletableFuture[0])).join(); + CompletableFuture.allOf(guildFutures.toArray(new CompletableFuture[0])).join(); + }, VirtualThreadUtil.getCacheExecutor()); + } + + /** + * 异步批量获取用户 - 使用虚拟线程 + * + *

并行获取多个用户,显著提升批量操作性能 + * + * @param userIds 用户ID列表 + * @return 异步用户列表 + */ + public CompletableFuture> batchGetUsersAsync(List userIds) { + List> futures = userIds.stream() + .map(this::getUserAsync) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())); + } + + /** + * 异步批量获取服务器 - 使用虚拟线程 + * + *

并行获取多个服务器,显著提升批量操作性能 + * + * @param guildIds 服务器ID列表 + * @return 异步服务器列表 + */ + public CompletableFuture> batchGetGuildsAsync(List guildIds) { + List> futures = guildIds.stream() + .map(this::getGuildAsync) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())); + } + + /** + * 异步缓存清理 - 使用虚拟线程 + * + *

在虚拟线程中执行缓存清理操作,避免阻塞主线程 + * + * @return 异步清理结果 + */ + public CompletableFuture cleanupCacheAsync() { + return CompletableFuture.runAsync(() -> { + users.cleanUp(); + guilds.cleanUp(); + channels.cleanUp(); + roles.cleanUp(); + emojis.cleanUp(); + }, VirtualThreadUtil.getCacheExecutor()); + } + + /** + * 异步获取缓存统计 - 使用虚拟线程 + * + *

收集所有缓存的统计信息 + * + * @return 异步缓存统计结果 + */ + public CompletableFuture getCacheStatsAsync() { + return CompletableFuture.supplyAsync(() -> { + return new CacheStats( + users.estimatedSize(), + guilds.estimatedSize(), + channels.estimatedSize(), + roles.estimatedSize(), + emojis.estimatedSize(), + users.stats(), + guilds.stats() + ); + }, VirtualThreadUtil.getCacheExecutor()); + } + + /** + * 缓存统计数据类 + */ + public static class CacheStats { + private final long userCacheSize; + private final long guildCacheSize; + private final long channelCacheSize; + private final long roleCacheSize; + private final long emojiCacheSize; + private final com.github.benmanes.caffeine.cache.stats.CacheStats userStats; + private final com.github.benmanes.caffeine.cache.stats.CacheStats guildStats; + + public CacheStats(long userCacheSize, long guildCacheSize, long channelCacheSize, + long roleCacheSize, long emojiCacheSize, + com.github.benmanes.caffeine.cache.stats.CacheStats userStats, + com.github.benmanes.caffeine.cache.stats.CacheStats guildStats) { + this.userCacheSize = userCacheSize; + this.guildCacheSize = guildCacheSize; + this.channelCacheSize = channelCacheSize; + this.roleCacheSize = roleCacheSize; + this.emojiCacheSize = emojiCacheSize; + this.userStats = userStats; + this.guildStats = guildStats; + } + + public long getUserCacheSize() { return userCacheSize; } + public long getGuildCacheSize() { return guildCacheSize; } + public long getChannelCacheSize() { return channelCacheSize; } + public long getRoleCacheSize() { return roleCacheSize; } + public long getEmojiCacheSize() { return emojiCacheSize; } + public com.github.benmanes.caffeine.cache.stats.CacheStats getUserStats() { return userStats; } + public com.github.benmanes.caffeine.cache.stats.CacheStats getGuildStats() { return guildStats; } + + @Override + public String toString() { + return String.format( + "CacheStats{users=%d, guilds=%d, channels=%d, roles=%d, emojis=%d, " + + "userHitRate=%.2f%%, guildHitRate=%.2f%%}", + userCacheSize, guildCacheSize, channelCacheSize, roleCacheSize, emojiCacheSize, + userStats.hitRate() * 100, guildStats.hitRate() * 100 + ); + } + } + } interface UncheckedFunction { diff --git a/src/main/java/snw/kookbc/impl/tasks/BotMarketPingThread.java b/src/main/java/snw/kookbc/impl/tasks/BotMarketPingThread.java index 6ea3f3bb..332f582b 100644 --- a/src/main/java/snw/kookbc/impl/tasks/BotMarketPingThread.java +++ b/src/main/java/snw/kookbc/impl/tasks/BotMarketPingThread.java @@ -18,17 +18,16 @@ package snw.kookbc.impl.tasks; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import snw.kookbc.impl.KBCClient; +import snw.kookbc.util.JacksonUtil; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; -import static snw.kookbc.util.GsonUtil.get; public final class BotMarketPingThread extends Thread { private final KBCClient client; @@ -57,7 +56,7 @@ public void run() { run0(); } catch (InterruptedException ignored) { } catch (Exception e) { - client.getCore().getLogger().error("Thread terminated because an exception occurred.", e); + client.getCore().getLogger().error("线程因发生异常而终止", e); } } @@ -73,17 +72,27 @@ public void run0() throws InterruptedException { try (Response response = networkClient.newCall(request).execute()) { if (response.body() != null) { String resStr = response.body().string(); - JsonObject object = JsonParser.parseString(resStr).getAsJsonObject(); - int status = get(object, "code").getAsInt(); + JsonNode jsonNode = JacksonUtil.parse(resStr); + + // 使用 Jackson 安全地处理响应 + JsonNode codeNode = jsonNode.get("code"); + if (codeNode == null || codeNode.isNull()) { + throw new RuntimeException("Invalid BotMarket response: missing 'code' field"); + } + + int status = codeNode.asInt(); if (status != 0) { - throw new RuntimeException(String.format("Unexpected Response Code: %s, message: %s", status, - get(object, "message").getAsString())); + JsonNode messageNode = jsonNode.get("message"); + String message = messageNode != null && !messageNode.isNull() + ? messageNode.asText() + : "Unknown error"; + throw new RuntimeException(String.format("Unexpected Response Code: %s, message: %s", status, message)); } } else { throw new RuntimeException("No response body when we attempting to PING BotMarket."); } } catch (Exception e) { - client.getCore().getLogger().error("Unable to PING BotMarket.", e); + client.getCore().getLogger().error("无法 PING BotMarket", e); continue; } client.getCore().getLogger().debug("PING BotMarket success"); diff --git a/src/main/java/snw/kookbc/impl/tasks/StopSignalListener.java b/src/main/java/snw/kookbc/impl/tasks/StopSignalListener.java index ef6ae879..0f94434f 100644 --- a/src/main/java/snw/kookbc/impl/tasks/StopSignalListener.java +++ b/src/main/java/snw/kookbc/impl/tasks/StopSignalListener.java @@ -46,7 +46,7 @@ public void run() { if (localFile.exists()) { //noinspection ResultOfMethodCallIgnored localFile.delete(); - client.getCore().getLogger().info("Received stop signal by new file. Stopping!"); + client.getCore().getLogger().info("通过新文件收到停止信号,正在停止!"); client.shutdown(); return; } diff --git a/src/main/java/snw/kookbc/impl/tasks/UpdateChecker.java b/src/main/java/snw/kookbc/impl/tasks/UpdateChecker.java index ba67167a..d7651f29 100644 --- a/src/main/java/snw/kookbc/impl/tasks/UpdateChecker.java +++ b/src/main/java/snw/kookbc/impl/tasks/UpdateChecker.java @@ -22,8 +22,7 @@ import java.util.Objects; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -31,6 +30,7 @@ import okhttp3.ResponseBody; import snw.kookbc.SharedConstants; import snw.kookbc.impl.KBCClient; +import snw.kookbc.util.JacksonUtil; public final class UpdateChecker extends Thread { private final KBCClient client; @@ -45,29 +45,50 @@ public void run() { try { run0(); } catch (Exception e) { - client.getCore().getLogger().warn("Unable to check update from remote.", e); + client.getCore().getLogger().warn("无法从远程检查更新", e); } } private void run0() throws Exception { - client.getCore().getLogger().info("Checking updates..."); + client.getCore().getLogger().info("正在检查更新..."); if (!Objects.equals(SharedConstants.REPO_URL, "https://github.com/SNWCreations/KookBC")) { client.getCore().getLogger() - .warn("Not Official KookBC! We cannot check updates for you. Is this a fork version?"); + .warn("非官方 KookBC!我们无法为您检查更新。这是一个分支版本吗?"); return; } final Request req = new Request.Builder() .get() .url("https://api.github.com/repos/SNWCreations/KookBC/releases/latest") .build(); - JsonObject resObj; + JsonNode resObj; try (Response response = new OkHttpClient().newCall(req).execute()) { final ResponseBody body = response.body(); assert body != null; - resObj = JsonParser.parseString(body.string()).getAsJsonObject(); + resObj = JacksonUtil.parse(body.string()); } - String receivedVersion = resObj.get("tag_name").getAsString(); + // 检查 GitHub API 错误响应 + JsonNode messageNode = resObj.get("message"); + if (messageNode != null && !messageNode.isNull()) { + String errorMessage = messageNode.asText(); + if (errorMessage.contains("API rate limit exceeded")) { + client.getCore().getLogger() + .warn("无法检查更新!GitHub API 请求频率限制已超出,请稍后重试"); + } else { + client.getCore().getLogger() + .warn("无法检查更新!GitHub API 返回错误: {}", errorMessage); + } + return; + } + + JsonNode tagNameNode = resObj.get("tag_name"); + if (tagNameNode == null || tagNameNode.isNull()) { + client.getCore().getLogger() + .warn("无法检查更新!GitHub API 响应缺少 'tag_name' 字段,API 格式可能已更改"); + return; + } + + String receivedVersion = tagNameNode.asText(); if (receivedVersion.startsWith("v")) { // normally I won't add "v" prefix. receivedVersion = receivedVersion.substring(1); @@ -78,36 +99,45 @@ private void run0() throws Exception { versionDifference = getVersionDifference(client.getCore().getImplementationVersion(), receivedVersion); } catch (NumberFormatException e) { client.getCore().getLogger() - .warn("Cannot check update! We can't recognize version! Custom build or snapshot API?"); + .warn("无法检查更新!版本号无法识别!自定义构建版本或快照 API?"); return; } switch (versionDifference) { case -1: { - client.getCore().getLogger().info("Update available! Information is following:"); - client.getCore().getLogger().info("New Version: {}, Currently on: {}", receivedVersion, + client.getCore().getLogger().info("发现可用更新!相关信息如下:"); + client.getCore().getLogger().info("最新版本: {},当前版本: {}", receivedVersion, client.getCore().getImplementationVersion()); - client.getCore().getLogger().info("Release Title: {}", resObj.get("name").getAsString()); - client.getCore().getLogger().info("Release Time: {}", resObj.get("published_at").getAsString()); - client.getCore().getLogger().info("Release message is following:"); - for (String body : resObj.get("body").getAsString().split("\r\n")) { + + JsonNode nameNode = resObj.get("name"); + String releaseName = nameNode != null && !nameNode.isNull() ? nameNode.asText() : "未知"; + client.getCore().getLogger().info("发布标题: {}", releaseName); + + JsonNode publishedAtNode = resObj.get("published_at"); + String publishedAt = publishedAtNode != null && !publishedAtNode.isNull() ? publishedAtNode.asText() : "未知"; + client.getCore().getLogger().info("发布时间: {}", publishedAt); + + client.getCore().getLogger().info("发布说明如下:"); + JsonNode bodyNode = resObj.get("body"); + String releaseBody = bodyNode != null && !bodyNode.isNull() ? bodyNode.asText() : "无发布说明"; + for (String body : releaseBody.split("\r\n")) { client.getCore().getLogger().info(body); } client.getCore().getLogger().info( - "You can get the new version of KookBC at: https://github.com/SNWCreations/KookBC/releases/{}", + "您可以在以下地址获取新版本的 KookBC: https://github.com/SNWCreations/KookBC/releases/{}", receivedVersion); break; } case 0: { - client.getCore().getLogger().info("You are using the latest version! :)"); + client.getCore().getLogger().info("您正在使用最新版本!:)"); break; } case 1: { - client.getCore().getLogger().info("Your KookBC is newer than the latest version from remote!"); - client.getCore().getLogger().info("Are you using development version?"); + client.getCore().getLogger().info("您的 KookBC 版本比远程最新版本还要新!"); + client.getCore().getLogger().info("您是否正在使用开发版本?"); break; } default: { - client.getCore().getLogger().info("Unable to compare the version! Internal method returns {}", + client.getCore().getLogger().info("无法比较版本!内部方法返回值: {}", versionDifference); break; } diff --git a/src/main/java/snw/kookbc/interfaces/AsyncHttpAPI.java b/src/main/java/snw/kookbc/interfaces/AsyncHttpAPI.java new file mode 100644 index 00000000..a17850f5 --- /dev/null +++ b/src/main/java/snw/kookbc/interfaces/AsyncHttpAPI.java @@ -0,0 +1,262 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.interfaces; + +import java.io.File; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import snw.jkook.HttpAPI; +import snw.jkook.entity.Guild; +import snw.jkook.entity.User; +import snw.jkook.entity.channel.Channel; + +/** + * 异步 HTTP API 接口 - 为插件提供高性能的异步 API 调用能力 + * + *

该接口提供了 KookBC HTTP API 的异步版本,利用 Java 21 虚拟线程技术 + * 实现高并发、低延迟的 API 调用。所有方法都返回 {@link CompletableFuture}, + * 支持链式调用和组合操作。 + * + *

性能优势: + *

    + *
  • 使用虚拟线程,支持大量并发请求而不阻塞系统
  • + *
  • 内置智能请求合并,避免重复的并发请求
  • + *
  • 批量操作 API,显著减少网络调用次数
  • + *
  • 异步执行,不阻塞主线程或事件处理
  • + *
+ * + *

使用示例: + *

{@code
+ * // 异步文件上传
+ * asyncHttpAPI.uploadFileAsync(file)
+ *     .thenAccept(url -> {
+ *         // 处理上传结果
+ *         logger.info("文件上传成功: {}", url);
+ *     })
+ *     .exceptionally(throwable -> {
+ *         // 处理异常
+ *         logger.error("文件上传失败", throwable);
+ *         return null;
+ *     });
+ *
+ * // 批量获取用户信息
+ * List userIds = Arrays.asList("123", "456", "789");
+ * asyncHttpAPI.getBatchUsersAsync(userIds)
+ *     .thenAccept(users -> {
+ *         // 处理批量结果
+ *         users.forEach(user ->
+ *             logger.info("用户: {}", user.getName()));
+ *     });
+ *
+ * // 组合异步操作
+ * CompletableFuture uploadFuture = asyncHttpAPI.uploadFileAsync(file);
+ * CompletableFuture userFuture = asyncHttpAPI.getUserAsync("123");
+ *
+ * CompletableFuture.allOf(uploadFuture, userFuture)
+ *     .thenRun(() -> {
+ *         String url = uploadFuture.join();
+ *         User user = userFuture.join();
+ *         // 所有操作都完成
+ *     });
+ * }
+ * + *

错误处理: + * 所有异步方法在遇到错误时会返回失败的 {@link CompletableFuture}。 + * 建议使用 {@code exceptionally()} 或 {@code handle()} 方法进行错误处理。 + * + *

线程安全: + * 该接口的所有方法都是线程安全的,可以在多线程环境中安全使用。 + * + * @since KookBC 0.33.0 + * @see HttpAPI + * @see java.util.concurrent.CompletableFuture + */ +public interface AsyncHttpAPI { + + // ===== 文件操作 ===== + + /** + * 异步上传文件 + * + *

将指定的文件异步上传到 Kook 服务器,并返回包含文件 URL 的 Future。 + * 支持的文件类型包括图片、音频、视频和其他文档。 + * + * @param file 要上传的文件,不能为 null + * @return 异步结果,包含上传后的文件 URL + * @throws IllegalArgumentException 如果文件为 null 或不存在 + */ + @NotNull + CompletableFuture uploadFileAsync(@NotNull File file); + + /** + * 异步上传文件内容 + * + *

将指定的字节数组内容异步上传到 Kook 服务器。 + * 适用于内存中的文件内容或动态生成的内容。 + * + * @param filename 文件名,用于服务器端识别文件类型 + * @param content 文件内容的字节数组 + * @return 异步结果,包含上传后的文件 URL + * @throws IllegalArgumentException 如果文件名为空或内容为 null + */ + @NotNull + CompletableFuture uploadFileAsync(@NotNull String filename, @NotNull byte[] content); + + /** + * 异步上传网络文件 + * + *

从指定 URL 下载文件并上传到 Kook 服务器。 + * 适用于转存其他平台的文件资源。 + * + * @param fileName 目标文件名 + * @param url 源文件的网络地址 + * @return 异步结果,包含上传后的文件 URL + * @throws IllegalArgumentException 如果 URL 格式错误 + */ + @NotNull + CompletableFuture uploadFileAsync(@NotNull String fileName, @NotNull String url); + + // ===== 邀请管理 ===== + + /** + * 异步删除邀请链接 + * + *

删除指定的服务器邀请链接。只有具有相应权限的 Bot 才能执行此操作。 + * + * @param urlCode 邀请链接的代码部分 + * @return 异步删除操作的结果 + * @throws IllegalArgumentException 如果邀请代码为空 + */ + @NotNull + CompletableFuture removeInviteAsync(@NotNull String urlCode); + + // ===== 实体获取 ===== + + /** + * 异步获取用户信息 + * + *

根据用户 ID 异步获取用户详细信息。 + * 如果用户在本地缓存中存在,会直接返回缓存结果。 + * + * @param id 用户 ID + * @return 异步结果,包含用户信息 + * @throws IllegalArgumentException 如果用户 ID 为空 + */ + @NotNull + CompletableFuture getUserAsync(@NotNull String id); + + /** + * 异步获取服务器信息 + * + *

根据服务器 ID 异步获取服务器详细信息。 + * 如果服务器在本地缓存中存在,会直接返回缓存结果。 + * + * @param id 服务器 ID + * @return 异步结果,包含服务器信息 + * @throws IllegalArgumentException 如果服务器 ID 为空 + */ + @NotNull + CompletableFuture getGuildAsync(@NotNull String id); + + /** + * 异步获取频道信息 + * + *

根据频道 ID 异步获取频道详细信息。 + * 如果频道在本地缓存中存在,会直接返回缓存结果。 + * + * @param id 频道 ID + * @return 异步结果,包含频道信息 + * @throws IllegalArgumentException 如果频道 ID 为空 + */ + @NotNull + CompletableFuture getChannelAsync(@NotNull String id); + + // ===== 批量操作 ===== + + /** + * 批量异步获取用户信息 + * + *

并行获取多个用户的详细信息,显著提升获取大量用户信息的性能。 + * 所有请求会并行执行,然后汇总结果。 + * + *

性能说明: + * 相比逐个调用 {@link #getUserAsync(String)},批量操作可以: + *

    + *
  • 减少网络往返次数
  • + *
  • 提高并发处理能力
  • + *
  • 降低总体响应时间
  • + *
+ * + * @param userIds 用户 ID 列表 + * @return 异步结果,包含用户信息列表(顺序与输入 ID 列表对应) + * @throws IllegalArgumentException 如果 ID 列表为 null 或包含 null 元素 + */ + @NotNull + CompletableFuture> getBatchUsersAsync(@NotNull List userIds); + + /** + * 批量异步获取服务器信息 + * + *

并行获取多个服务器的详细信息,适用于需要大量服务器信息的场景。 + * + * @param guildIds 服务器 ID 列表 + * @return 异步结果,包含服务器信息列表(顺序与输入 ID 列表对应) + * @throws IllegalArgumentException 如果 ID 列表为 null 或包含 null 元素 + */ + @NotNull + CompletableFuture> getBatchGuildsAsync(@NotNull List guildIds); + + /** + * 批量异步获取频道信息 + * + *

并行获取多个频道的详细信息,适用于需要大量频道信息的场景。 + * + * @param channelIds 频道 ID 列表 + * @return 异步结果,包含频道信息列表(顺序与输入 ID 列表对应) + * @throws IllegalArgumentException 如果 ID 列表为 null 或包含 null 元素 + */ + @NotNull + CompletableFuture> getBatchChannelsAsync(@NotNull List channelIds); + + // ===== 性能监控 ===== + + /** + * 获取当前正在进行的异步请求数量 + * + *

用于监控系统性能和调试目的。 + * 当系统负载较高时,可以通过此方法了解当前的异步请求状况。 + * + * @return 正在进行的异步请求数量 + */ + int getOngoingRequestCount(); + + /** + * 清理请求缓存 + * + *

清理内部的请求合并缓存,主要用于测试或特殊情况。 + * 在正常使用中不需要调用此方法,系统会自动管理缓存。 + * + *

注意:此操作可能会影响正在进行的请求合并。 + */ + void clearRequestCache(); +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/interfaces/Updatable.java b/src/main/java/snw/kookbc/interfaces/Updatable.java index 285056de..091bf252 100644 --- a/src/main/java/snw/kookbc/interfaces/Updatable.java +++ b/src/main/java/snw/kookbc/interfaces/Updatable.java @@ -18,14 +18,14 @@ package snw.kookbc.interfaces; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; // Represents an object which can be updated. public interface Updatable { - // Use the provided object to update the data inside this instance. + // Use the provided JsonNode to update the data inside this instance. // MUST lock the object itself to ensure the read operations during the update progress // will be blocked until the update is done. - void update(JsonObject data); + void update(JsonNode data); } diff --git a/src/main/java/snw/kookbc/interfaces/network/webhook/Request.java b/src/main/java/snw/kookbc/interfaces/network/webhook/Request.java index 7b49f286..3dad4159 100644 --- a/src/main/java/snw/kookbc/interfaces/network/webhook/Request.java +++ b/src/main/java/snw/kookbc/interfaces/network/webhook/Request.java @@ -28,8 +28,6 @@ public interface Request { // To parsed Java JSON Object. // No actual type for T, because it is depend on the dependency of implementation. - // e.g. - // com.google.gson.JsonObject json = request.toJson(); // it should be OK T toJson(); // Only for implementation use. diff --git a/src/main/java/snw/kookbc/interfaces/network/webhook/WebhookServer.java b/src/main/java/snw/kookbc/interfaces/network/webhook/WebhookServer.java index 80afecf2..7097d258 100644 --- a/src/main/java/snw/kookbc/interfaces/network/webhook/WebhookServer.java +++ b/src/main/java/snw/kookbc/interfaces/network/webhook/WebhookServer.java @@ -18,7 +18,7 @@ package snw.kookbc.interfaces.network.webhook; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import snw.kookbc.interfaces.Lifecycle; public interface WebhookServer extends Lifecycle { @@ -26,7 +26,7 @@ public interface WebhookServer extends Lifecycle { // Should only be called once during its lifecycle. // So its implementations should be protected. // Only for implementation use. - void setHandler(RequestHandler handler); + void setHandler(RequestHandler handler); // Set the endpoint of the Webhook handler, should be called BEFORE THE SERVER STARTS. void setEndpoint(String path); diff --git a/src/main/java/snw/kookbc/test/JacksonCardSerializationTest.java b/src/main/java/snw/kookbc/test/JacksonCardSerializationTest.java new file mode 100644 index 00000000..9fe041ac --- /dev/null +++ b/src/main/java/snw/kookbc/test/JacksonCardSerializationTest.java @@ -0,0 +1,78 @@ +package snw.kookbc.test; + +import snw.jkook.message.component.card.CardBuilder; +import snw.jkook.message.component.card.Size; +import snw.jkook.message.component.card.Theme; +import snw.jkook.message.component.card.element.MarkdownElement; +import snw.jkook.message.component.card.element.PlainTextElement; +import snw.jkook.message.component.card.module.ContextModule; +import snw.jkook.message.component.card.module.DividerModule; +import snw.jkook.message.component.card.module.HeaderModule; +import snw.jkook.message.component.card.module.SectionModule; +import snw.kookbc.util.JacksonCardUtil; +import snw.kookbc.impl.entity.builder.MessageBuilder; + +import java.util.Collections; + +/** + * 测试Jackson卡片序列化功能 + */ +public class JacksonCardSerializationTest { + + public static void main(String[] args) { + try { + System.out.println("=== Jackson卡片序列化测试 ==="); + + // 测试DividerModule序列化 + DividerModule divider = DividerModule.INSTANCE; + String dividerJson = JacksonCardUtil.toJson(divider); + System.out.println("✅ DividerModule序列化成功: " + dividerJson); + + // 测试HeaderModule序列化 + HeaderModule header = new HeaderModule(new PlainTextElement("测试标题")); + String headerJson = JacksonCardUtil.toJson(header); + System.out.println("✅ HeaderModule序列化成功: " + headerJson); + + // 测试SectionModule序列化 + SectionModule section = new SectionModule(new MarkdownElement("**测试内容**")); + String sectionJson = JacksonCardUtil.toJson(section); + System.out.println("✅ SectionModule序列化成功: " + sectionJson); + + // 测试ContextModule序列化 + ContextModule context = new ContextModule(Collections.singletonList(new MarkdownElement("测试上下文"))); + String contextJson = JacksonCardUtil.toJson(context); + System.out.println("✅ ContextModule序列化成功: " + contextJson); + + // 测试完整的CardComponent序列化(模拟Help命令结构) + var card = new CardBuilder() + .setTheme(Theme.SUCCESS) + .setSize(Size.LG) + .addModule(new HeaderModule(new PlainTextElement("命令帮助 (1/1)"))) + .addModule(DividerModule.INSTANCE) + .addModule(new SectionModule(new MarkdownElement("(/)**plugins**: 获取已安装到此 KookBC 实例的插件列表。"))) + .addModule(new SectionModule(new MarkdownElement("(/)**help**: 此命令没有简介。"))) + .addModule(DividerModule.INSTANCE) + .addModule(new ContextModule(Collections.singletonList( + new MarkdownElement("由 [KookBC](https://github.com/SNWCreations/KookBC) v0.33.0 驱动 - JKook API 0.54.1") + ))) + .build(); + + String cardJson = JacksonCardUtil.toJson(card); + System.out.println("✅ 完整Help命令卡片序列化成功:"); + System.out.println(" " + cardJson); + + // 现在测试消息构建器的序列化 + System.out.println("\n=== 测试MessageBuilder序列化 ==="); + Object[] result = MessageBuilder.serialize(card); + System.out.println("✅ MessageBuilder.serialize结果:"); + System.out.println(" 类型: " + result[0]); + System.out.println(" JSON: " + result[1]); + + System.out.println("\n🎉 所有测试通过!Jackson卡片序列化修复成功!"); + + } catch (Exception e) { + System.err.println("❌ 测试失败:"); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/test/JacksonCardTest.java b/src/main/java/snw/kookbc/test/JacksonCardTest.java new file mode 100644 index 00000000..b6562ef6 --- /dev/null +++ b/src/main/java/snw/kookbc/test/JacksonCardTest.java @@ -0,0 +1,107 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package snw.kookbc.test; + +import snw.jkook.message.component.card.CardComponent; +import snw.kookbc.impl.entity.builder.CardBuilder; +import snw.kookbc.util.JacksonCardUtil; + +/** + * Jackson卡片系统测试工具 + */ +public class JacksonCardTest { + + /** + * 测试复杂卡片JSON的反序列化 + * 模拟用户报告的错误场景 + */ + public static void testComplexCardDeserialization() { + // 这是用户提供的复杂卡片JSON + String complexCardJson = "[{\"theme\":\"info\",\"color\":\"\",\"size\":\"lg\",\"expand\":false,\"modules\":[{\"type\":\"section\",\"mode\":\"right\",\"accessory\":{\"type\":\"button\",\"theme\":\"secondary\",\"value\":\"{\\n \\\"action\\\": \\\"播放卡片按钮\\\",\\n \\\"voiceChannelID\\\": \\\"8418843659211643\\\",\\n \\\"event\\\": \\\"歌曲列表\\\"\\n}\",\"click\":\"return-val\",\"text\":{\"type\":\"kmarkdown\",\"content\":\"142 \\/ 326\",\"elements\":[]},\"external\":true,\"elements\":[]},\"text\":{\"type\":\"kmarkdown\",\"content\":\"**[**⛏minecraft高手⛏**]**\\t\\t| 正在为你播放 😊 \",\"elements\":[]},\"elements\":[]},{\"type\":\"section\",\"mode\":\"left\",\"accessory\":{\"type\":\"image\",\"src\":\"https:\\/\\/img.kookapp.cn\\/attachments\\/2025-09\\/26\\/VbW1qWRpB814z14z.jpeg\",\"alt\":\"\",\"size\":\"sm\",\"circle\":false,\"title\":\"\",\"fallbackUrl\":\"\",\"elements\":[]},\"text\":{\"type\":\"plain-text\",\"emoji\":true,\"content\":\" The des Alizes - Foxtail-Grass Studio\",\"elements\":[]},\"elements\":[]},{\"type\":\"context\",\"elements\":[{\"type\":\"plain-text\",\"emoji\":true,\"content\":\"音源: \",\"elements\":[]},{\"type\":\"image\",\"src\":\"https:\\/\\/img.kookapp.cn\\/assets\\/2023-05\\/hULgrDPVq200w00w.png\",\"alt\":\"\",\"size\":\"sm\",\"circle\":true,\"title\":\"\",\"fallbackUrl\":\"\",\"elements\":[]},{\"type\":\"plain-text\",\"emoji\":true,\"content\":\" | 模式: 随机播放\",\"elements\":[]},{\"type\":\"plain-text\",\"emoji\":true,\"content\":\" | 音量: 0.5\",\"elements\":[]},{\"type\":\"kmarkdown\",\"content\":\" | 如果有问题欢迎加入-> [官方服务器](https:\\/\\/kook.top\\/JOHwp4) \",\"elements\":[]}]},{\"type\":\"action-group\",\"elements\":[{\"type\":\"button\",\"theme\":\"primary\",\"value\":\"{\\n \\\"action\\\": \\\"播放卡片按钮\\\",\\n \\\"voiceChannelID\\\": \\\"8418843659211643\\\",\\n \\\"event\\\": \\\"上一首歌\\\"\\n}\",\"click\":\"return-val\",\"text\":{\"type\":\"plain-text\",\"emoji\":true,\"content\":\"上一首歌\",\"elements\":[]},\"external\":true,\"elements\":[]},{\"type\":\"button\",\"theme\":\"danger\",\"value\":\"{\\n \\\"action\\\": \\\"播放卡片按钮\\\",\\n \\\"voiceChannelID\\\": \\\"8418843659211643\\\",\\n \\\"event\\\": \\\"暂停播放\\\"\\n}\",\"click\":\"return-val\",\"text\":{\"type\":\"plain-text\",\"emoji\":true,\"content\":\"暂停播放\",\"elements\":[]},\"external\":true,\"elements\":[]},{\"type\":\"button\",\"theme\":\"primary\",\"value\":\"{\\n \\\"action\\\": \\\"播放卡片按钮\\\",\\n \\\"voiceChannelID\\\": \\\"8418843659211643\\\",\\n \\\"event\\\": \\\"下一首歌\\\"\\n}\",\"click\":\"return-val\",\"text\":{\"type\":\"plain-text\",\"emoji\":true,\"content\":\"下一首歌\",\"elements\":[]},\"external\":true,\"elements\":[]},{\"type\":\"button\",\"theme\":\"secondary\",\"value\":\"{\\n \\\"action\\\": \\\"播放卡片按钮\\\",\\n \\\"voiceChannelID\\\": \\\"8418843659211643\\\",\\n \\\"event\\\": \\\"切换模式\\\"\\n}\",\"click\":\"return-val\",\"text\":{\"type\":\"plain-text\",\"emoji\":true,\"content\":\"切换模式\",\"elements\":[]},\"external\":true,\"elements\":[]}]}],\"type\":\"card\"}]"; + + try { + System.out.println("=== Jackson卡片系统测试 ==="); + System.out.println("开始测试复杂卡片JSON反序列化..."); + + // 使用Jackson解析 + Object result = CardBuilder.buildCard(complexCardJson); + + if (result != null) { + System.out.println("✅ Jackson反序列化成功!"); + System.out.println("结果类型: " + result.getClass().getSimpleName()); + + // 测试序列化回JSON + String serializedJson; + if (result instanceof CardComponent) { + serializedJson = JacksonCardUtil.toJson(result); + } else { + serializedJson = JacksonCardUtil.toJson(result); + } + + System.out.println("✅ Jackson序列化成功!"); + System.out.println("序列化JSON长度: " + serializedJson.length()); + + // 验证往返转换 + Object roundTrip = CardBuilder.buildCard(serializedJson); + if (roundTrip != null) { + System.out.println("✅ JSON往返转换成功!"); + } else { + System.out.println("❌ JSON往返转换失败"); + } + + } else { + System.out.println("❌ Jackson反序列化返回null"); + } + + } catch (Exception e) { + System.out.println("❌ Jackson测试失败: " + e.getClass().getSimpleName()); + System.out.println("错误信息: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 测试缺失字段处理 + */ + public static void testMissingFieldHandling() { + System.out.println("\n=== 缺失字段处理测试 ==="); + + // 测试缺少可选字段的卡片 + String incompleteCardJson = "[{\"type\":\"card\",\"size\":\"lg\",\"modules\":[{\"type\":\"section\",\"text\":{\"type\":\"plain-text\",\"content\":\"简单文本\"}}]}]"; + + try { + Object result = CardBuilder.buildCard(incompleteCardJson); + if (result != null) { + System.out.println("✅ 缺失字段处理成功!"); + } else { + System.out.println("❌ 缺失字段处理失败"); + } + } catch (Exception e) { + System.out.println("❌ 缺失字段处理异常: " + e.getMessage()); + } + } + + /** + * 主测试方法 + */ + public static void main(String[] args) { + testComplexCardDeserialization(); + testMissingFieldHandling(); + System.out.println("\n=== Jackson卡片系统测试完成 ==="); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/util/AsyncFileUtil.java b/src/main/java/snw/kookbc/util/AsyncFileUtil.java new file mode 100644 index 00000000..5d38aa03 --- /dev/null +++ b/src/main/java/snw/kookbc/util/AsyncFileUtil.java @@ -0,0 +1,313 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * 异步文件操作工具类 - 基于虚拟线程的高性能文件 I/O + * + *

使用虚拟线程处理文件 I/O 操作,避免阻塞主线程, + * 特别适合处理大量文件操作或大文件操作的场景。 + * + * @since Java 21 + */ +public final class AsyncFileUtil { + + private AsyncFileUtil() { + // 工具类,禁止实例化 + } + + // ===== 异步文件读取 ===== + + /** + * 异步读取文件全部内容为字符串 + * + * @param path 文件路径 + * @return 异步文件内容 + */ + public static CompletableFuture readStringAsync(Path path) { + return CompletableFuture.supplyAsync(() -> { + try { + return Files.readString(path); + } catch (IOException e) { + throw new RuntimeException("读取文件失败: " + path, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + /** + * 异步读取文件所有行 + * + * @param path 文件路径 + * @return 异步文件行列表 + */ + public static CompletableFuture> readAllLinesAsync(Path path) { + return CompletableFuture.supplyAsync(() -> { + try { + return Files.readAllLines(path); + } catch (IOException e) { + throw new RuntimeException("读取文件行失败: " + path, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + /** + * 异步读取文件全部字节 + * + * @param path 文件路径 + * @return 异步文件字节数组 + */ + public static CompletableFuture readAllBytesAsync(Path path) { + return CompletableFuture.supplyAsync(() -> { + try { + return Files.readAllBytes(path); + } catch (IOException e) { + throw new RuntimeException("读取文件字节失败: " + path, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + // ===== 异步文件写入 ===== + + /** + * 异步写入字符串到文件 + * + * @param path 文件路径 + * @param content 文件内容 + * @return 异步写入结果 + */ + public static CompletableFuture writeStringAsync(Path path, String content) { + return CompletableFuture.runAsync(() -> { + try { + Files.writeString(path, content); + } catch (IOException e) { + throw new RuntimeException("写入文件失败: " + path, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + /** + * 异步追加字符串到文件 + * + * @param path 文件路径 + * @param content 要追加的内容 + * @return 异步写入结果 + */ + public static CompletableFuture appendStringAsync(Path path, String content) { + return CompletableFuture.runAsync(() -> { + try { + Files.writeString(path, content, StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } catch (IOException e) { + throw new RuntimeException("追加文件失败: " + path, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + /** + * 异步写入行列表到文件 + * + * @param path 文件路径 + * @param lines 行列表 + * @return 异步写入结果 + */ + public static CompletableFuture writeAllLinesAsync(Path path, List lines) { + return CompletableFuture.runAsync(() -> { + try { + Files.write(path, lines); + } catch (IOException e) { + throw new RuntimeException("写入文件行失败: " + path, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + /** + * 异步写入字节数组到文件 + * + * @param path 文件路径 + * @param bytes 字节数组 + * @return 异步写入结果 + */ + public static CompletableFuture writeAllBytesAsync(Path path, byte[] bytes) { + return CompletableFuture.runAsync(() -> { + try { + Files.write(path, bytes); + } catch (IOException e) { + throw new RuntimeException("写入文件字节失败: " + path, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + // ===== 异步文件操作 ===== + + /** + * 异步检查文件是否存在 + * + * @param path 文件路径 + * @return 异步结果 + */ + public static CompletableFuture existsAsync(Path path) { + return CompletableFuture.supplyAsync(() -> Files.exists(path), VirtualThreadUtil.getFileIoExecutor()); + } + + /** + * 异步创建目录 + * + * @param path 目录路径 + * @return 异步创建结果 + */ + public static CompletableFuture createDirectoriesAsync(Path path) { + return CompletableFuture.supplyAsync(() -> { + try { + return Files.createDirectories(path); + } catch (IOException e) { + throw new RuntimeException("创建目录失败: " + path, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + /** + * 异步删除文件 + * + * @param path 文件路径 + * @return 异步删除结果 + */ + public static CompletableFuture deleteAsync(Path path) { + return CompletableFuture.runAsync(() -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw new RuntimeException("删除文件失败: " + path, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + /** + * 异步复制文件 + * + * @param source 源文件路径 + * @param target 目标文件路径 + * @return 异步复制结果 + */ + public static CompletableFuture copyAsync(Path source, Path target) { + return CompletableFuture.supplyAsync(() -> { + try { + return Files.copy(source, target); + } catch (IOException e) { + throw new RuntimeException("复制文件失败: " + source + " -> " + target, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + /** + * 异步移动文件 + * + * @param source 源文件路径 + * @param target 目标文件路径 + * @return 异步移动结果 + */ + public static CompletableFuture moveAsync(Path source, Path target) { + return CompletableFuture.supplyAsync(() -> { + try { + return Files.move(source, target); + } catch (IOException e) { + throw new RuntimeException("移动文件失败: " + source + " -> " + target, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } + + // ===== 批量文件操作 ===== + + /** + * 批量异步读取多个文件 + * + * @param paths 文件路径列表 + * @return 异步结果列表 + */ + public static CompletableFuture> batchReadStringAsync(List paths) { + List> futures = paths.stream() + .map(AsyncFileUtil::readStringAsync) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())); + } + + /** + * 批量异步写入多个文件 + * + * @param pathContentMap 路径和内容的映射 + * @return 异步写入结果 + */ + public static CompletableFuture batchWriteStringAsync(java.util.Map pathContentMap) { + List> futures = pathContentMap.entrySet().stream() + .map(entry -> writeStringAsync(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } + + // ===== 高级功能 ===== + + /** + * 异步备份文件(复制到 .backup 后缀) + * + * @param path 原文件路径 + * @return 异步备份结果 + */ + public static CompletableFuture backupAsync(Path path) { + Path backupPath = Path.of(path.toString() + ".backup"); + return copyAsync(path, backupPath); + } + + /** + * 异步安全写入文件(先写临时文件,再原子性替换) + * + * @param path 目标文件路径 + * @param content 文件内容 + * @return 异步写入结果 + */ + public static CompletableFuture safeWriteStringAsync(Path path, String content) { + return CompletableFuture.runAsync(() -> { + Path tempPath = Path.of(path.toString() + ".tmp"); + try { + // 写入临时文件 + Files.writeString(tempPath, content); + // 原子性替换 + Files.move(tempPath, path); + } catch (IOException e) { + // 清理临时文件 + try { + Files.deleteIfExists(tempPath); + } catch (IOException cleanupEx) { + e.addSuppressed(cleanupEx); + } + throw new RuntimeException("安全写入文件失败: " + path, e); + } + }, VirtualThreadUtil.getFileIoExecutor()); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/util/GsonUtil.java b/src/main/java/snw/kookbc/util/GsonUtil.java deleted file mode 100644 index a6e31fae..00000000 --- a/src/main/java/snw/kookbc/util/GsonUtil.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. - * Copyright (C) 2022 - 2023 KookBC contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package snw.kookbc.util; - -import com.google.gson.*; -import com.google.gson.reflect.TypeToken; -import snw.jkook.message.component.TemplateMessage; -import snw.jkook.message.component.card.CardComponent; -import snw.jkook.message.component.card.MultipleCardComponent; -import snw.jkook.message.component.card.element.ButtonElement; -import snw.jkook.message.component.card.element.ImageElement; -import snw.jkook.message.component.card.element.MarkdownElement; -import snw.jkook.message.component.card.element.PlainTextElement; -import snw.jkook.message.component.card.module.*; -import snw.jkook.message.component.card.structure.Paragraph; -import snw.jkook.util.Validate; -import snw.kookbc.impl.serializer.component.TemplateMessageSerializer; -import snw.kookbc.impl.serializer.component.card.CardComponentSerializer; -import snw.kookbc.impl.serializer.component.card.MultipleCardComponentSerializer; -import snw.kookbc.impl.serializer.component.card.element.ButtonElementSerializer; -import snw.kookbc.impl.serializer.component.card.element.ContentElementSerializer; -import snw.kookbc.impl.serializer.component.card.element.ImageElementSerializer; -import snw.kookbc.impl.serializer.component.card.module.*; -import snw.kookbc.impl.serializer.component.card.structure.ParagraphSerializer; - -import java.lang.reflect.Type; -import java.util.List; -import java.util.NoSuchElementException; - -public final class GsonUtil { - public static final Gson CARD_GSON = new GsonBuilder() - // Template - .registerTypeAdapter(TemplateMessage.class, new TemplateMessageSerializer()) - // Card - .registerTypeAdapter(CardComponent.class, new CardComponentSerializer()) - .registerTypeAdapter(MultipleCardComponent.class, new MultipleCardComponentSerializer()) - - // Element - .registerTypeAdapter(ButtonElement.class, new ButtonElementSerializer()) - .registerTypeAdapter(ImageElement.class, new ImageElementSerializer()) - .registerTypeAdapter(MarkdownElement.class, - new ContentElementSerializer<>("kmarkdown", MarkdownElement::getContent, MarkdownElement::new)) - .registerTypeAdapter(PlainTextElement.class, - new ContentElementSerializer<>("plain-text", PlainTextElement::getContent, PlainTextElement::new)) - - // Structure - .registerTypeAdapter(Paragraph.class, new ParagraphSerializer()) - - // Module - .registerTypeAdapter(ActionGroupModule.class, new ActionGroupModuleSerializer()) - .registerTypeAdapter(ContainerModule.class, new ContainerModuleSerializer()) - .registerTypeAdapter(ContextModule.class, new ContextModuleSerializer()) - .registerTypeAdapter(CountdownModule.class, new CountdownModuleSerializer()) - .registerTypeAdapter(DividerModule.class, new DividerModuleSerializer()) - .registerTypeAdapter(FileModule.class, new FileModuleSerializer()) - .registerTypeAdapter(HeaderModule.class, new HeaderModuleSerializer()) - .registerTypeAdapter(ImageGroupModule.class, new ImageGroupModuleSerializer()) - .registerTypeAdapter(InviteModule.class, new InviteModuleSerializer()) - .registerTypeAdapter(SectionModule.class, new SectionModuleSerializer()) - - .disableHtmlEscaping() - .create(); - - public static final Gson NORMAL_GSON = new Gson(); - - public static Type createListType(Class elementType) { - Validate.notNull(elementType); - return TypeToken.getParameterized(List.class, elementType).getType(); - } - - // Return false if the provided object does not contain the specified key, - // or the value mapped to it is JSON null. - public static boolean has(JsonObject object, String key) { - return object.has(key) && !object.get(key).isJsonNull(); - } - - // Return the element object from the provided object using the key. - public static JsonElement get(JsonObject object, String key) { - JsonElement result = null; - if (object.has(key)) { - result = object.get(key); - if (result.isJsonNull()) { - result = null; // DO NOT RETURN JSON NULL. - } - } - if (result == null) { - throw new NoSuchElementException("There is no valid value mapped to requested key '" + key + "'."); - } - return result; - } - - public static String getAsString(JsonObject object, String key) { - return get(object, key).getAsString(); - } - - public static int getAsInt(JsonObject object, String key) { - return get(object, key).getAsInt(); - } - - public static long getAsLong(JsonObject object, String key) { - return get(object, key).getAsLong(); - } - - public static double getAsDouble(JsonObject object, String key) { - return get(object, key).getAsDouble(); - } - - public static boolean getAsBoolean(JsonObject object, String key) { - return get(object, key).getAsBoolean(); - } - - public static JsonObject getAsJsonObject(JsonObject object, String key) { - return get(object, key).getAsJsonObject(); - } - - public static JsonArray getAsJsonArray(JsonObject object, String key) { - return get(object, key).getAsJsonArray(); - } - - public static JsonPrimitive getAsJsonPrimitive(JsonObject object, String key) { - return get(object, key).getAsJsonPrimitive(); - } - - private GsonUtil() { - } -} diff --git a/src/main/java/snw/kookbc/util/JacksonCardUtil.java b/src/main/java/snw/kookbc/util/JacksonCardUtil.java new file mode 100644 index 00000000..3f46c921 --- /dev/null +++ b/src/main/java/snw/kookbc/util/JacksonCardUtil.java @@ -0,0 +1,225 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package snw.kookbc.util; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import snw.jkook.message.component.TemplateMessage; +import snw.jkook.message.component.card.CardComponent; +import snw.jkook.message.component.card.MultipleCardComponent; +import snw.jkook.message.component.card.element.BaseElement; +import snw.jkook.message.component.card.element.ButtonElement; +import snw.jkook.message.component.card.element.ImageElement; +import snw.jkook.message.component.card.element.MarkdownElement; +import snw.jkook.message.component.card.element.PlainTextElement; +import snw.jkook.message.component.card.module.BaseModule; +import snw.jkook.message.component.card.module.ActionGroupModule; +import snw.jkook.message.component.card.module.ContainerModule; +import snw.jkook.message.component.card.module.ContextModule; +import snw.jkook.message.component.card.module.CountdownModule; +import snw.jkook.message.component.card.module.DividerModule; +import snw.jkook.message.component.card.module.FileModule; +import snw.jkook.message.component.card.module.HeaderModule; +import snw.jkook.message.component.card.module.ImageGroupModule; +import snw.jkook.message.component.card.module.InviteModule; +import snw.jkook.message.component.card.module.SectionModule; +import snw.jkook.message.component.card.structure.Paragraph; +import snw.kookbc.impl.serializer.component.jackson.JacksonTemplateMessageDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.JacksonCardComponentDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.JacksonMultipleCardComponentDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.element.JacksonBaseElementDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.element.JacksonButtonElementDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.element.JacksonButtonElementSerializer; +import snw.kookbc.impl.serializer.component.jackson.card.element.JacksonImageElementDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.element.JacksonContentElementDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.element.JacksonPlainTextElementSerializer; +import snw.kookbc.impl.serializer.component.jackson.card.element.JacksonMarkdownElementSerializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonBaseModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonActionGroupModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonActionGroupModuleSerializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonContainerModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonContextModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonContextModuleSerializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonCountdownModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonDividerModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonDividerModuleSerializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonFileModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonHeaderModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonHeaderModuleSerializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonImageGroupModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonInviteModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonSectionModuleDeserializer; +import snw.kookbc.impl.serializer.component.jackson.card.module.JacksonSectionModuleSerializer; +import snw.kookbc.impl.serializer.component.jackson.card.structure.JacksonParagraphDeserializer; + +/** + * Jackson卡片消息处理工具类 + * 提供高性能、null-safe的卡片消息序列化/反序列化功能 + */ +public final class JacksonCardUtil { + + private static final ObjectMapper CARD_MAPPER; + + static { + CARD_MAPPER = new ObjectMapper(); + + // 配置JSON处理选项 + CARD_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + CARD_MAPPER.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false); + // 关键修复:允许序列化空Bean对象(如DividerModule) + CARD_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + CARD_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + // 创建自定义序列化器模块 + SimpleModule cardModule = new SimpleModule("KookCardModule"); + + // 注册顶层组件序列化器 + cardModule.addDeserializer(TemplateMessage.class, new JacksonTemplateMessageDeserializer()); + cardModule.addDeserializer(CardComponent.class, new JacksonCardComponentDeserializer()); + cardModule.addSerializer(CardComponent.class, new JacksonCardComponentDeserializer.CardComponentSerializer()); + cardModule.addDeserializer(MultipleCardComponent.class, new JacksonMultipleCardComponentDeserializer()); + cardModule.addSerializer(MultipleCardComponent.class, new JacksonMultipleCardComponentDeserializer.MultipleCardComponentSerializer()); + + // 注册元素序列化器(多态处理) + cardModule.addDeserializer(BaseElement.class, new JacksonBaseElementDeserializer()); + cardModule.addDeserializer(ButtonElement.class, new JacksonButtonElementDeserializer()); + cardModule.addSerializer(ButtonElement.class, new JacksonButtonElementSerializer()); + cardModule.addDeserializer(ImageElement.class, new JacksonImageElementDeserializer()); + cardModule.addSerializer(ImageElement.class, new JacksonImageElementDeserializer.ImageElementSerializer()); + cardModule.addDeserializer(MarkdownElement.class, new JacksonContentElementDeserializer<>(MarkdownElement::new)); + cardModule.addSerializer(MarkdownElement.class, new JacksonMarkdownElementSerializer()); + cardModule.addDeserializer(PlainTextElement.class, new JacksonContentElementDeserializer<>(PlainTextElement::new)); + cardModule.addSerializer(PlainTextElement.class, new JacksonPlainTextElementSerializer()); + + // 注册模块序列化器(多态处理) + cardModule.addDeserializer(BaseModule.class, new JacksonBaseModuleDeserializer()); + cardModule.addDeserializer(ActionGroupModule.class, new JacksonActionGroupModuleDeserializer()); + cardModule.addSerializer(ActionGroupModule.class, new JacksonActionGroupModuleSerializer()); + cardModule.addDeserializer(ContainerModule.class, new JacksonContainerModuleDeserializer()); + cardModule.addDeserializer(ContextModule.class, new JacksonContextModuleDeserializer()); + cardModule.addSerializer(ContextModule.class, new JacksonContextModuleSerializer()); + cardModule.addDeserializer(CountdownModule.class, new JacksonCountdownModuleDeserializer()); + cardModule.addDeserializer(DividerModule.class, new JacksonDividerModuleDeserializer()); + // 注册DividerModule的Jackson序列化器 + cardModule.addSerializer(DividerModule.class, new JacksonDividerModuleSerializer()); + cardModule.addDeserializer(FileModule.class, new JacksonFileModuleDeserializer()); + cardModule.addDeserializer(HeaderModule.class, new JacksonHeaderModuleDeserializer()); + cardModule.addSerializer(HeaderModule.class, new JacksonHeaderModuleSerializer()); + cardModule.addDeserializer(ImageGroupModule.class, new JacksonImageGroupModuleDeserializer()); + cardModule.addDeserializer(InviteModule.class, new JacksonInviteModuleDeserializer()); + cardModule.addDeserializer(SectionModule.class, new JacksonSectionModuleDeserializer()); + cardModule.addSerializer(SectionModule.class, new JacksonSectionModuleSerializer()); + + // 注册结构序列化器 + cardModule.addDeserializer(Paragraph.class, new JacksonParagraphDeserializer()); + + CARD_MAPPER.registerModule(cardModule); + } + + private JacksonCardUtil() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + // ===== 卡片消息专用序列化方法 ===== + + public static T fromJson(String json, Class classOfT) { + try { + return CARD_MAPPER.readValue(json, classOfT); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to deserialize card JSON to " + classOfT.getName(), e); + } + } + + public static T fromJson(JsonNode node, Class classOfT) { + try { + return CARD_MAPPER.treeToValue(node, classOfT); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to deserialize card JsonNode to " + classOfT.getName(), e); + } + } + + public static String toJson(Object obj) { + try { + return CARD_MAPPER.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize card object to JSON", e); + } + } + + public static JsonNode toJsonNode(Object obj) { + return CARD_MAPPER.valueToTree(obj); + } + + public static JsonNode parse(String json) { + try { + return CARD_MAPPER.readTree(json); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse card JSON: " + json, e); + } + } + + // ===== null-safe字段访问方法 ===== + + public static boolean has(JsonNode node, String fieldName) { + JsonNode field = node.get(fieldName); + return field != null && !field.isNull(); + } + + public static String getRequiredString(JsonNode node, String fieldName) { + JsonNode field = node.get(fieldName); + if (field == null || field.isNull()) { + throw new IllegalArgumentException("Required field '" + fieldName + "' is missing or null"); + } + return field.asText(); + } + + public static String getStringOrDefault(JsonNode node, String fieldName, String defaultValue) { + JsonNode field = node.get(fieldName); + return (field != null && !field.isNull()) ? field.asText() : defaultValue; + } + + public static int getIntOrDefault(JsonNode node, String fieldName, int defaultValue) { + JsonNode field = node.get(fieldName); + return (field != null && !field.isNull()) ? field.asInt() : defaultValue; + } + + public static long getLongOrDefault(JsonNode node, String fieldName, long defaultValue) { + JsonNode field = node.get(fieldName); + return (field != null && !field.isNull()) ? field.asLong() : defaultValue; + } + + public static boolean getBooleanOrDefault(JsonNode node, String fieldName, boolean defaultValue) { + JsonNode field = node.get(fieldName); + return (field != null && !field.isNull()) ? field.asBoolean() : defaultValue; + } + + public static double getDoubleOrDefault(JsonNode node, String fieldName, double defaultValue) { + JsonNode field = node.get(fieldName); + return (field != null && !field.isNull()) ? field.asDouble() : defaultValue; + } + + // 获取配置好的ObjectMapper实例 + public static ObjectMapper getMapper() { + return CARD_MAPPER; + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/util/JacksonUtil.java b/src/main/java/snw/kookbc/util/JacksonUtil.java new file mode 100644 index 00000000..ab9723d5 --- /dev/null +++ b/src/main/java/snw/kookbc/util/JacksonUtil.java @@ -0,0 +1,301 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package snw.kookbc.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import snw.jkook.util.Validate; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Jackson JSON工具类 - 高性能JSON处理 + */ +public final class JacksonUtil { + + // Jackson核心对象 + private static final ObjectMapper MAPPER; + + static { + MAPPER = new ObjectMapper(); + MAPPER.registerModule(new JavaTimeModule()); + // 配置Jackson以处理缺失字段和null值 + MAPPER.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + MAPPER.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false); + } + + private JacksonUtil() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + // ===== 基础JSON操作 ===== + + public static JsonNode parse(String json) { + try { + return MAPPER.readTree(json); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse JSON: " + json, e); + } + } + + public static String toJson(Object obj) { + try { + return MAPPER.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize object to JSON", e); + } + } + + public static T fromJson(String json, Class classOfT) { + try { + return MAPPER.readValue(json, classOfT); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to deserialize JSON to " + classOfT.getName(), e); + } + } + + public static T fromJson(String json, TypeReference typeRef) { + try { + return MAPPER.readValue(json, typeRef); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to deserialize JSON", e); + } + } + + // ===== 节点访问方法 ===== + + public static JsonNode get(JsonNode node, String fieldName) { + Validate.notNull(node, "JsonNode cannot be null"); + JsonNode result = node.get(fieldName); + if (result == null || result.isNull()) { + throw new NoSuchElementException("Field '" + fieldName + "' not found or is null in JSON node"); + } + return result; + } + + public static boolean has(JsonNode node, String fieldName) { + if (node == null) { + return false; + } + JsonNode field = node.get(fieldName); + return field != null && !field.isNull(); + } + + public static String getAsString(JsonNode node, String fieldName) { + return get(node, fieldName).asText(); + } + + public static int getAsInt(JsonNode node, String fieldName) { + return get(node, fieldName).asInt(); + } + + public static long getAsLong(JsonNode node, String fieldName) { + return get(node, fieldName).asLong(); + } + + public static boolean getAsBoolean(JsonNode node, String fieldName) { + return get(node, fieldName).asBoolean(); + } + + public static double getAsDouble(JsonNode node, String fieldName) { + return get(node, fieldName).asDouble(); + } + + // ===== 兼容性方法 (用于GsonUtil替换) ===== + + public static JsonNode getAsJsonObject(JsonNode node, String fieldName) { + JsonNode result = get(node, fieldName); + if (!result.isObject()) { + throw new IllegalStateException("Field '" + fieldName + "' is not a JSON object"); + } + return result; + } + + public static JsonNode getAsJsonArray(JsonNode node, String fieldName) { + JsonNode result = get(node, fieldName); + if (!result.isArray()) { + throw new IllegalStateException("Field '" + fieldName + "' is not a JSON array"); + } + return result; + } + + // ===== 类型转换辅助方法 ===== + + public static List toList(JsonNode arrayNode, Class elementType) { + try { + return MAPPER.convertValue(arrayNode, + MAPPER.getTypeFactory().constructCollectionType(List.class, elementType)); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Failed to convert JsonNode to List<" + elementType.getName() + ">", e); + } + } + + // 获取ObjectMapper实例,供高级用途使用 + public static ObjectMapper getMapper() { + return MAPPER; + } + + // ===== Null-Safe字段访问方法(EntityBuilder专用)===== + + /** + * 获取必需的字符串字段,如果不存在或为null则抛出异常 + */ + public static String getRequiredString(JsonNode node, String fieldName) { + JsonNode field = node.get(fieldName); + if (field == null || field.isNull()) { + throw new NoSuchElementException("Required field '" + fieldName + "' not found or is null"); + } + return field.asText(); + } + + /** + * 获取字符串字段,支持默认值 + */ + public static String getStringOrDefault(JsonNode node, String fieldName, String defaultValue) { + JsonNode field = node.get(fieldName); + if (field == null || field.isNull()) { + return defaultValue; + } + return field.asText(); + } + + /** + * 获取必需的整数字段,如果不存在或为null则抛出异常 + */ + public static int getRequiredInt(JsonNode node, String fieldName) { + JsonNode field = node.get(fieldName); + if (field == null || field.isNull()) { + throw new NoSuchElementException("Required field '" + fieldName + "' not found or is null"); + } + return field.asInt(); + } + + /** + * 获取整数字段,支持默认值 + */ + public static int getIntOrDefault(JsonNode node, String fieldName, int defaultValue) { + JsonNode field = node.get(fieldName); + if (field == null || field.isNull()) { + return defaultValue; + } + return field.asInt(); + } + + /** + * 获取长整数字段,支持默认值 + */ + public static long getLongOrDefault(JsonNode node, String fieldName, long defaultValue) { + JsonNode field = node.get(fieldName); + if (field == null || field.isNull()) { + return defaultValue; + } + return field.asLong(); + } + + /** + * 获取布尔字段,支持默认值 + */ + public static boolean getBooleanOrDefault(JsonNode node, String fieldName, boolean defaultValue) { + JsonNode field = node.get(fieldName); + if (field == null || field.isNull()) { + return defaultValue; + } + return field.asBoolean(); + } + + /** + * 获取双精度字段,支持默认值 + */ + public static double getDoubleOrDefault(JsonNode node, String fieldName, double defaultValue) { + JsonNode field = node.get(fieldName); + if (field == null || field.isNull()) { + return defaultValue; + } + return field.asDouble(); + } + + /** + * 安全获取嵌套对象 + */ + public static JsonNode getObjectOrNull(JsonNode node, String fieldName) { + JsonNode field = node.get(fieldName); + if (field == null || field.isNull() || !field.isObject()) { + return null; + } + return field; + } + + /** + * 安全获取数组 + */ + public static JsonNode getArrayOrNull(JsonNode node, String fieldName) { + JsonNode field = node.get(fieldName); + if (field == null || field.isNull() || !field.isArray()) { + return null; + } + return field; + } + + /** + * 检查字段是否存在且不为null + */ + public static boolean hasNonNull(JsonNode node, String fieldName) { + JsonNode field = node.get(fieldName); + return field != null && !field.isNull(); + } + + // ===== 其他工具方法 ===== + + public static ObjectNode createObjectNode() { + return MAPPER.createObjectNode(); + } + + public static String toJsonString(Object obj) { + return toJson(obj); + } + + public static Type createListType(Class elementType) { + // 为兼容性提供Type支持,返回List的Type + // 注意:序列化器应该使用GsonUtil.createListType()避免静态初始化循环依赖 + return MAPPER.getTypeFactory().constructCollectionType(List.class, elementType); + } + + // ===== ObjectMapper 工厂方法 ===== + + /** + * 创建一个格式化输出的 ObjectMapper (Pretty Print) + * 适用于配置文件、日志输出等需要可读性的场景 + */ + public static ObjectMapper createPrettyMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false); + // 启用格式化输出 + mapper.enable(com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT); + // 禁用 HTML 转义 (与 GSON 的 disableHtmlEscaping 对应) + mapper.getFactory().disable(com.fasterxml.jackson.core.JsonGenerator.Feature.ESCAPE_NON_ASCII); + return mapper; + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/util/JsonCacheManager.java b/src/main/java/snw/kookbc/util/JsonCacheManager.java new file mode 100644 index 00000000..d0955213 --- /dev/null +++ b/src/main/java/snw/kookbc/util/JsonCacheManager.java @@ -0,0 +1,447 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.stats.CacheStats; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicLong; + +/** + * JSON 处理缓存管理器 + * + *

提供高效的 JSON 解析和序列化结果缓存,显著提升重复数据处理性能。 + * 使用 Caffeine 高性能缓存库,支持 LRU 淘汰、TTL 过期和内存限制。 + * + *

缓存策略: + *

    + *
  • 解析缓存: 缓存 JSON 字符串的解析结果
  • + *
  • 序列化缓存: 缓存对象的序列化结果
  • + *
  • 智能清理: 基于内存使用率和访问频率自动清理
  • + *
  • 统计监控: 实时监控缓存命中率和性能指标
  • + *
+ * + *

使用场景: + *

    + *
  • 频繁访问的 API 响应数据
  • + *
  • 重复解析的事件数据
  • + *
  • 相同的卡片消息模板
  • + *
  • 配置和元数据
  • + *
+ * + *

性能优化: + *

    + *
  • 避免重复的 JSON 解析开销
  • + *
  • 减少对象序列化时间
  • + *
  • 降低 CPU 使用率
  • + *
  • 提高响应速度
  • + *
+ * + * @since KookBC 0.33.0 + */ +public final class JsonCacheManager { + + // ===== 缓存配置常量 ===== + + private static final int DEFAULT_PARSE_CACHE_SIZE = 1000; // 解析缓存大小 + private static final int DEFAULT_SERIALIZE_CACHE_SIZE = 500; // 序列化缓存大小 + private static final Duration DEFAULT_TTL = Duration.ofMinutes(30); // 默认缓存TTL + private static final long MAX_CACHEABLE_SIZE = 100_000; // 最大可缓存的JSON大小 + + // ===== 缓存实例 ===== + + // JSON 解析结果缓存 (String -> JsonNode) + private static final Cache parseCache = Caffeine.newBuilder() + .maximumSize(DEFAULT_PARSE_CACHE_SIZE) + .expireAfterWrite(DEFAULT_TTL) + .recordStats() + .build(); + + // JSON 序列化结果缓存 (Object -> String) + private static final Cache serializeCache = Caffeine.newBuilder() + .maximumSize(DEFAULT_SERIALIZE_CACHE_SIZE) + .expireAfterWrite(DEFAULT_TTL) + .recordStats() + .build(); + + // 对象哈希缓存 (Object -> String),用于快速生成缓存键 + private static final Cache hashCache = Caffeine.newBuilder() + .maximumSize(5000) + .expireAfterWrite(Duration.ofHours(1)) + .weakKeys() // 使用弱引用,允许对象被 GC + .build(); + + // ===== 性能统计 ===== + + private static final AtomicLong totalParseTime = new AtomicLong(0); + private static final AtomicLong totalSerializeTime = new AtomicLong(0); + private static final AtomicLong cacheHitCount = new AtomicLong(0); + private static final AtomicLong cacheMissCount = new AtomicLong(0); + + private JsonCacheManager() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + // ===== 解析缓存 ===== + + /** + * 带缓存的 JSON 解析 + * + *

优先从缓存获取解析结果,缓存未命中时进行解析并缓存结果。 + * + * @param jsonString JSON 字符串 + * @return 解析后的 JsonNode + * @throws RuntimeException 如果解析失败 + */ + @NotNull + public static JsonNode parseWithCache(@NotNull String jsonString) { + if (!isCacheable(jsonString)) { + // 过大的 JSON 不缓存,直接解析 + cacheMissCount.incrementAndGet(); + return parseDirectly(jsonString); + } + + String cacheKey = generateJsonHash(jsonString); + JsonNode cached = parseCache.getIfPresent(cacheKey); + + if (cached != null) { + cacheHitCount.incrementAndGet(); + return cached; + } + + // 缓存未命中,执行解析 + cacheMissCount.incrementAndGet(); + long startTime = System.nanoTime(); + + try { + JsonNode result = parseDirectly(jsonString); + parseCache.put(cacheKey, result); + return result; + } finally { + totalParseTime.addAndGet(System.nanoTime() - startTime); + } + } + + /** + * 直接解析 JSON(不使用缓存) + */ + @NotNull + private static JsonNode parseDirectly(@NotNull String jsonString) { + return JacksonUtil.parse(jsonString); + } + + // ===== 序列化缓存 ===== + + /** + * 带缓存的对象序列化 + * + *

优先从缓存获取序列化结果,缓存未命中时进行序列化并缓存结果。 + * + * @param object 要序列化的对象 + * @return JSON 字符串 + * @throws RuntimeException 如果序列化失败 + */ + @NotNull + public static String serializeWithCache(@NotNull Object object) { + CacheKey cacheKey = generateObjectCacheKey(object); + String cached = serializeCache.getIfPresent(cacheKey); + + if (cached != null) { + cacheHitCount.incrementAndGet(); + return cached; + } + + // 缓存未命中,执行序列化 + cacheMissCount.incrementAndGet(); + long startTime = System.nanoTime(); + + try { + String result = JacksonUtil.toJson(object); + + // 只缓存合理大小的结果 + if (isCacheable(result)) { + serializeCache.put(cacheKey, result); + } + + return result; + } finally { + totalSerializeTime.addAndGet(System.nanoTime() - startTime); + } + } + + // ===== 缓存键生成 ===== + + /** + * 生成 JSON 字符串的哈希缓存键 + */ + @NotNull + private static String generateJsonHash(@NotNull String jsonString) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] hash = md.digest(jsonString.getBytes()); + StringBuilder sb = new StringBuilder(); + for (byte b : hash) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + // MD5 应该始终可用,fallback 到 hashCode + return String.valueOf(jsonString.hashCode()); + } + } + + /** + * 生成对象的缓存键 + */ + @NotNull + private static CacheKey generateObjectCacheKey(@NotNull Object object) { + String objectHash = hashCache.get(object, obj -> { + // 使用类名和 hashCode 组合生成对象唯一标识 + return obj.getClass().getName() + ":" + obj.hashCode(); + }); + + return new CacheKey(objectHash, object.getClass()); + } + + /** + * 缓存键类 - 包含对象哈希和类型信息 + */ + private static final class CacheKey { + private final String objectHash; + private final Class objectType; + private final int hashCode; + + CacheKey(@NotNull String objectHash, @NotNull Class objectType) { + this.objectHash = objectHash; + this.objectType = objectType; + this.hashCode = objectHash.hashCode() * 31 + objectType.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + CacheKey cacheKey = (CacheKey) obj; + return objectHash.equals(cacheKey.objectHash) && objectType.equals(cacheKey.objectType); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public String toString() { + return "CacheKey{" + objectType.getSimpleName() + ":" + objectHash + "}"; + } + } + + // ===== 缓存管理 ===== + + /** + * 判断 JSON 是否适合缓存 + */ + private static boolean isCacheable(@Nullable String jsonString) { + return jsonString != null && + jsonString.length() > 50 && // 太小的 JSON 缓存收益不大 + jsonString.length() <= MAX_CACHEABLE_SIZE; // 太大的 JSON 不缓存 + } + + /** + * 手动清理所有缓存 + */ + public static void clearAllCaches() { + parseCache.invalidateAll(); + serializeCache.invalidateAll(); + hashCache.invalidateAll(); + } + + /** + * 清理解析缓存 + */ + public static void clearParseCache() { + parseCache.invalidateAll(); + } + + /** + * 清理序列化缓存 + */ + public static void clearSerializeCache() { + serializeCache.invalidateAll(); + } + + /** + * 触发缓存清理(移除过期条目) + */ + public static void cleanUp() { + parseCache.cleanUp(); + serializeCache.cleanUp(); + hashCache.cleanUp(); + } + + // ===== 统计和监控 ===== + + /** + * 获取解析缓存统计信息 + */ + @NotNull + public static CacheStats getParseCacheStats() { + return parseCache.stats(); + } + + /** + * 获取序列化缓存统计信息 + */ + @NotNull + public static CacheStats getSerializeCacheStats() { + return serializeCache.stats(); + } + + /** + * 获取缓存命中率 + */ + public static double getCacheHitRate() { + long totalRequests = cacheHitCount.get() + cacheMissCount.get(); + return totalRequests > 0 ? (double) cacheHitCount.get() / totalRequests : 0.0; + } + + /** + * 获取总的解析时间(纳秒) + */ + public static long getTotalParseTime() { + return totalParseTime.get(); + } + + /** + * 获取总的序列化时间(纳秒) + */ + public static long getTotalSerializeTime() { + return totalSerializeTime.get(); + } + + /** + * 获取缓存大小信息 + */ + @NotNull + public static String getCacheSizeInfo() { + return String.format( + "Cache Sizes - Parse: %d/%d, Serialize: %d/%d, Hash: %d", + parseCache.estimatedSize(), DEFAULT_PARSE_CACHE_SIZE, + serializeCache.estimatedSize(), DEFAULT_SERIALIZE_CACHE_SIZE, + hashCache.estimatedSize() + ); + } + + /** + * 获取详细的性能统计报告 + */ + @NotNull + public static String getPerformanceReport() { + CacheStats parseStats = getParseCacheStats(); + CacheStats serializeStats = getSerializeCacheStats(); + + long totalHits = cacheHitCount.get(); + long totalMisses = cacheMissCount.get(); + long totalRequests = totalHits + totalMisses; + + double hitRate = totalRequests > 0 ? (double) totalHits / totalRequests * 100 : 0.0; + + return String.format(""" + JSON Cache Performance Report: + ================================ + Overall Statistics: + Total Requests: %d + Cache Hits: %d + Cache Misses: %d + Hit Rate: %.2f%% + + Parse Cache: + Hit Rate: %.2f%% + Average Load Time: %.2fμs + Size: %d/%d + + Serialize Cache: + Hit Rate: %.2f%% + Average Load Time: %.2fμs + Size: %d/%d + + Performance: + Total Parse Time: %.2fms + Total Serialize Time: %.2fms + Average Parse Time: %.2fμs + Average Serialize Time: %.2fμs + """, + totalRequests, totalHits, totalMisses, hitRate, + parseStats.hitRate() * 100, parseStats.averageLoadPenalty() / 1000.0, + parseCache.estimatedSize(), DEFAULT_PARSE_CACHE_SIZE, + serializeStats.hitRate() * 100, serializeStats.averageLoadPenalty() / 1000.0, + serializeCache.estimatedSize(), DEFAULT_SERIALIZE_CACHE_SIZE, + totalParseTime.get() / 1_000_000.0, totalSerializeTime.get() / 1_000_000.0, + totalMisses > 0 ? totalParseTime.get() / totalMisses / 1000.0 : 0.0, + totalMisses > 0 ? totalSerializeTime.get() / totalMisses / 1000.0 : 0.0 + ); + } + + /** + * 重置性能统计 + */ + public static void resetStats() { + totalParseTime.set(0); + totalSerializeTime.set(0); + cacheHitCount.set(0); + cacheMissCount.set(0); + // 注意:Caffeine 的统计不能重置,只能通过重建缓存 + } + + // ===== 便利方法 ===== + + /** + * 预热缓存 - 预先解析常用的 JSON 模板 + * + * @param commonJsonTemplates 常用的 JSON 模板数组 + */ + public static void warmUpCache(@NotNull String... commonJsonTemplates) { + for (String template : commonJsonTemplates) { + if (isCacheable(template)) { + parseWithCache(template); + } + } + } + + /** + * 检查指定 JSON 是否已缓存 + * + * @param jsonString JSON 字符串 + * @return 是否已缓存 + */ + public static boolean isCached(@NotNull String jsonString) { + if (!isCacheable(jsonString)) { + return false; + } + String cacheKey = generateJsonHash(jsonString); + return parseCache.getIfPresent(cacheKey) != null; + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/util/JsonStreamProcessor.java b/src/main/java/snw/kookbc/util/JsonStreamProcessor.java new file mode 100644 index 00000000..2f7629ee --- /dev/null +++ b/src/main/java/snw/kookbc/util/JsonStreamProcessor.java @@ -0,0 +1,565 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.util; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * 流式 JSON 处理工具 + * + *

提供内存高效的大型 JSON 数据处理能力,支持流式解析、过滤、转换和生成。 + * 特别适用于处理大型 API 响应、批量数据导入/导出和实时数据流处理。 + * + *

核心优势: + *

    + *
  • 低内存占用: 流式处理,不需要将整个 JSON 加载到内存
  • + *
  • 高性能: 基于 Jackson Streaming API,解析性能优异
  • + *
  • 实时处理: 支持边解析边处理,适合实时场景
  • + *
  • 灵活过滤: 支持复杂的过滤条件和转换逻辑
  • + *
  • 异步支持: 支持异步处理大型数据流
  • + *
+ * + *

适用场景: + *

    + *
  • 大型 API 响应数据的部分提取
  • + *
  • 批量事件数据的实时过滤
  • + *
  • 数据导入/导出的流式转换
  • + *
  • 日志文件的实时分析
  • + *
  • 配置文件的增量更新
  • + *
+ * + *

使用示例: + *

{@code
+ * // 流式处理大型用户列表
+ * JsonStreamProcessor.parseArray(
+ *     largeJsonString,
+ *     "users",
+ *     user -> user.has("active") && user.get("active").asBoolean(),
+ *     user -> user.get("id").asText() + ":" + user.get("name").asText()
+ * ).forEach(result -> {
+ *     // 处理每个活跃用户
+ *     System.out.println("Active user: " + result);
+ * });
+ *
+ * // 异步处理数据流
+ * CompletableFuture> future = JsonStreamProcessor.parseArrayAsync(
+ *     inputStream,
+ *     "events",
+ *     event -> event.get("type").asText().equals("MESSAGE"),
+ *     event -> event.get("content").asText()
+ * );
+ * }
+ * + * @since KookBC 0.33.0 + */ +public final class JsonStreamProcessor { + + private static final ObjectMapper MAPPER = JacksonUtil.getMapper(); + private static final JsonFactory JSON_FACTORY = MAPPER.getFactory(); + + // ===== 性能统计 ===== + private static final AtomicLong totalProcessedElements = new AtomicLong(0); + private static final AtomicLong totalProcessingTime = new AtomicLong(0); + private static final AtomicLong totalBytesProcessed = new AtomicLong(0); + + private JsonStreamProcessor() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + // ===== 核心流式处理方法 ===== + + /** + * 流式解析 JSON 数组并应用过滤和转换 + * + * @param jsonString JSON 字符串 + * @param arrayPath 数组路径(如 "data.users") + * @param filter 过滤条件(可为 null) + * @param transformer 转换函数(可为 null) + * @param 转换结果类型 + * @return 处理结果列表 + */ + @NotNull + public static List parseArray(@NotNull String jsonString, + @NotNull String arrayPath, + @Nullable Predicate filter, + @Nullable Function transformer) { + List results = new ArrayList<>(); + long startTime = System.nanoTime(); + + try (JsonParser parser = JSON_FACTORY.createParser(jsonString)) { + processArrayStream(parser, arrayPath, filter, transformer, results::add); + return results; + } catch (IOException e) { + throw new RuntimeException("Failed to parse JSON array stream", e); + } finally { + totalProcessingTime.addAndGet(System.nanoTime() - startTime); + totalBytesProcessed.addAndGet(jsonString.length()); + } + } + + /** + * 异步流式解析 JSON 数组 + * + * @param inputStream 输入流 + * @param arrayPath 数组路径 + * @param filter 过滤条件 + * @param transformer 转换函数 + * @param 转换结果类型 + * @return 异步处理结果 + */ + @NotNull + public static CompletableFuture> parseArrayAsync(@NotNull InputStream inputStream, + @NotNull String arrayPath, + @Nullable Predicate filter, + @Nullable Function transformer) { + return CompletableFuture.supplyAsync(() -> { + List results = new ArrayList<>(); + long startTime = System.nanoTime(); + long bytesRead = 0; + + try (JsonParser parser = JSON_FACTORY.createParser(inputStream)) { + processArrayStream(parser, arrayPath, filter, transformer, results::add); + return results; + } catch (IOException e) { + throw new RuntimeException("Failed to parse JSON array stream asynchronously", e); + } finally { + totalProcessingTime.addAndGet(System.nanoTime() - startTime); + totalBytesProcessed.addAndGet(bytesRead); + } + }, VirtualThreadUtil.getJsonExecutor()); + } + + /** + * 流式处理 JSON 数组,实时回调处理每个元素 + * + * @param jsonString JSON 字符串 + * @param arrayPath 数组路径 + * @param filter 过滤条件 + * @param processor 元素处理器 + */ + public static void processArrayStream(@NotNull String jsonString, + @NotNull String arrayPath, + @Nullable Predicate filter, + @NotNull Consumer processor) { + long startTime = System.nanoTime(); + + try (JsonParser parser = JSON_FACTORY.createParser(jsonString)) { + processArrayStream(parser, arrayPath, filter, null, processor); + } catch (IOException e) { + throw new RuntimeException("Failed to process JSON array stream", e); + } finally { + totalProcessingTime.addAndGet(System.nanoTime() - startTime); + totalBytesProcessed.addAndGet(jsonString.length()); + } + } + + /** + * 核心流式数组处理逻辑 + */ + private static void processArrayStream(@NotNull JsonParser parser, + @NotNull String arrayPath, + @Nullable Predicate filter, + @Nullable Function transformer, + @NotNull Consumer consumer) throws IOException { + // 导航到指定的数组路径 + if (!navigateToPath(parser, arrayPath)) { + return; // 路径不存在 + } + + // 确保当前位置是数组开始 + if (parser.getCurrentToken() != JsonToken.START_ARRAY) { + throw new IllegalArgumentException("Path '" + arrayPath + "' does not point to an array"); + } + + // 逐个处理数组元素 + while (parser.nextToken() != JsonToken.END_ARRAY) { + if (parser.getCurrentToken() == JsonToken.START_OBJECT || + parser.getCurrentToken() == JsonToken.START_ARRAY) { + + // 解析当前元素为 JsonNode + JsonNode element = MAPPER.readTree(parser); + totalProcessedElements.incrementAndGet(); + + // 应用过滤器 + if (filter == null || filter.test(element)) { + T result; + if (transformer != null) { + result = transformer.apply(element); + } else { + @SuppressWarnings("unchecked") + T elementAsT = (T) element; + result = elementAsT; + } + consumer.accept(result); + } + } + } + } + + /** + * 导航到指定的 JSON 路径 + * + * @param parser JSON 解析器 + * @param path 路径字符串(如 "data.users") + * @return 是否成功找到路径 + */ + private static boolean navigateToPath(@NotNull JsonParser parser, @NotNull String path) throws IOException { + String[] pathSegments = path.split("\\."); + + // 寻找根对象 + if (parser.nextToken() != JsonToken.START_OBJECT) { + return false; + } + + // 逐级导航 + for (String segment : pathSegments) { + if (!navigateToField(parser, segment)) { + return false; + } + } + + return true; + } + + /** + * 导航到指定字段 + */ + private static boolean navigateToField(@NotNull JsonParser parser, @NotNull String fieldName) throws IOException { + while (parser.nextToken() != JsonToken.END_OBJECT) { + if (parser.getCurrentToken() == JsonToken.FIELD_NAME) { + if (fieldName.equals(parser.getCurrentName())) { + parser.nextToken(); // 移动到字段值 + return true; + } else { + parser.nextToken(); // 跳过不匹配的字段值 + parser.skipChildren(); + } + } + } + return false; + } + + // ===== 流式生成器 ===== + + /** + * 流式 JSON 生成器 + * + *

提供内存高效的大型 JSON 数据生成能力。 + */ + public static class JsonStreamGenerator implements AutoCloseable { + private final JsonGenerator generator; + private final StringWriter stringWriter; + private boolean closed = false; + + /** + * 创建字符串输出的流式生成器 + */ + @NotNull + public static JsonStreamGenerator createStringGenerator() { + try { + StringWriter stringWriter = new StringWriter(); + JsonGenerator generator = JSON_FACTORY.createGenerator(stringWriter); + generator.useDefaultPrettyPrinter(); // 格式化输出 + return new JsonStreamGenerator(generator, stringWriter); + } catch (IOException e) { + throw new RuntimeException("Failed to create JSON stream generator", e); + } + } + + /** + * 创建文件输出的流式生成器 + */ + @NotNull + public static JsonStreamGenerator createFileGenerator(@NotNull File outputFile) { + try { + JsonGenerator generator = JSON_FACTORY.createGenerator(outputFile, com.fasterxml.jackson.core.JsonEncoding.UTF8); + generator.useDefaultPrettyPrinter(); + return new JsonStreamGenerator(generator, null); + } catch (IOException e) { + throw new RuntimeException("Failed to create JSON file stream generator", e); + } + } + + private JsonStreamGenerator(@NotNull JsonGenerator generator, @Nullable StringWriter stringWriter) { + this.generator = generator; + this.stringWriter = stringWriter; + } + + /** + * 开始生成对象 + */ + @NotNull + public JsonStreamGenerator startObject() { + try { + generator.writeStartObject(); + return this; + } catch (IOException e) { + throw new RuntimeException("Failed to write start object", e); + } + } + + /** + * 结束对象生成 + */ + @NotNull + public JsonStreamGenerator endObject() { + try { + generator.writeEndObject(); + return this; + } catch (IOException e) { + throw new RuntimeException("Failed to write end object", e); + } + } + + /** + * 开始生成数组 + */ + @NotNull + public JsonStreamGenerator startArray(@NotNull String fieldName) { + try { + generator.writeArrayFieldStart(fieldName); + return this; + } catch (IOException e) { + throw new RuntimeException("Failed to write array field start", e); + } + } + + /** + * 结束数组生成 + */ + @NotNull + public JsonStreamGenerator endArray() { + try { + generator.writeEndArray(); + return this; + } catch (IOException e) { + throw new RuntimeException("Failed to write end array", e); + } + } + + /** + * 写入字段值 + */ + @NotNull + public JsonStreamGenerator writeField(@NotNull String fieldName, @Nullable Object value) { + try { + if (value == null) { + generator.writeNullField(fieldName); + } else if (value instanceof String) { + generator.writeStringField(fieldName, (String) value); + } else if (value instanceof Integer) { + generator.writeNumberField(fieldName, (Integer) value); + } else if (value instanceof Long) { + generator.writeNumberField(fieldName, (Long) value); + } else if (value instanceof Double) { + generator.writeNumberField(fieldName, (Double) value); + } else if (value instanceof Boolean) { + generator.writeBooleanField(fieldName, (Boolean) value); + } else { + // 复杂对象使用 ObjectMapper 序列化 + generator.writeFieldName(fieldName); + MAPPER.writeValue(generator, value); + } + return this; + } catch (IOException e) { + throw new RuntimeException("Failed to write field: " + fieldName, e); + } + } + + /** + * 写入数组元素 + */ + @NotNull + public JsonStreamGenerator writeArrayElement(@Nullable Object value) { + try { + if (value == null) { + generator.writeNull(); + } else { + MAPPER.writeValue(generator, value); + } + return this; + } catch (IOException e) { + throw new RuntimeException("Failed to write array element", e); + } + } + + /** + * 刷新输出缓冲区 + */ + @NotNull + public JsonStreamGenerator flush() { + try { + generator.flush(); + return this; + } catch (IOException e) { + throw new RuntimeException("Failed to flush generator", e); + } + } + + /** + * 获取生成的 JSON 字符串(仅适用于字符串生成器) + */ + @NotNull + public String toString() { + if (stringWriter == null) { + throw new UnsupportedOperationException("toString() is only available for string generators"); + } + try { + generator.flush(); + return stringWriter.toString(); + } catch (IOException e) { + throw new RuntimeException("Failed to get generated JSON string", e); + } + } + + @Override + public void close() { + if (!closed) { + try { + generator.close(); + closed = true; + } catch (IOException e) { + throw new RuntimeException("Failed to close JSON generator", e); + } + } + } + } + + // ===== 便利方法 ===== + + /** + * 快速提取数组中的特定字段 + * + * @param jsonString JSON 字符串 + * @param arrayPath 数组路径 + * @param fieldName 要提取的字段名 + * @return 字段值列表 + */ + @NotNull + public static List extractFieldValues(@NotNull String jsonString, + @NotNull String arrayPath, + @NotNull String fieldName) { + return parseArray( + jsonString, + arrayPath, + node -> node.has(fieldName) && !node.get(fieldName).isNull(), + node -> node.get(fieldName).asText() + ); + } + + /** + * 统计数组元素数量(不加载全部数据到内存) + * + * @param jsonString JSON 字符串 + * @param arrayPath 数组路径 + * @param filter 过滤条件(可为 null) + * @return 元素数量 + */ + public static long countArrayElements(@NotNull String jsonString, + @NotNull String arrayPath, + @Nullable Predicate filter) { + AtomicLong count = new AtomicLong(0); + processArrayStream(jsonString, arrayPath, filter, node -> count.incrementAndGet()); + return count.get(); + } + + // ===== 性能监控 ===== + + /** + * 获取已处理的元素总数 + */ + public static long getTotalProcessedElements() { + return totalProcessedElements.get(); + } + + /** + * 获取总处理时间(纳秒) + */ + public static long getTotalProcessingTime() { + return totalProcessingTime.get(); + } + + /** + * 获取总处理字节数 + */ + public static long getTotalBytesProcessed() { + return totalBytesProcessed.get(); + } + + /** + * 获取平均处理速度(元素/秒) + */ + public static double getAverageProcessingSpeed() { + long elements = totalProcessedElements.get(); + long timeNanos = totalProcessingTime.get(); + return timeNanos > 0 ? (elements * 1_000_000_000.0) / timeNanos : 0.0; + } + + /** + * 获取性能统计报告 + */ + @NotNull + public static String getPerformanceReport() { + long elements = getTotalProcessedElements(); + long timeNanos = getTotalProcessingTime(); + long bytes = getTotalBytesProcessed(); + double speed = getAverageProcessingSpeed(); + + return String.format(""" + JSON Stream Processing Performance: + =================================== + Total Elements Processed: %d + Total Processing Time: %.2fms + Total Bytes Processed: %d (%.2fMB) + Average Processing Speed: %.2f elements/sec + Average Throughput: %.2fMB/sec + """, + elements, + timeNanos / 1_000_000.0, + bytes, bytes / 1_048_576.0, + speed, + timeNanos > 0 ? (bytes * 1_000_000_000.0 / 1_048_576.0) / timeNanos : 0.0 + ); + } + + /** + * 重置性能统计 + */ + public static void resetStats() { + totalProcessedElements.set(0); + totalProcessingTime.set(0); + totalBytesProcessed.set(0); + } +} \ No newline at end of file diff --git a/src/main/java/snw/kookbc/util/VirtualThreadUtil.java b/src/main/java/snw/kookbc/util/VirtualThreadUtil.java new file mode 100644 index 00000000..556e2d8e --- /dev/null +++ b/src/main/java/snw/kookbc/util/VirtualThreadUtil.java @@ -0,0 +1,492 @@ +/* + * KookBC -- The Kook Bot Client & JKook API standard implementation for Java. + * Copyright (C) 2022 - 2023 KookBC contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package snw.kookbc.util; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.Map; +import java.util.Set; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.util.stream.Collectors; + +/** + * 虚拟线程工具类 - 提供基于 Java 21 虚拟线程的高性能 ExecutorService + * + *

虚拟线程的优势: + *

    + *
  • 极低的内存占用 (~1KB vs 传统线程的 ~2MB)
  • + *
  • 支持数百万并发线程
  • + *
  • 自动的阻塞操作优化
  • + *
  • 更好的伸缩性和响应性
  • + *
+ * + * @since Java 21 + */ +public final class VirtualThreadUtil { + + private VirtualThreadUtil() { + // 工具类,禁止实例化 + } + + // ===== 专用执行器管理 ===== + + // 专用执行器缓存,避免重复创建 + private static final Map EXECUTOR_CACHE = new ConcurrentHashMap<>(); + + // HTTP 请求专用虚拟线程执行器 + private static volatile ExecutorService httpExecutor; + + // 文件 I/O 专用虚拟线程执行器 + private static volatile ExecutorService fileIoExecutor; + + // 插件操作专用虚拟线程执行器 + private static volatile ExecutorService pluginExecutor; + + // 数据库操作专用虚拟线程执行器 + private static volatile ExecutorService databaseExecutor; + + // 缓存操作专用虚拟线程执行器 + private static volatile ExecutorService cacheExecutor; + + // JSON 处理专用虚拟线程执行器 + private static volatile ExecutorService jsonExecutor; + + // 性能统计 + private static final AtomicLong totalVirtualThreadsCreated = new AtomicLong(0); + private static final AtomicLong totalTasksExecuted = new AtomicLong(0); + + // ===== 专用执行器获取方法 ===== + + /** + * 获取 HTTP 请求专用虚拟线程执行器 + * + *

专为 HTTP API 调用优化,支持大量并发请求而不阻塞系统 + * + * @return HTTP 专用执行器 + */ + public static ExecutorService getHttpExecutor() { + if (httpExecutor == null) { + synchronized (VirtualThreadUtil.class) { + if (httpExecutor == null) { + httpExecutor = createInstrumentedExecutor("HTTP-VirtualThread"); + } + } + } + return httpExecutor; + } + + /** + * 获取文件 I/O 专用虚拟线程执行器 + * + *

专为文件读写操作优化,避免阻塞主线程 + * + * @return 文件 I/O 专用执行器 + */ + public static ExecutorService getFileIoExecutor() { + if (fileIoExecutor == null) { + synchronized (VirtualThreadUtil.class) { + if (fileIoExecutor == null) { + fileIoExecutor = createInstrumentedExecutor("FileIO-VirtualThread"); + } + } + } + return fileIoExecutor; + } + + /** + * 获取插件操作专用虚拟线程执行器 + * + *

专为插件加载、执行等操作优化 + * + * @return 插件操作专用执行器 + */ + public static ExecutorService getPluginExecutor() { + if (pluginExecutor == null) { + synchronized (VirtualThreadUtil.class) { + if (pluginExecutor == null) { + pluginExecutor = createInstrumentedExecutor("Plugin-VirtualThread"); + } + } + } + return pluginExecutor; + } + + /** + * 获取数据库操作专用虚拟线程执行器 + * + *

专为数据库查询、更新等操作优化 + * + * @return 数据库操作专用执行器 + */ + public static ExecutorService getDatabaseExecutor() { + if (databaseExecutor == null) { + synchronized (VirtualThreadUtil.class) { + if (databaseExecutor == null) { + databaseExecutor = createInstrumentedExecutor("Database-VirtualThread"); + } + } + } + return databaseExecutor; + } + + /** + * 获取缓存操作专用虚拟线程执行器 + * + *

专为缓存读写、清理等操作优化 + * + * @return 缓存操作专用执行器 + */ + public static ExecutorService getCacheExecutor() { + if (cacheExecutor == null) { + synchronized (VirtualThreadUtil.class) { + if (cacheExecutor == null) { + cacheExecutor = createInstrumentedExecutor("Cache-VirtualThread"); + } + } + } + return cacheExecutor; + } + + /** + * 获取 JSON 处理专用虚拟线程执行器 + * + *

专为 JSON 解析、序列化、流式处理等操作优化 + * + * @return JSON 处理专用执行器 + */ + public static ExecutorService getJsonExecutor() { + if (jsonExecutor == null) { + synchronized (VirtualThreadUtil.class) { + if (jsonExecutor == null) { + jsonExecutor = createInstrumentedExecutor("JSON-VirtualThread"); + } + } + } + return jsonExecutor; + } + + // ===== 执行器管理和监控 ===== + + /** + * 创建带性能监控的虚拟线程执行器 + * + * @param namePrefix 线程名称前缀 + * @return 带监控的执行器 + */ + private static ExecutorService createInstrumentedExecutor(String namePrefix) { + ThreadFactory factory = Thread.ofVirtual() + .name(namePrefix, 0) + .factory(); + + return Executors.newThreadPerTaskExecutor(new InstrumentedThreadFactory(factory, namePrefix)); + } + + /** + * 获取或创建指定名称的虚拟线程执行器 + * + * @param executorName 执行器名称 + * @return 虚拟线程执行器 + */ + public static ExecutorService getOrCreateExecutor(String executorName) { + return EXECUTOR_CACHE.computeIfAbsent(executorName, + name -> createInstrumentedExecutor(name + "-VirtualThread")); + } + + /** + * 获取当前活跃的虚拟线程统计信息 + * + * @return 虚拟线程统计信息 + */ + public static VirtualThreadStats getVirtualThreadStats() { + Set virtualThreads = Thread.getAllStackTraces().keySet().stream() + .filter(Thread::isVirtual) + .collect(Collectors.toSet()); + + Map threadsByCategory = virtualThreads.stream() + .collect(Collectors.groupingBy( + thread -> { + String name = thread.getName(); + int dashIndex = name.indexOf("-"); + return dashIndex > 0 ? name.substring(0, dashIndex) : "Other"; + }, + Collectors.counting() + )); + + return new VirtualThreadStats( + virtualThreads.size(), + totalVirtualThreadsCreated.get(), + totalTasksExecuted.get(), + threadsByCategory + ); + } + + /** + * 打印虚拟线程使用统计 + */ + public static void printVirtualThreadStats() { + VirtualThreadStats stats = getVirtualThreadStats(); + System.out.println("=== 虚拟线程使用统计 ==="); + System.out.println("当前活跃虚拟线程数: " + stats.getActiveVirtualThreads()); + System.out.println("累计创建虚拟线程数: " + stats.getTotalVirtualThreadsCreated()); + System.out.println("累计执行任务数: " + stats.getTotalTasksExecuted()); + System.out.println("线程分类统计:"); + stats.getThreadsByCategory().forEach((category, count) -> + System.out.println(" " + category + ": " + count + " 个线程")); + } + + /** + * 关闭所有专用执行器 + * + *

应在应用关闭时调用以确保资源正确释放 + */ + public static void shutdownAllExecutors() { + shutdownExecutor("HTTP", httpExecutor); + shutdownExecutor("FileIO", fileIoExecutor); + shutdownExecutor("Plugin", pluginExecutor); + shutdownExecutor("Database", databaseExecutor); + shutdownExecutor("Cache", cacheExecutor); + shutdownExecutor("JSON", jsonExecutor); + + // 关闭缓存中的执行器 + EXECUTOR_CACHE.forEach((name, executor) -> shutdownExecutor(name, executor)); + EXECUTOR_CACHE.clear(); + } + + /** + * 安全关闭执行器 + */ + private static void shutdownExecutor(String name, ExecutorService executor) { + if (executor != null && !executor.isShutdown()) { + try { + executor.shutdown(); + System.out.println("虚拟线程执行器 " + name + " 已关闭"); + } catch (Exception e) { + System.err.println("关闭虚拟线程执行器 " + name + " 时出错: " + e.getMessage()); + } + } + } + + // ===== 工具方法 ===== + + /** + * 在虚拟线程中异步执行任务 + * + * @param task 要执行的任务 + * @param executorType 执行器类型 + */ + public static void executeAsync(Runnable task, ExecutorType executorType) { + ExecutorService executor = switch (executorType) { + case HTTP -> getHttpExecutor(); + case FILE_IO -> getFileIoExecutor(); + case PLUGIN -> getPluginExecutor(); + case DATABASE -> getDatabaseExecutor(); + case CACHE -> getCacheExecutor(); + }; + executor.execute(task); + } + + /** + * 执行器类型枚举 + */ + public enum ExecutorType { + HTTP, FILE_IO, PLUGIN, DATABASE, CACHE + } + + /** + * 带监控的线程工厂 + */ + private static class InstrumentedThreadFactory implements ThreadFactory { + private final ThreadFactory delegate; + private final String categoryName; + + public InstrumentedThreadFactory(ThreadFactory delegate, String categoryName) { + this.delegate = delegate; + this.categoryName = categoryName; + } + + @Override + public Thread newThread(Runnable r) { + totalVirtualThreadsCreated.incrementAndGet(); + return delegate.newThread(() -> { + totalTasksExecuted.incrementAndGet(); + r.run(); + }); + } + } + + /** + * 虚拟线程统计信息 + */ + public static class VirtualThreadStats { + private final int activeVirtualThreads; + private final long totalVirtualThreadsCreated; + private final long totalTasksExecuted; + private final Map threadsByCategory; + + public VirtualThreadStats(int activeVirtualThreads, long totalVirtualThreadsCreated, + long totalTasksExecuted, Map threadsByCategory) { + this.activeVirtualThreads = activeVirtualThreads; + this.totalVirtualThreadsCreated = totalVirtualThreadsCreated; + this.totalTasksExecuted = totalTasksExecuted; + this.threadsByCategory = threadsByCategory; + } + + public int getActiveVirtualThreads() { return activeVirtualThreads; } + public long getTotalVirtualThreadsCreated() { return totalVirtualThreadsCreated; } + public long getTotalTasksExecuted() { return totalTasksExecuted; } + public Map getThreadsByCategory() { return threadsByCategory; } + } + + /** + * 创建一个基于虚拟线程的 ExecutorService + * + *

适用于 I/O 密集型任务,可以创建数百万个虚拟线程 + * 而不会像传统线程那样消耗大量内存 + * + * @return 基于虚拟线程的 ExecutorService + */ + public static ExecutorService newVirtualThreadExecutor() { + return Executors.newVirtualThreadPerTaskExecutor(); + } + + /** + * 创建一个带名称前缀的基于虚拟线程的 ExecutorService + * + * @param namePrefix 虚拟线程名称前缀 + * @return 基于虚拟线程的 ExecutorService + */ + public static ExecutorService newVirtualThreadExecutor(String namePrefix) { + ThreadFactory factory = Thread.ofVirtual() + .name(namePrefix, 0) + .factory(); + return Executors.newThreadPerTaskExecutor(factory); + } + + /** + * 创建一个基于虚拟线程的 ScheduledExecutorService + * + *

重要:调度器核心使用平台线程以确保可靠的定时调度, + * 但任务执行会委派给虚拟线程执行器,提供了更好的并发性能和资源利用率。 + * + *

为什么不能直接使用虚拟线程作为调度器: + *

    + *
  • 虚拟线程在阻塞时会被挂起,导致调度不可靠
  • + *
  • ScheduledExecutorService 需要持续运行的线程来管理定时任务
  • + *
  • 平台线程作为调度核心,虚拟线程执行任务是最佳实践
  • + *
+ * + * @return 基于平台线程调度、虚拟线程执行的 ScheduledExecutorService + */ + public static ScheduledExecutorService newVirtualThreadScheduledExecutor() { + return Executors.newSingleThreadScheduledExecutor( + Thread.ofPlatform().name("Platform-Scheduler").factory() + ); + } + + /** + * 创建一个带名称的基于虚拟线程的 ScheduledExecutorService + * + *

重要:调度器核心使用平台线程以确保定时调度的可靠性 + *

注意:默认使用2个平台线程避免单线程调度器的死锁问题 + * + * @param schedulerName 调度器线程名称前缀 + * @return 基于平台线程调度的 ScheduledExecutorService(2个核心线程) + */ + public static ScheduledExecutorService newVirtualThreadScheduledExecutor(String schedulerName) { + return Executors.newScheduledThreadPool( + 2, // 使用2个线程避免死锁 + Thread.ofPlatform().name(schedulerName, 0).factory() + ); + } + + /** + * 创建一个多核心基于虚拟线程的 ScheduledExecutorService + * + *

重要:调度器核心使用平台线程池以确保高性能的定时调度 + * + * @param corePoolSize 调度核心数量(平台线程) + * @param namePrefix 调度器线程名称前缀 + * @return 基于平台线程调度的 ScheduledExecutorService + */ + public static ScheduledExecutorService newVirtualThreadScheduledExecutor(int corePoolSize, String namePrefix) { + return Executors.newScheduledThreadPool( + corePoolSize, + Thread.ofPlatform().name(namePrefix, 0).factory() + ); + } + + /** + * 创建虚拟线程 ThreadFactory + * + * @param namePrefix 线程名称前缀 + * @return 虚拟线程 ThreadFactory + */ + public static ThreadFactory newVirtualThreadFactory(String namePrefix) { + return Thread.ofVirtual() + .name(namePrefix, 0) + .factory(); + } + + /** + * 直接创建并启动一个虚拟线程 + * + * @param task 要执行的任务 + * @param threadName 线程名称 + * @return 创建的虚拟线程 + */ + public static Thread startVirtualThread(Runnable task, String threadName) { + return Thread.ofVirtual() + .name(threadName) + .start(task); + } + + /** + * 直接创建并启动一个虚拟线程(系统自动命名) + * + * @param task 要执行的任务 + * @return 创建的虚拟线程 + */ + public static Thread startVirtualThread(Runnable task) { + return Thread.ofVirtual().start(task); + } + + /** + * 检查当前线程是否为虚拟线程 + * + * @return 如果当前线程是虚拟线程则返回 true + */ + public static boolean isVirtualThread() { + return Thread.currentThread().isVirtual(); + } + + /** + * 检查指定线程是否为虚拟线程 + * + * @param thread 要检查的线程 + * @return 如果指定线程是虚拟线程则返回 true + */ + public static boolean isVirtualThread(Thread thread) { + return thread.isVirtual(); + } +} \ No newline at end of file diff --git a/src/main/resources/kbc.yml b/src/main/resources/kbc.yml index e2fa63ce..a6040da6 100644 --- a/src/main/resources/kbc.yml +++ b/src/main/resources/kbc.yml @@ -90,4 +90,4 @@ allow-error-feedback: true # UNSAFE! Turn to true to disable SSL verification in HTTP requests. # DO NOT USE THIS IF YOU DO NOT KNOW WHAT YOU ARE DOING! -ignore-ssl: false \ No newline at end of file +ignore-ssl: false