From 57ba115f7f96b3bcc14177d11b17efa401fe94cb Mon Sep 17 00:00:00 2001 From: RealSeek <66584031+RealSeek@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:15:39 +0800 Subject: [PATCH 01/10] refactor: huge project refactor powered by Claude AI (#113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build!(core): upgrade to java 21 and adopt virtual threads - update java target and minimum toolchain to version 21 in build configuration - add `VirtualThreadUtil` to provide high-performance virtual thread-based executors - refactor schedulers, event executors, and network threads to use virtual threads - this improves concurrency and performance for I/O-bound tasks throughout the application BREAKING CHANGE: The project now requires Java 21 or later to build and run. * perf(json): migrate from gson to jackson for core json processing - introduce `JacksonUtil` as a high-performance replacement for `GsonUtil` - add jackson dependencies to the project build configuration - refactor `HttpAPIImpl` to use jackson for parsing api responses, improving performance * test(framework): introduce comprehensive testing and benchmarking suite - add a full suite of unit tests using JUnit 5, Mockito, and AssertJ for core components - configure JaCoCo for code coverage reporting and set coverage verification goals - set up JMH (Java Microbenchmark Harness) for performance benchmarking - add benchmark tests for virtual threads vs platform threads and jackson vs gson * docs(architecture): add extensive project and module architecture documentation - create detailed markdown documents for a project overview and key modules - document the architecture of the core, command, entity, event, network, and plugin systems - use mermaid diagrams to illustrate system design, class relationships, and data flows * refactor(json)!: migrate core JSON processing from Gson to Jackson This commit replaces Gson with Jackson as the primary JSON processing library. The migration significantly improves performance and enhances robustness against incomplete API responses by providing better null-safety. New stuff: - introduced `JacksonUtil` for high-performance, null-safe JSON handling - introduced `JacksonCardUtil` for specialized card message serialization Refactor: - network layer now uses `JsonNode` for API responses - entity builders (`EntityBuilder`, `MessageBuilder`) now prioritize Jackson for safer construction - all event deserializers updated to use Jackson with a Gson fallback for compatibility - all entity implementations (`*Impl.java`) now support updates from `JsonNode` - all page iterators now process `JsonNode` for improved performance Docs: - updated architecture documents (`CLAUDE.md`) to reflect the new dual-engine JSON architecture BREAKING CHANGE: The core JSON processing engine has been switched from Gson to Jackson. Internal APIs that previously exposed `com.google.gson.JsonObject` now primarily use `com.fasterxml.jackson.databind.JsonNode`. While compatibility layers are in place, projects with deep integration may need adjustments. * feat(async): introduce virtual threads for enhanced performance and async apis - feat(util): Introduced `VirtualThreadUtil` for centralized management of Java 21 virtual threads. - Provides dedicated, lazily-initialized executors for tasks like HTTP, File I/O, and plugins. - Includes performance monitoring for thread creation and task execution. - perf(event): Implemented optional parallel event processing using virtual threads. - Allows concurrent execution of event listeners to significantly improve throughput. - Added advanced performance monitoring with periodic reports for the event system. - New settings are configurable in `kbc.yml`. - perf(network): Added an asynchronous API to `NetworkClient` for non-blocking operations. - New `*Async` methods leverage virtual threads for high-concurrency HTTP requests. - Introduced batch methods (`batchGetAsync`, `batchPostAsync`) for parallel API calls. - perf(plugin): Implemented asynchronous methods for plugin management in `SimplePluginManager`. - New `*Async` methods (load, enable, disable, reload) prevent blocking during I/O operations. - perf(storage): Introduced an asynchronous API for `EntityStorage` to improve cache operations. - `*Async` methods for fetching entities and managing the cache run on virtual threads. - fix(channel): Corrected Jackson deserialization for `VoiceChannelImpl.StreamingInfoImpl`. - Added `@JsonCreator` and `@JsonProperty` annotations for robust JSON parsing. * feat(http): introduce async http api with request coalescing - feat: Add `AsyncHttpAPI` interface and implementation for non-blocking HTTP requests. - This includes async methods for file uploads (from file, bytes, and URL), entity fetching, and batch operations. - perf: Implement a smart `RequestCoalescer` to merge identical concurrent requests. - This avoids redundant network calls, significantly improving performance and reducing API spam under high load. - refactor: Update existing synchronous methods to use the new async backend, centralizing logic and improving efficiency. - test: Introduce comprehensive unit tests for all new async functionality and the coalescing mechanism. * perf(network): overhaul http client for enhanced concurrency and performance - refactor OkHttpClient configuration for high-throughput scenarios - utilize a virtual thread-backed dispatcher to improve request handling capacity - configure a larger connection pool (50 idle, 15 min keep-alive) to reduce connection churn - fine-tune connection, read, write, and call timeouts for better reliability - enable automatic retries on connection failure and support for HTTP/2 protocol * feat(util): add dedicated json executor and network monitoring - util: introduce a dedicated virtual thread executor for json processing to isolate workloads - network: add methods to monitor connection pool statistics (total, idle, active connections) - network: implement a method to manually evict all idle connections from the pool * feat(core): introduce advanced performance and concurrency framework This update introduces a framework for high-performance JSON processing and asynchronous operations. - JSON Optimization: - `JsonEngineSelector`: Intelligently selects between Jackson and Gson for optimal performance. - `JsonCacheManager`: Implements a Caffeine-based cache for JSON parsing and serialization. - `JsonStreamProcessor`: Provides a low-memory, stream-based API for processing large JSON. - Asynchronous APIs: - `AsyncHttpAPI`: New public interface for non-blocking, high-concurrency HTTP calls. - `AsyncFileUtil`: Utility for non-blocking file I/O using virtual threads. - Performance Benchmarks: - Added JMH benchmarks for new JSON and HTTP connection pool optimizations. refactor(card): migrate card serialization from Gson to Jackson Replaced the entire card message serialization system, moving from Gson to Jackson for improved performance, flexibility, and robustness. - Fix: - Addresses a critical bug where empty modules (e.g., `DividerModule`) would fail to serialize. - Refactor: - Created `JacksonCardUtil` to centralize all card-related JSON logic. - Implemented custom Jackson serializers and deserializers for all card components. - Test: - Added extensive tests to verify the new Jackson-based serialization with complex examples. refactor(event): simplify event manager and remove monitoring code Streamlined `EventManagerImpl` by removing internal performance monitoring and reporting features. The core parallel event processing logic is retained. - chore(config): - Removed deprecated performance-related options from the `kbc.yml` configuration file. * i18n: localize log and cli messages to chinese - translate log outputs, exception messages, and command-line interface text to chinese - this change aims to improve the user experience for chinese-speaking users refactor(launch): remove redundant startup messages - remove warning messages about mixin support on launch to provide a cleaner console output * fix(serializer): enhance robustness of event deserializers - Improve null-safety in various event deserializers to prevent crashes from malformed API responses. - ChannelInfoUpdateEventDeserializer: - Add null checks for 'id' and 'type' fields, throwing an exception if 'id' is missing. - GuildUserNickNameUpdateEventDeserializer: - Add a null check for 'user_id' and default 'nickname' to an empty string. - Enable the Jackson deserialization path to leverage its improved null handling. - UserInfoUpdateEventDeserializer: - Accommodate inconsistent API field naming by checking for both 'user_id' and 'body_id'. - Ensure 'username' and 'avatar' updates are null-safe. - refactor(network): simplify and correct event processing logic - Change event handling in ListenerImpl to be synchronous, ensuring events are processed in the order they are received and preventing race conditions. - Simplify the logic for processing buffered out-of-order messages by replacing the iterator-based loop with a cleaner `while` loop and a dedicated `find()` helper method. - style: Remove trailing newline in Session.java. * fix: remove java home path in gradle.properties * Adapt LiteCommands API, selfProcessor -> self * build: Update gradle to 9.1.0 * build: gradle 9.14.x * feat: update line number * refactor(json): migrate from gson to jackson for all json processing This commit completes the full migration from GSON to Jackson for all JSON processing to improve performance, unify the JSON handling library, and simplify the codebase. Refactor: - Remove the GSON dependency entirely from the project. - Rewrite `GsonUtil` to be a compatibility layer that delegates all operations to Jackson. - Gut and deprecate `JsonEngineSelector` as it is no longer needed; Jackson is now the sole engine. - Update entity classes to use Jackson's `JsonNode` for updates, keeping GSON-based methods only for backward compatibility. Build: - Update `build.gradle.kts` to remove the GSON library. - Downgrade Gradle from 9.1 to 8.9. Test: - Update unit tests and performance benchmarks to reflect the removal of GSON and test the new Jackson-based implementation. BREAKING CHANGE: The `Updatable` interface now requires implementing `update(JsonNode data)` instead of the previous GSON-based `update(JsonObject data)`. All implementers must be updated. * feat(thread): implement thread channel feature - feat: Add full support for Kook Thread Channels (Type 4), enabling creation, viewing, and interaction with posts and replies. - feat: Introduce new entities: `ThreadChannel`, `ThreadPost`, `ThreadReply`, and `ThreadCategory`. - feat: Implement `ThreadPostIterator` and `ThreadReplyIterator` for efficient pagination of posts and replies. - feat: Register new API routes in `HttpAPIRoute` for all thread-related operations. - feat: Update `EntityBuilder` and `EntityBuildUtil` to correctly construct thread channel entities. * refactor(core): migrate to jackson and improve schedulers - refactor: Migrate the entire project from Gson to Jackson for JSON processing, improving performance, memory usage, and type safety. - refactor: Remove all deprecated Gson-based methods, especially in `PageIterator` and event deserializer classes. - refactor: Implement a robust Jackson-based event deserialization system using `JKookEventModule`, making it the primary method with Gson as a legacy fallback. - refactor: Refactor `VirtualThreadUtil` to use platform threads for `ScheduledExecutorService` cores, ensuring reliable and precise task scheduling as per best practices. - refactor: Convert `UserPermissionSaved` to use Jackson for serialization. - chore: Add `GsonCompat` utility class to offer a compatibility layer during the transition. * perf(pageiter): optimize entity fetching and pagination - perf: Enhance `PageIterator` implementations (e.g., `GuildUserListIterator`) to parse full entity data from list API responses, drastically reducing unnecessary HTTP calls. - perf: Improve `PageIteratorImpl` to correctly handle pagination for API endpoints that do not return a `meta` block, relying on the item count instead. * fix(scheduler): correct runTaskTimer argument order - fix: Correct the parameter order in `SchedulerImpl.runTaskTimer` by swapping `delay` and `period` to match the `ScheduledExecutorService#scheduleAtFixedRate` method signature. * build(deps): update jkook dependency - build: Update the JKook dependency to `io.github.snwcreations:jkook:0.54.2`. - build: Modify `build.gradle.kts` and `libs.versions.toml` to reflect the new Maven coordinates and version for the JKook library. * refactor(core)!: complete migration to jackson for json processing - remove the gson dependency and all related serializer/deserializer classes - the project now exclusively uses jackson for all json operations, simplifying the codebase and dependency tree - refactor eventfactory to remove gson fallback logic, relying solely on jackson for event deserialization - clean up entity and message classes by removing gson compatibility layers - update build configuration to remove the gson library BREAKING CHANGE: The Gson library has been completely removed from the project. All JSON serialization and deserialization is now handled exclusively by Jackson. Internal components relying on the old Gson-based infrastructure will no longer work. * perf(json, entity): optimize JSON object conversion and permission calculation - introduce JacksonUtil.convertFromGsonJsonObject for direct Gson to Jackson conversion - eliminate toString() serialization overhead in entity update methods and message builders by using direct conversion - refactor UserImpl#update by removing redundant internal synchronization - leverage virtual threads and parallel streams in UserImpl#calculateChannel for concurrent permission evaluation * docs(architecture): remove CLAUDE.md architecture document - the CLAUDE.md file, providing project architecture overview, is no longer needed - this document is being replaced by other, more up-to-date documentation sources * refactor(json)!: remove GSON and related JSON utilities - completely remove GSON library and its associated utility classes - remove GsonCompat, GsonUtil, and JsonEngineSelector as JSON processing is now fully handled by JacksonUtil - update all internal code usages to directly leverage JacksonUtil for JSON operations - remove deprecated JSON processing benchmarks - simplify the project's JSON processing stack BREAKING CHANGE: Direct usage of snw.kookbc.util.GsonCompat, snw.kookbc.util.GsonUtil, and snw.kookbc.util.JsonEngineSelector classes is no longer supported. All JSON processing now exclusively uses Jackson. * fix(ws): improve connection stability and smart reconnection - introduce `ReconnectStrategy` to manage reconnection attempts and delays - enhance error handling and logging across `Connector` and `WebSocketMessageProcessor` - refine `Reconnector` thread logic to handle states more robustly - increase WebSocket connection timeout for better resilience against network fluctuations * fix(websocket): enhance error logging for message processing - provide raw message content in error logs for uncompressed WebSocket messages - include decompressed message in error logs for compressed WebSocket messages - add byte size to error logs for WebSocket data decompression failures - simplify static imports for `JacksonUtil` in `ChannelImpl` - simplify `Collectors` import in `UserImpl` - simplify `UnknownHostException` usage in `WebSocketMessageProcessor` * build: bump version to 0.33.0 * feat(network): implement intelligent reconnection strategy - introduce `ReconnectStrategy` for robust WebSocket reconnection - use exponential backoff (1, 2, 4, 8, 16, 32, 60s) with infinite retry - differentiate recoverable (network) and unrecoverable (auth) exceptions - automatically reset retry counter after 5 minutes of stable connection - provide statistics for monitoring reconnection attempts * refactor(project): remove legacy test infrastructure and AI docs - delete all JUnit 5 tests, AssertJ assertions, Mockito mocks, and Testcontainers setups - remove JMH benchmarks for HTTP connection pool, system performance, and virtual threads - eliminate AI-generated architectural documentation (`CLAUDE.md` files) - update `build.gradle.kts` to remove test and benchmark dependencies/configurations - update `.gitignore` to include AI tool directories and generated documentation --------- Co-authored-by: huanmeng-qwq <1871735932@qq.com> Co-authored-by: ZX夏夜之风 --- .gitignore | 9 +- build.gradle.kts | 23 +- .../kotlin/publish-conventions.gradle.kts | 4 +- gradle/libs.versions.toml | 26 +- gradle/wrapper/gradle-wrapper.properties | 2 +- src/main/java/snw/kookbc/CLIOptions.java | 2 +- src/main/java/snw/kookbc/LaunchMain.java | 53 +- src/main/java/snw/kookbc/Main.java | 47 +- src/main/java/snw/kookbc/SharedConstants.java | 24 +- src/main/java/snw/kookbc/impl/CoreImpl.java | 4 +- .../java/snw/kookbc/impl/HttpAPIImpl.java | 362 +++++++++-- src/main/java/snw/kookbc/impl/KBCClient.java | 39 +- .../command/litecommands/KookScheduler.java | 108 ++-- .../command/litecommands/LiteKookFactory.java | 2 +- .../result/ResultAnnotationResolver.java | 2 +- .../litecommands/argument/EmojiArgument.java | 234 ++++---- .../litecommands/argument/RoleArgument.java | 228 +++---- .../kookbc/impl/entity/CustomEmojiImpl.java | 19 +- .../java/snw/kookbc/impl/entity/GameImpl.java | 18 +- .../snw/kookbc/impl/entity/GuildImpl.java | 69 +-- .../java/snw/kookbc/impl/entity/RoleImpl.java | 32 +- .../java/snw/kookbc/impl/entity/UserImpl.java | 121 ++-- .../impl/entity/builder/CardBuilder.java | 139 ++++- .../impl/entity/builder/EntityBuildUtil.java | 124 +++- .../impl/entity/builder/EntityBuilder.java | 252 ++++++-- .../impl/entity/builder/MessageBuilder.java | 166 +++-- .../impl/entity/channel/CategoryImpl.java | 15 +- .../impl/entity/channel/ChannelImpl.java | 29 +- .../channel/NonCategoryChannelImpl.java | 17 +- .../impl/entity/channel/TextChannelImpl.java | 14 +- .../entity/channel/ThreadChannelImpl.java | 232 +++++++ .../impl/entity/channel/VoiceChannelImpl.java | 74 ++- .../entity/thread/ThreadCategoryImpl.java | 125 ++++ .../impl/entity/thread/ThreadPostImpl.java | 297 +++++++++ .../impl/entity/thread/ThreadReplyImpl.java | 225 +++++++ .../snw/kookbc/impl/event/EventFactory.java | 159 +++-- .../kookbc/impl/event/EventManagerImpl.java | 105 +++- .../internal/UserClickButtonListener.java | 12 +- .../kookbc/impl/launch/LaunchClassLoader.java | 10 +- .../impl/message/ChannelMessageImpl.java | 13 +- .../snw/kookbc/impl/message/MessageImpl.java | 13 +- .../impl/message/PrivateMessageImpl.java | 20 +- .../kookbc/impl/mixin/MixinServiceKookBC.java | 55 +- .../java/snw/kookbc/impl/network/Bucket.java | 8 + .../java/snw/kookbc/impl/network/Frame.java | 8 +- .../snw/kookbc/impl/network/HttpAPIRoute.java | 12 +- .../impl/network/IgnoreSNListenerImpl.java | 4 +- .../snw/kookbc/impl/network/ListenerImpl.java | 78 +-- .../kookbc/impl/network/NetworkClient.java | 218 ++++++- .../java/snw/kookbc/impl/network/Session.java | 2 +- .../impl/network/webhook/EncryptUtils.java | 5 +- .../impl/network/webhook/JLHttpRequest.java | 13 +- .../network/webhook/JLHttpRequestHandler.java | 49 +- .../network/webhook/JLHttpRequestWrapper.java | 6 +- .../network/webhook/JLHttpWebhookServer.java | 10 +- .../snw/kookbc/impl/network/ws/Connector.java | 114 +++- .../impl/network/ws/ReconnectStrategy.java | 361 +++++++++++ .../kookbc/impl/network/ws/Reconnector.java | 29 +- .../network/ws/WebSocketMessageProcessor.java | 91 ++- .../pageiter/ChannelInvitationIterator.java | 23 +- .../impl/pageiter/ChannelMessageIterator.java | 33 +- .../kookbc/impl/pageiter/GameIterator.java | 19 +- .../pageiter/GuildBannedUserIterator.java | 14 +- .../pageiter/GuildChannelListIterator.java | 14 +- .../impl/pageiter/GuildEmojiListIterator.java | 22 +- .../pageiter/GuildInvitationsIterator.java | 23 +- .../impl/pageiter/GuildRoleListIterator.java | 22 +- .../impl/pageiter/GuildUserListIterator.java | 16 +- .../impl/pageiter/JoinedGuildIterator.java | 24 +- .../pageiter/JoinedVoiceChannelsIterator.java | 15 +- .../impl/pageiter/PageIteratorImpl.java | 53 +- .../impl/pageiter/ThreadPostIterator.java | 82 +++ .../impl/pageiter/ThreadReplyIterator.java | 66 ++ .../UserJoinedVoiceChannelIterator.java | 12 +- .../impl/permissions/UserPermissionSaved.java | 29 +- .../impl/plugin/MixinPluginManager.java | 2 +- .../impl/plugin/SimplePluginManager.java | 242 +++++++- .../kookbc/impl/scheduler/SchedulerImpl.java | 13 +- .../component/TemplateMessageSerializer.java | 17 - .../card/CardComponentSerializer.java | 93 --- .../card/MultipleCardComponentSerializer.java | 41 -- .../card/element/ButtonElementSerializer.java | 76 --- .../element/ContentElementSerializer.java | 53 -- .../card/element/ImageElementSerializer.java | 51 -- .../module/ActionGroupModuleSerializer.java | 54 -- .../module/ContainerModuleSerializer.java | 49 -- .../card/module/ContextModuleSerializer.java | 74 --- .../module/CountdownModuleSerializer.java | 49 -- .../card/module/DividerModuleSerializer.java | 45 -- .../card/module/FileModuleSerializer.java | 55 -- .../card/module/HeaderModuleSerializer.java | 46 -- .../module/ImageGroupModuleSerializer.java | 44 -- .../card/module/InviteModuleSerializer.java | 42 -- .../card/module/SectionModuleSerializer.java | 87 --- .../card/structure/ParagraphSerializer.java | 67 --- .../JacksonTemplateMessageDeserializer.java | 60 ++ .../JacksonCardComponentDeserializer.java | 146 +++++ ...ksonMultipleCardComponentDeserializer.java | 77 +++ .../JacksonBaseElementDeserializer.java | 99 +++ .../JacksonButtonElementDeserializer.java | 124 ++++ .../JacksonButtonElementSerializer.java | 62 ++ .../JacksonContentElementDeserializer.java | 116 ++++ .../JacksonImageElementDeserializer.java | 90 +++ .../JacksonMarkdownElementSerializer.java | 46 ++ .../JacksonPlainTextElementSerializer.java | 49 ++ .../JacksonActionGroupModuleDeserializer.java | 66 ++ .../JacksonActionGroupModuleSerializer.java | 47 ++ .../module/JacksonBaseModuleDeserializer.java | 113 ++++ .../JacksonContainerModuleDeserializer.java | 64 ++ .../JacksonContextModuleDeserializer.java | 67 +++ .../JacksonContextModuleSerializer.java | 47 ++ .../JacksonCountdownModuleDeserializer.java | 30 + .../JacksonDividerModuleDeserializer.java | 41 ++ .../JacksonDividerModuleSerializer.java | 40 ++ .../module/JacksonFileModuleDeserializer.java | 32 + .../JacksonHeaderModuleDeserializer.java | 37 ++ .../module/JacksonHeaderModuleSerializer.java | 45 ++ .../JacksonImageGroupModuleDeserializer.java | 45 ++ .../JacksonInviteModuleDeserializer.java | 26 + .../JacksonSectionModuleDeserializer.java | 133 +++++ .../JacksonSectionModuleSerializer.java | 58 ++ .../JacksonParagraphDeserializer.java | 72 +++ .../event/BaseEventDeserializer.java | 48 -- .../event/NormalEventDeserializer.java | 49 -- .../guild/GuildBanUserEventDeserializer.java | 63 -- .../item/ItemConsumedEventDeserializer.java | 58 -- .../jackson/BaseJacksonEventDeserializer.java | 98 +++ .../event/jackson/JKookEventModule.java | 130 ++++ ...hannelCreateEventJacksonDeserializer.java} | 27 +- ...hannelDeleteEventJacksonDeserializer.java} | 33 +- ...elInfoUpdateEventJacksonDeserializer.java} | 44 +- ...essageDeleteEventJacksonDeserializer.java} | 34 +- ...annelMessageEventJacksonDeserializer.java} | 26 +- ...elMessagePinEventJacksonDeserializer.java} | 38 +- ...MessageUnpinEventJacksonDeserializer.java} | 38 +- ...essageUpdateEventJacksonDeserializer.java} | 37 +- ...uildAddEmojiEventJacksonDeserializer.java} | 27 +- .../GuildBanUserEventJacksonDeserializer.java | 66 ++ .../GuildDeleteEventJacksonDeserializer.java} | 31 +- ...ldInfoUpdateEventJacksonDeserializer.java} | 31 +- ...dRemoveEmojiEventJacksonDeserializer.java} | 30 +- ...ildUnbanUserEventJacksonDeserializer.java} | 59 +- ...dUpdateEmojiEventJacksonDeserializer.java} | 31 +- ...ckNameUpdateEventJacksonDeserializer.java} | 53 +- .../ItemConsumedEventJacksonDeserializer.java | 51 ++ ...essageDeleteEventJacksonDeserializer.java} | 31 +- ...sageReceivedEventJacksonDeserializer.java} | 26 +- ...essageUpdateEventJacksonDeserializer.java} | 33 +- .../RoleCreateEventJacksonDeserializer.java} | 31 +- .../RoleDeleteEventJacksonDeserializer.java} | 34 +- ...leInfoUpdateEventJacksonDeserializer.java} | 32 +- ...rAddReactionEventJacksonDeserializer.java} | 41 +- ...rClickButtonEventJacksonDeserializer.java} | 40 +- ...serInfoUpdateEventJacksonDeserializer.java | 64 ++ ...serJoinGuildEventJacksonDeserializer.java} | 41 +- ...VoiceChannelEventJacksonDeserializer.java} | 33 +- ...erLeaveGuildEventJacksonDeserializer.java} | 39 +- ...VoiceChannelEventJacksonDeserializer.java} | 33 +- .../UserOfflineEventJacksonDeserializer.java} | 31 +- .../UserOnlineEventJacksonDeserializer.java} | 31 +- ...moveReactionEventJacksonDeserializer.java} | 38 +- .../user/UserInfoUpdateEventDeserializer.java | 49 -- .../kookbc/impl/storage/EntityStorage.java | 250 ++++++++ .../impl/tasks/BotMarketPingThread.java | 23 +- .../snw/kookbc/impl/tasks/UpdateChecker.java | 52 +- .../snw/kookbc/interfaces/AsyncHttpAPI.java | 261 ++++++++ .../java/snw/kookbc/interfaces/Updatable.java | 6 +- .../network/webhook/WebhookServer.java | 4 +- .../test/JacksonCardSerializationTest.java | 78 +++ .../java/snw/kookbc/test/JacksonCardTest.java | 107 ++++ .../java/snw/kookbc/util/AsyncFileUtil.java | 313 ++++++++++ src/main/java/snw/kookbc/util/GsonUtil.java | 141 ----- .../java/snw/kookbc/util/JacksonCardUtil.java | 225 +++++++ .../java/snw/kookbc/util/JacksonUtil.java | 347 +++++++++++ .../snw/kookbc/util/JsonCacheManager.java | 447 ++++++++++++++ .../snw/kookbc/util/JsonStreamProcessor.java | 565 ++++++++++++++++++ .../snw/kookbc/util/VirtualThreadUtil.java | 492 +++++++++++++++ src/main/resources/kbc.yml | 2 +- 178 files changed, 9989 insertions(+), 3084 deletions(-) create mode 100644 src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java create mode 100644 src/main/java/snw/kookbc/impl/entity/thread/ThreadCategoryImpl.java create mode 100644 src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java create mode 100644 src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java create mode 100644 src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java create mode 100644 src/main/java/snw/kookbc/impl/pageiter/ThreadPostIterator.java create mode 100644 src/main/java/snw/kookbc/impl/pageiter/ThreadReplyIterator.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/TemplateMessageSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/CardComponentSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/MultipleCardComponentSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/element/ButtonElementSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/element/ContentElementSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/element/ImageElementSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/module/ActionGroupModuleSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/module/ContainerModuleSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/module/ContextModuleSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/module/CountdownModuleSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/module/DividerModuleSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/module/FileModuleSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/module/HeaderModuleSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/module/ImageGroupModuleSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/module/InviteModuleSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/module/SectionModuleSerializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/component/card/structure/ParagraphSerializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/JacksonTemplateMessageDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/JacksonCardComponentDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/JacksonMultipleCardComponentDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonBaseElementDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonButtonElementDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonButtonElementSerializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonContentElementDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonImageElementDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonMarkdownElementSerializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/element/JacksonPlainTextElementSerializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonActionGroupModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonActionGroupModuleSerializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonBaseModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContainerModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContextModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonContextModuleSerializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonCountdownModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonDividerModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonDividerModuleSerializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonFileModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonHeaderModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonHeaderModuleSerializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonImageGroupModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonInviteModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonSectionModuleDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/module/JacksonSectionModuleSerializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/component/jackson/card/structure/JacksonParagraphDeserializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/event/BaseEventDeserializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/event/NormalEventDeserializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/event/guild/GuildBanUserEventDeserializer.java delete mode 100644 src/main/java/snw/kookbc/impl/serializer/event/item/ItemConsumedEventDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/event/jackson/BaseJacksonEventDeserializer.java create mode 100644 src/main/java/snw/kookbc/impl/serializer/event/jackson/JKookEventModule.java rename src/main/java/snw/kookbc/impl/serializer/event/{channel/ChannelCreateEventDeserializer.java => jackson/channel/ChannelCreateEventJacksonDeserializer.java} (68%) rename src/main/java/snw/kookbc/impl/serializer/event/{channel/ChannelDeleteEventDeserializer.java => jackson/channel/ChannelDeleteEventJacksonDeserializer.java} (60%) rename src/main/java/snw/kookbc/impl/serializer/event/{channel/ChannelInfoUpdateEventDeserializer.java => jackson/channel/ChannelInfoUpdateEventJacksonDeserializer.java} (53%) rename src/main/java/snw/kookbc/impl/serializer/event/{channel/ChannelMessageDeleteEventDeserializer.java => jackson/channel/ChannelMessageDeleteEventJacksonDeserializer.java} (58%) rename src/main/java/snw/kookbc/impl/serializer/event/{channel/ChannelMessageEventDeserializer.java => jackson/channel/ChannelMessageEventJacksonDeserializer.java} (70%) rename src/main/java/snw/kookbc/impl/serializer/event/{channel/ChannelMessagePinEventDeserializer.java => jackson/channel/ChannelMessagePinEventJacksonDeserializer.java} (57%) rename src/main/java/snw/kookbc/impl/serializer/event/{channel/ChannelMessageUnpinEventDeserializer.java => jackson/channel/ChannelMessageUnpinEventJacksonDeserializer.java} (57%) rename src/main/java/snw/kookbc/impl/serializer/event/{channel/ChannelMessageUpdateEventDeserializer.java => jackson/channel/ChannelMessageUpdateEventJacksonDeserializer.java} (64%) rename src/main/java/snw/kookbc/impl/serializer/event/{guild/GuildAddEmojiEventDeserializer.java => jackson/guild/GuildAddEmojiEventJacksonDeserializer.java} (67%) create mode 100644 src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildBanUserEventJacksonDeserializer.java rename src/main/java/snw/kookbc/impl/serializer/event/{guild/GuildDeleteEventDeserializer.java => jackson/guild/GuildDeleteEventJacksonDeserializer.java} (60%) rename src/main/java/snw/kookbc/impl/serializer/event/{guild/GuildInfoUpdateEventDeserializer.java => jackson/guild/GuildInfoUpdateEventJacksonDeserializer.java} (60%) rename src/main/java/snw/kookbc/impl/serializer/event/{guild/GuildRemoveEmojiEventDeserializer.java => jackson/guild/GuildRemoveEmojiEventJacksonDeserializer.java} (66%) rename src/main/java/snw/kookbc/impl/serializer/event/{guild/GuildUnbanUserEventDeserializer.java => jackson/guild/GuildUnbanUserEventJacksonDeserializer.java} (53%) rename src/main/java/snw/kookbc/impl/serializer/event/{guild/GuildUpdateEmojiEventDeserializer.java => jackson/guild/GuildUpdateEmojiEventJacksonDeserializer.java} (65%) rename src/main/java/snw/kookbc/impl/serializer/event/{guild/GuildUserNickNameUpdateEventDeserializer.java => jackson/guild/GuildUserNickNameUpdateEventJacksonDeserializer.java} (50%) create mode 100644 src/main/java/snw/kookbc/impl/serializer/event/jackson/item/ItemConsumedEventJacksonDeserializer.java rename src/main/java/snw/kookbc/impl/serializer/event/{pm/PrivateMessageDeleteEventDeserializer.java => jackson/pm/PrivateMessageDeleteEventJacksonDeserializer.java} (61%) rename src/main/java/snw/kookbc/impl/serializer/event/{pm/PrivateMessageReceivedEventDeserializer.java => jackson/pm/PrivateMessageReceivedEventJacksonDeserializer.java} (69%) rename src/main/java/snw/kookbc/impl/serializer/event/{pm/PrivateMessageUpdateEventDeserializer.java => jackson/pm/PrivateMessageUpdateEventJacksonDeserializer.java} (56%) rename src/main/java/snw/kookbc/impl/serializer/event/{role/RoleCreateEventDeserializer.java => jackson/role/RoleCreateEventJacksonDeserializer.java} (65%) rename src/main/java/snw/kookbc/impl/serializer/event/{role/RoleDeleteEventDeserializer.java => jackson/role/RoleDeleteEventJacksonDeserializer.java} (60%) rename src/main/java/snw/kookbc/impl/serializer/event/{role/RoleInfoUpdateEventDeserializer.java => jackson/role/RoleInfoUpdateEventJacksonDeserializer.java} (61%) rename src/main/java/snw/kookbc/impl/serializer/event/{user/UserAddReactionEventDeserializer.java => jackson/user/UserAddReactionEventJacksonDeserializer.java} (55%) rename src/main/java/snw/kookbc/impl/serializer/event/{user/UserClickButtonEventDeserializer.java => jackson/user/UserClickButtonEventJacksonDeserializer.java} (61%) create mode 100644 src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserInfoUpdateEventJacksonDeserializer.java rename src/main/java/snw/kookbc/impl/serializer/event/{user/UserJoinGuildEventDeserializer.java => jackson/user/UserJoinGuildEventJacksonDeserializer.java} (56%) rename src/main/java/snw/kookbc/impl/serializer/event/{user/UserJoinVoiceChannelEventDeserializer.java => jackson/user/UserJoinVoiceChannelEventJacksonDeserializer.java} (60%) rename src/main/java/snw/kookbc/impl/serializer/event/{user/UserLeaveGuildEventDeserializer.java => jackson/user/UserLeaveGuildEventJacksonDeserializer.java} (60%) rename src/main/java/snw/kookbc/impl/serializer/event/{user/UserLeaveVoiceChannelEventDeserializer.java => jackson/user/UserLeaveVoiceChannelEventJacksonDeserializer.java} (60%) rename src/main/java/snw/kookbc/impl/serializer/event/{user/UserOfflineEventDeserializer.java => jackson/user/UserOfflineEventJacksonDeserializer.java} (59%) rename src/main/java/snw/kookbc/impl/serializer/event/{user/UserOnlineEventDeserializer.java => jackson/user/UserOnlineEventJacksonDeserializer.java} (59%) rename src/main/java/snw/kookbc/impl/serializer/event/{user/UserRemoveReactionEventDeserializer.java => jackson/user/UserRemoveReactionEventJacksonDeserializer.java} (61%) delete mode 100644 src/main/java/snw/kookbc/impl/serializer/event/user/UserInfoUpdateEventDeserializer.java create mode 100644 src/main/java/snw/kookbc/interfaces/AsyncHttpAPI.java create mode 100644 src/main/java/snw/kookbc/test/JacksonCardSerializationTest.java create mode 100644 src/main/java/snw/kookbc/test/JacksonCardTest.java create mode 100644 src/main/java/snw/kookbc/util/AsyncFileUtil.java delete mode 100644 src/main/java/snw/kookbc/util/GsonUtil.java create mode 100644 src/main/java/snw/kookbc/util/JacksonCardUtil.java create mode 100644 src/main/java/snw/kookbc/util/JacksonUtil.java create mode 100644 src/main/java/snw/kookbc/util/JsonCacheManager.java create mode 100644 src/main/java/snw/kookbc/util/JsonStreamProcessor.java create mode 100644 src/main/java/snw/kookbc/util/VirtualThreadUtil.java 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/build.gradle.kts b/build.gradle.kts index 2b7525da..23af08c7 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) @@ -37,7 +37,11 @@ dependencies { shadowApi(libs.net.kyori.event.api) shadowApi(libs.net.kyori.event.method) shadowApi(libs.net.freeutils.jlhttp) - shadowApi(libs.com.google.code.gson.gson) + // GSON 已移除 - 项目已完全迁移到 Jackson (v0.52.0+) + 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) @@ -47,8 +51,8 @@ dependencies { group = "io.github.snwcreations" version = "0.32.2" 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..7adbf530 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,7 @@ [versions] com-github-ben-manes-caffeine-caffeine = "2.9.3" -com-github-snwcreations-jkook = "0.54.1" +io-github-snwcreations-jkook = "0.54.2" com-github-snwcreations-terminalconsoleappender = "1.3.5" -com-google-code-gson-gson = "2.10.1" com-squareup-okhttp3-okhttp = "4.10.0" dev-rollczi-litecommands-framework = "3.9.5" net-bytebuddy-byte-buddy = "1.12.10" @@ -18,12 +17,18 @@ 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" } +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-google-code-gson-gson = { module = "com.google.code.gson:gson", version.ref = "com-google-code-gson-gson" } 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 +44,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..6c49c86c 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..77a4dbf7 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); @@ -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"); enablePlugins(); - getCore().getLogger().info("Running delayed init tasks"); + getCore().getLogger().info("正在运行延迟初始化任务"); ((SchedulerImpl) core.getScheduler()).runAfterPluginInitTasks(); getCore().getLogger().debug("Starting Network"); 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 @@ -356,7 +357,7 @@ 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) { @@ -453,15 +454,15 @@ public synchronized void shutdown() { } 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(); 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/entity/CustomEmojiImpl.java b/src/main/java/snw/kookbc/impl/entity/CustomEmojiImpl.java index d8e42801..7daf5ca5 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; @@ -83,9 +85,16 @@ public void setName0(String name) { this.name = name; } + // GSON compatibility method + public synchronized void update(com.google.gson.JsonObject data) { + update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); + } + + // ===== Jackson API - 高性能版本 ===== + @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..dc94400c 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; @@ -97,9 +98,16 @@ public void setNameAndIcon(@NotNull String name, @NotNull String icon) { this.icon = icon; } + // GSON compatibility method + public synchronized void update(com.google.gson.JsonObject data) { + update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); + } + + // ===== Jackson API - 高性能版本 ===== + @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..86f8b026 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,8 +250,8 @@ public Role createRole(String s) { .put("guild_id", getId()) .put("name", s) .build(); - JsonObject res = client.getNetworkClient().post(HttpAPIRoute.ROLE_CREATE.toFullURL(), body); - Role result = client.getEntityBuilder().buildRole(this, res); + JsonNode res = client.getNetworkClient().post(HttpAPIRoute.ROLE_CREATE.toFullURL(), body); + Role result = client.getEntityBuilder().buildRole(this, snw.kookbc.util.JacksonUtil.convertToGsonJsonObject(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 = snw.kookbc.util.JacksonUtil.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 snw.kookbc.util.JacksonUtil.get(object, "url").asText(); } @Override @@ -369,18 +365,23 @@ public void setAvatar(String avatarUrl) { this.avatarUrl = avatarUrl; } + // GSON compatibility method - marked for deprecation + public synchronized void updateFromGson(com.google.gson.JsonObject data) { + update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); + } + @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 +391,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..23a21c34 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; @@ -163,14 +166,21 @@ public void setMentionable0(boolean mentionable) { this.mentionable = mentionable; } + // GSON compatibility method + public synchronized void update(com.google.gson.JsonObject data) { + update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); + } + + // ===== Jackson API - 高性能版本 ===== + @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..c45c8940 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)); } @@ -346,18 +344,39 @@ public void setVipAvatarUrl(String vipAvatarUrl) { this.vipAvatarUrl = vipAvatarUrl; } + // GSON compatibility method + public void update(com.google.gson.JsonObject data) { + // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 + update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); + } + @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 +387,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 +424,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("Error occurred while calculating built-in permissions", e); + return false; + } catch (Exception e) { + client.getCore().getLogger().error("Error occurred while calculating built-in permissions", 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..9dd36cbd 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,147 @@ 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."); + + return JacksonCardUtil.fromJson(objectNode, CardComponent.class); + } + + /** + * 从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"); + } } - public static JsonArray serialize(CardComponent component) { - JsonArray result = new JsonArray(); + // ===== Gson兼容方法(向后兼容)===== + + /** + * 从Gson JsonArray构建多卡片组件(向后兼容) + * @param array Gson JsonArray + * @return MultipleCardComponent + */ + public static MultipleCardComponent buildCard(com.google.gson.JsonArray array) { + // 转换Gson JsonArray到Jackson JsonNode + JsonNode arrayNode = JacksonCardUtil.parse(array.toString()); + return buildCardArray(arrayNode); + } + + /** + * 从Gson JsonObject构建单个卡片组件(向后兼容) + * @param object Gson JsonObject + * @return CardComponent + */ + public static CardComponent buildCard(com.google.gson.JsonObject object) { + // 转换Gson JsonObject到Jackson JsonNode + JsonNode objectNode = JacksonCardUtil.parse(object.toString()); + return buildCardObject(objectNode); + } + + // ===== 序列化方法 ===== + + /** + * 序列化单个卡片组件为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); + } + + /** + * 序列化单个卡片组件为Gson JsonArray(向后兼容) + * @param component CardComponent + * @return JsonArray + */ + public static com.google.gson.JsonArray serialize(CardComponent component) { + com.google.gson.JsonArray result = new com.google.gson.JsonArray(); result.add(serialize0(component)); return result; } - public static JsonArray serialize(MultipleCardComponent component) { - JsonArray array = new JsonArray(); + /** + * 序列化多卡片组件为Gson JsonArray(向后兼容) + * @param component MultipleCardComponent + * @return JsonArray + */ + public static com.google.gson.JsonArray serialize(MultipleCardComponent component) { + com.google.gson.JsonArray array = new com.google.gson.JsonArray(); for (CardComponent card : component.getComponents()) { array.add(serialize0(card)); } return array; } - public static JsonObject serialize0(CardComponent component) { - return GsonUtil.CARD_GSON.toJsonTree(component).getAsJsonObject(); + /** + * 序列化单个卡片组件为Gson JsonObject(向后兼容) + * @param component CardComponent + * @return JsonObject + */ + public static com.google.gson.JsonObject serialize0(CardComponent component) { + String json = JacksonCardUtil.toJson(component); + return new com.google.gson.JsonParser().parse(json).getAsJsonObject(); } - } 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..4a6e49fd 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; @@ -38,15 +34,18 @@ 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(); + // ===== Gson兼容方法(向后兼容)===== + + public static Collection parseRPO(com.google.gson.JsonObject object) { + com.google.gson.JsonArray array = object.getAsJsonArray("permission_overwrites"); Collection rpo = new ConcurrentLinkedQueue<>(); - for (JsonElement element : array) { - JsonObject orpo = element.getAsJsonObject(); + for (com.google.gson.JsonElement element : array) { + com.google.gson.JsonObject orpo = element.getAsJsonObject(); rpo.add( new Channel.RolePermissionOverwrite( orpo.get("role_id").getAsInt(), @@ -56,12 +55,12 @@ public static Collection parseRPO(JsonObject ob return rpo; } - public static Collection parseUPO(KBCClient client, JsonObject object) { - JsonArray array = get(object, "permission_users").getAsJsonArray(); + public static Collection parseUPO(KBCClient client, com.google.gson.JsonObject object) { + com.google.gson.JsonArray array = object.getAsJsonArray("permission_users"); Collection upo = new ConcurrentLinkedQueue<>(); - for (JsonElement element : array) { - JsonObject oupo = element.getAsJsonObject(); - JsonObject rawUser = oupo.getAsJsonObject("user"); + for (com.google.gson.JsonElement element : array) { + com.google.gson.JsonObject oupo = element.getAsJsonObject(); + com.google.gson.JsonObject rawUser = oupo.getAsJsonObject("user"); upo.add( new Channel.UserPermissionOverwrite( client.getStorage().getUser(rawUser.get("id").getAsString(), rawUser), @@ -71,9 +70,9 @@ public static Collection parseUPO(KBCClient cli return upo; } - public static NotifyType parseNotifyType(JsonObject object) { + public static NotifyType parseNotifyType(com.google.gson.JsonObject object) { Guild.NotifyType type = null; - int rawNotifyType = getAsInt(object, "notify_type"); + int rawNotifyType = object.get("notify_type").getAsInt(); for (Guild.NotifyType value : Guild.NotifyType.values()) { if (value.getValue() == rawNotifyType) { type = value; @@ -84,7 +83,7 @@ public static NotifyType parseNotifyType(JsonObject object) { return type; } - public static Guild parseEmojiGuild(String id, KBCClient client, JsonObject object) { + public static Guild parseEmojiGuild(String id, KBCClient client, com.google.gson.JsonObject object) { Guild guild = null; if (id.contains("/")) { guild = client.getStorage().getGuild(id.substring(0, id.indexOf("/"))); @@ -101,9 +100,98 @@ public static Channel parseChannel(KBCClient client, String id, int type) { 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 = snw.kookbc.util.JacksonUtil.getArrayOrNull(node, "permission_overwrites"); + Collection rpo = new ConcurrentLinkedQueue<>(); + + if (array != null && array.isArray()) { + for (JsonNode element : array) { + if (element != null && element.isObject()) { + int roleId = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "role_id", 0); + int allow = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "allow", 0); + int deny = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "deny", 0); + + rpo.add(new Channel.RolePermissionOverwrite(roleId, allow, deny)); + } + } + } + return rpo; + } + + /** + * 解析用户权限覆写 (Jackson版本,安全处理不完整JSON) + */ + public static Collection parseUPO(KBCClient client, JsonNode node) { + JsonNode array = snw.kookbc.util.JacksonUtil.getArrayOrNull(node, "permission_users"); + Collection upo = new ConcurrentLinkedQueue<>(); + + if (array != null && array.isArray()) { + for (JsonNode element : array) { + if (element != null && element.isObject()) { + JsonNode rawUser = snw.kookbc.util.JacksonUtil.getObjectOrNull(element, "user"); + if (rawUser != null) { + String userId = snw.kookbc.util.JacksonUtil.getRequiredString(rawUser, "id"); + int allow = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "allow", 0); + int deny = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "deny", 0); + + // 临时桥接到Gson JsonObject + com.google.gson.JsonObject gsonUser = convertToGsonJsonObject(rawUser); + upo.add(new Channel.UserPermissionOverwrite( + client.getStorage().getUser(userId, gsonUser), + allow, deny)); + } + } + } + } + return upo; + } + + /** + * 解析通知类型 (Jackson版本,安全处理不完整JSON) + */ + public static NotifyType parseNotifyType(JsonNode node) { + int rawNotifyType = snw.kookbc.util.JacksonUtil.getIntOrDefault(node, "notify_type", 0); + + for (Guild.NotifyType value : Guild.NotifyType.values()) { + if (value.getValue() == rawNotifyType) { + return value; + } + } + + // 如果找不到匹配的通知类型,使用默认值而不是抛出异常 + return Guild.NotifyType.values()[0]; // 使用第一个枚举值作为默认 + } + + /** + * 解析表情所属服务器 (Jackson版本,安全处理不完整JSON) + */ + public static Guild parseEmojiGuild(String id, KBCClient client, JsonNode node) { + Guild guild = null; + if (id != null && id.contains("/")) { + String guildId = id.substring(0, id.indexOf("/")); + if (!guildId.isEmpty()) { + guild = client.getStorage().getGuild(guildId); + } + } + return guild; + } + + // 临时桥接方法 - 将JsonNode转换为JsonObject供现有代码使用 + private static com.google.gson.JsonObject convertToGsonJsonObject(JsonNode jacksonNode) { + return snw.kookbc.util.JacksonUtil.convertToGsonJsonObject(jacksonNode); + } + +} \ 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..2f8689c8 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,228 @@ 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"); + /** + * 构建User对象 (Gson兼容版本) + * 该方法保留用于向后兼容,内部委托给Jackson版本 + */ + public User buildUser(com.google.gson.JsonObject s) { + JsonNode node = convertFromGsonJsonObject(s); + return buildUser(node); + } + + /** + * 构建Guild对象 (Gson兼容版本) + * 该方法保留用于向后兼容,内部委托给Jackson版本 + */ + public Guild buildGuild(com.google.gson.JsonObject object) { + JsonNode node = convertFromGsonJsonObject(object); + return buildGuild(node); + } + + /** + * 构建Channel对象 (Gson兼容版本) + * 该方法保留用于向后兼容,内部委托给Jackson版本 + */ + public Channel buildChannel(com.google.gson.JsonObject object) { + JsonNode node = convertFromGsonJsonObject(object); + return buildChannel(node); + } + + /** + * 构建Role对象 (Gson兼容版本) + * 该方法保留用于向后兼容,内部委托给Jackson版本 + */ + public Role buildRole(Guild guild, com.google.gson.JsonObject object) { + JsonNode node = convertFromGsonJsonObject(object); + return buildRole(guild, node); + } + + /** + * 构建CustomEmoji对象 (Gson兼容版本) + * 该方法保留用于向后兼容,内部委托给Jackson版本 + */ + public CustomEmoji buildEmoji(com.google.gson.JsonObject object) { + JsonNode node = convertFromGsonJsonObject(object); + return buildEmoji(node); + } + + /** + * 构建Game对象 (Gson兼容版本) + * 该方法保留用于向后兼容,内部委托给Jackson版本 + */ + public Game buildGame(com.google.gson.JsonObject object) { + JsonNode node = convertFromGsonJsonObject(object); + return buildGame(node); + } + + // ===== 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..23a08f99 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; @@ -39,7 +40,7 @@ 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 +59,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,43 +86,24 @@ 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"); - final User author = getAuthor(extra); - final long timeStamp = getAsLong(object, "msg_timestamp"); - final Message quote = getQuote(extra); - return new PrivateMessageImpl(client, id, author, buildComponent(object), timeStamp, quote); + public PrivateMessage buildPrivateMessage(com.google.gson.JsonObject object) { + // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 + return buildPrivateMessage(JacksonUtil.convertFromGsonJsonObject(object)); } - public ChannelMessage buildChannelMessage(JsonObject object) { - final String id = getAsString(object, "msg_id"); - final JsonObject extra = getAsJsonObject(object, "extra"); - final User author = getAuthor(extra); - final long timeStamp = getAsLong(object, "msg_timestamp"); - 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); + public ChannelMessage buildChannelMessage(com.google.gson.JsonObject object) { + // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 + return buildChannelMessage(JacksonUtil.convertFromGsonJsonObject(object)); } - 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(com.google.gson.JsonObject extra) { + // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 + return getAuthor(JacksonUtil.convertFromGsonJsonObject(extra)); } - private Message getQuote(JsonObject extra) { - try { - final String quoteId = getAsString(getAsJsonObject(extra, "quote"), "rong_id"); - Message quote = client.getStorage().getMessage(quoteId); - if (quote == null) { - quote = client.getCore().getHttpAPI().getChannelMessage(quoteId); - } - return quote; - } catch (NoSuchElementException e) { - return null; - } + private Message getQuote(com.google.gson.JsonObject extra) { + // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 + return getQuote(JacksonUtil.convertFromGsonJsonObject(extra)); } private ChannelMessageImpl buildMessage(String id, User author, BaseComponent component, long timeStamp, @@ -136,31 +118,82 @@ private ChannelMessageImpl buildMessage(String id, User author, BaseComponent co throw new RuntimeException("We can not found channel type: " + channelType); } - public Message buildQuote(JsonObject object) { + public Message buildQuote(com.google.gson.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); + // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 + return buildQuote(JacksonUtil.convertFromGsonJsonObject(object)); + } + + public BaseComponent buildComponent(com.google.gson.JsonObject object) { + // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 + return buildComponent(JacksonUtil.convertFromGsonJsonObject(object)); } - public BaseComponent buildComponent(JsonObject object) { + // ===== 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 = JacksonUtil.get(node, "msg_timestamp").asLong(); + final Message quote = getQuote(extra); + return new PrivateMessageImpl(client, id, author, buildComponent(node), timeStamp, quote); + } + + 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 = JacksonUtil.get(node, "msg_timestamp").asLong(); + final Message quote = getQuote(extra); + 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(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(JsonNode extra) { + try { + 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 (Exception e) { + return null; + } + } + + 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 +202,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 +221,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 +237,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..e2c4eae0 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,13 @@ package snw.kookbc.impl.entity.channel; -import static snw.kookbc.util.GsonUtil.getAsJsonArray; +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 +47,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))); + snw.kookbc.util.JacksonUtil.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..105310d6 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; @@ -39,8 +39,9 @@ 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.convertFromGsonJsonObject; +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,14 +320,18 @@ 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")); - this.rpo = parseRPO(data); - this.upo = parseUPO(client, data); + // GSON compatibility method + public synchronized void update(com.google.gson.JsonObject data) { + update(convertFromGsonJsonObject(data)); + } + + public synchronized void update(JsonNode data) { + isTrue(Objects.equals(getId(), snw.kookbc.util.JacksonUtil.get(data, "id").asText()), "You can't update channel by using different data"); + this.name = snw.kookbc.util.JacksonUtil.get(data, "name").asText(); + this.permSync = snw.kookbc.util.JacksonUtil.get(data, "permission_sync").asInt() != 0; + this.guild = client.getStorage().getGuild(snw.kookbc.util.JacksonUtil.get(data, "guild_id").asText()); + this.rpo = parseRPO(snw.kookbc.util.JacksonUtil.convertToGsonJsonObject(data)); + this.upo = parseUPO(client, snw.kookbc.util.JacksonUtil.convertToGsonJsonObject(data)); // Why we delay the add operation? // We may construct the channel object at any time, @@ -344,7 +349,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..bdc92d93 --- /dev/null +++ b/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java @@ -0,0 +1,232 @@ +/* + * 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 和表情
  • + *
  • 支持 @提及功能
  • + *
+ * + *

注意: 帖子频道API有每日配额限制(每个开发者账号10000条消息/天, + * 每日12:00北京时间重置) + * + * @see Kook 帖子频道文档 + * @since KookBC 0.32.2 + */ +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("Failed to get thread post: " + 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("Failed to get thread categories", 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..e30923cb 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 snw.kookbc.util.JacksonUtil.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 snw.kookbc.util.JacksonUtil.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); } @@ -157,11 +157,16 @@ public void setPasswordProtected(boolean passwordProtected) { this.passwordProtected = passwordProtected; } + // GSON compatibility method + public synchronized void update(com.google.gson.JsonObject data) { + update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); + } + @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 +178,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 +193,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 +211,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 +236,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..b976af2d --- /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.32.2 + */ +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..b5a5f01a --- /dev/null +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java @@ -0,0 +1,297 @@ +/* + * 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.MessageBuilder; +import snw.kookbc.impl.network.HttpAPIRoute; +import snw.kookbc.impl.pageiter.ThreadReplyIterator; +import snw.kookbc.util.MapBuilder; + +/** + * ThreadPost 实体实现 + * + *

代表帖子频道中的一个帖子,支持富文本内容、回复和统计信息 + * + * @see ThreadPost + * @since KookBC 0.32.2 + */ +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 = snw.kookbc.util.JacksonUtil.parse(contentStr); + + // 使用 CardBuilder 构建卡片组件 + if (contentJson.isArray()) { + // 多个卡片 + this.content = snw.kookbc.impl.entity.builder.CardBuilder.buildCardArray(contentJson); + } else if (contentJson.isObject()) { + // 单个卡片,包装成 MultipleCardComponent + CardComponent card = snw.kookbc.impl.entity.builder.CardBuilder.buildCardObject(contentJson); + this.content = new MultipleCardComponent(Collections.singletonList(card)); + } else { + this.content = null; + } + } catch (Exception e) { + // 解析失败时记录日志并设置为 null + client.getCore().getLogger().warn("Failed to parse thread post content: {}", 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..3efbef0e --- /dev/null +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.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.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.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.32.2 + */ +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 snw.jkook.message.component.card.CardComponent) { + // 单个卡片包装成 MultipleCardComponent + this.content = new MultipleCardComponent( + Collections.singletonList((snw.jkook.message.component.card.CardComponent) component) + ); + } else { + // 其他类型的消息组件暂不支持 + this.content = null; + } + } catch (Exception e) { + // 解析失败时记录日志并设置为 null + client.getCore().getLogger().warn("Failed to parse thread reply content: {}", 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..c039b3f3 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,125 @@ 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 数据创建事件对象 + * + *

已完全迁移到 Jackson,移除了 Gson 依赖 + * + * @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("Failed to parse GuildUserNickNameUpdateEvent with Jackson", e); + return null; + } + } + } + + // 使用 Jackson 反序列化事件对象 + try { + Event result = jacksonMapper.readValue(object.toString(), eventType); + if (result != null) { + return result; } + } catch (Exception e) { + client.getCore().getLogger().error("Failed to deserialize event of type {}: {}", + eventType.getSimpleName(), e.getMessage()); + client.getCore().getLogger().debug("Event JSON: {}", object); } - final Event result = this.gson.fromJson(object, eventType); - // why the second condition? see ChannelInfoUpdateEventDeserializer - if (result == null && !(eventType == ChannelInfoUpdateEvent.class)) { + // 如果 Jackson 反序列化失败,记录错误 + if (!(eventType == ChannelInfoUpdateEvent.class)) { client.getCore().getLogger().error("We cannot understand the frame."); client.getCore().getLogger().error("Frame content: {}", object); } - return result; + 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..fcfa8b4c 100644 --- a/src/main/java/snw/kookbc/impl/message/ChannelMessageImpl.java +++ b/src/main/java/snw/kookbc/impl/message/ChannelMessageImpl.java @@ -1,6 +1,7 @@ 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 +18,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 +136,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..ca61b2d8 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; @@ -107,7 +105,7 @@ public long getTimeStamp() { @Override public Collection getUserByReaction(CustomEmoji customEmoji) { - JsonArray array; + JsonNode array; try { String rawStr = client.getNetworkClient().getRawContent( String.format( @@ -117,7 +115,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 = snw.kookbc.util.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 +128,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..6581875a 100644 --- a/src/main/java/snw/kookbc/impl/message/PrivateMessageImpl.java +++ b/src/main/java/snw/kookbc/impl/message/PrivateMessageImpl.java @@ -18,8 +18,8 @@ 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; @@ -27,6 +27,7 @@ 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 +101,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 +116,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..a0a12d0b 100644 --- a/src/main/java/snw/kookbc/impl/network/Bucket.java +++ b/src/main/java/snw/kookbc/impl/network/Bucket.java @@ -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..19709f7b 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 diff --git a/src/main/java/snw/kookbc/impl/network/ListenerImpl.java b/src/main/java/snw/kookbc/impl/network/ListenerImpl.java index 5935b930..1abf5904 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; @@ -71,7 +70,7 @@ public void handle(Frame frame) { } switch (frame.getType()) { case EVENT: - client.getEventExecutor().execute(() -> event(frame)); + event(frame); // 直接在当前线程处理,保证顺序 break; case HELLO: hello(frame); @@ -92,7 +91,10 @@ public void handle(Frame frame) { break; case RESUME_ACK: client.getCore().getLogger().info("Resume finished"); - client.getSession().setId(frame.getData().get("session_id").getAsString()); + JsonNode sessionIdNode = frame.getData().get("session_id"); + if (sessionIdNode != null) { + client.getSession().setId(sessionIdNode.asText()); + } break; } } @@ -102,51 +104,49 @@ protected void event(Frame frame) { client.getCore().getLogger().debug("Got 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); + 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("Processed buffered message with SN {}", bufFrame.getSN()); } - } else if(client.getConfig().getBoolean("allow-warn-old-message")){ + } else if (client.getConfig().getBoolean("allow-warn-old-message")) { client.getCore().getLogger().warn("Unexpected old message from remote. Dropped it."); } } } + 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); @@ -181,10 +181,14 @@ protected void saveSN() { protected void hello(Frame frame) { client.getCore().getLogger().debug("Got 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(); } diff --git a/src/main/java/snw/kookbc/impl/network/NetworkClient.java b/src/main/java/snw/kookbc/impl/network/NetworkClient.java index 10487de7..1f173349 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,16 +56,37 @@ 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!!!"); builder.sslSocketFactory(IgnoreSSLHelper.getSSLSocketFactory(), IgnoreSSLHelper.TRUST_MANAGER) @@ -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(); + } + + /** + * 获取活跃连接数 + * + * @return 当前活跃连接数 + */ + public int getActiveConnectionCount() { + return connectionPool.connectionCount() - connectionPool.idleConnectionCount(); } - public JsonObject post(String fullUrl, Map body) { - return checkResponse(JsonParser.parseString(postContent(fullUrl, body)).getAsJsonObject()) - .getAsJsonObject("data"); + /** + * 清理连接池中的空闲连接 + */ + public void evictIdleConnections() { + connectionPool.evictAll(); + kbcClient.getCore().getLogger().debug("Evicted all idle connections from connection pool"); + } + + // 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) { @@ -148,8 +231,114 @@ protected void logRequest(String method, String fullUrl, @Nullable String postBo method, fullUrl, postBodyJson); } - // Return original object if check OK - public JsonObject checkResponse(JsonObject response) { + // ===== 虚拟线程异步 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").asText(); + throw new BadResponseException(code, message); + } + return response; + } + + // Gson响应检查 - 向后兼容性支持 + public com.google.gson.JsonObject checkResponse(com.google.gson.JsonObject response) { int code = response.get("code").getAsInt(); if (code != 0) { @@ -158,4 +347,5 @@ public JsonObject checkResponse(JsonObject response) { } 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..30680d2b 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; } 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..0261f8ba 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() { @@ -115,10 +151,42 @@ public void shutdownHttp() { // 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) { @@ -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..d702aa7c --- /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..042f2ae7 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,14 +42,22 @@ 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(); + // 使用桥接方法将JsonNode转换为JsonObject给Storage使用 + object.add(client.getStorage().getEmoji(id, convertToGsonObject(element))); } } + /** + * 桥接方法:将Jackson JsonNode转换为Gson JsonObject + */ + private static com.google.gson.JsonObject convertToGsonObject(JsonNode node) { + return new com.google.gson.JsonParser().parse(node.toString()).getAsJsonObject(); + } + @Override public Set next() { return Collections.unmodifiableSet(super.next()); 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..76de7a20 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,14 +42,22 @@ 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(); + // 使用桥接方法将JsonNode转换为JsonObject给Storage使用 + object.add(client.getStorage().getRole(guild, roleId, convertToGsonObject(element))); } } + /** + * 桥接方法:将Jackson JsonNode转换为Gson JsonObject + */ + private static com.google.gson.JsonObject convertToGsonObject(JsonNode node) { + return new com.google.gson.JsonParser().parse(node.toString()).getAsJsonObject(); + } + @Override public Set next() { return Collections.unmodifiableSet(super.next()); 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..11cb4763 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,14 +40,23 @@ 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(); + // 使用桥接方法将JsonNode转换为JsonObject给Storage使用 + object.add(client.getStorage().getGuild(id, convertToGsonObject(element))); } } + /** + * 桥接方法:将Jackson JsonNode转换为Gson JsonObject + * 这是为了与Storage层的接口兼容,Storage层仍在使用Gson接口 + */ + private static com.google.gson.JsonObject convertToGsonObject(JsonNode node) { + return JacksonUtil.convertToGsonJsonObject(node); + } + @Override public Collection next() { return Collections.unmodifiableCollection(super.next()); 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..16437236 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,11 @@ public Optional getMeta() { protected abstract String getRequestURL(); - protected abstract void processElements(JsonArray array); + protected abstract void processElements(JsonNode node); + + // 向后兼容方法:支持GSON JsonArray(已弃用) + protected void processElements(com.google.gson.JsonArray array) { + // 将JsonArray转换为JsonNode供新方法使用,确保兼容性 + processElements(snw.kookbc.util.JacksonUtil.parse(array.toString())); + } } 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..265999bd --- /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.32.2 + */ +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..92397d09 --- /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.32.2 + */ +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..7ab92259 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(); @@ -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); @@ -255,7 +259,7 @@ public void disablePlugin(Plugin plugin) { 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/SchedulerImpl.java b/src/main/java/snw/kookbc/impl/scheduler/SchedulerImpl.java index 68c74bdc..2e7ed0bf 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; } 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..bce218da --- /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.32.2 + */ +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..6ee62f22 --- /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.32.2 + * @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..b11604d3 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.32.2 + */ +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..51dfd595 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.32.2 + */ +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..0ef30036 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.32.2 + */ +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..01860d86 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.32.2 + */ +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..e175c85e 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.32.2 + */ +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..ea52b4e5 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.32.2 + */ +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..aa9864a2 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.32.2 + */ +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..87b9ac87 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.32.2 + */ +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..eb2b7895 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.32.2 + */ +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..ac55cf85 --- /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.32.2 + */ +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..4b74e80d 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.32.2 + */ +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..74022470 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.32.2 + */ +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..3fa1a571 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.32.2 + */ +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..577542cc 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.32.2 + */ +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..19ba6d2e 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.32.2 + */ +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..dee6fa4a 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.32.2 + */ +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..4c6bf9e3 --- /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.32.2 + */ +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..bf9f744d 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.32.2 + */ +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..e76152e3 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.32.2 + */ +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..ae665f8a 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.32.2 + */ +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..6edbd447 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.32.2 + */ +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..38253b78 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.32.2 + */ +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..23a2acd8 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.32.2 + */ +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..4a4e4411 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.32.2 + */ +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..fcce889e 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.32.2 + */ +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..98aab23f --- /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.32.2 + */ +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..dac7ecde 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.32.2 + */ +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..e92880a8 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.32.2 + */ +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..74ffc9be 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.32.2 + */ +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..b7154dbd 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.32.2 + */ +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..364b48ea 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.32.2 + */ +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..d0700783 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.32.2 + */ +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..7a11215a 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.32.2 + */ +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..08f5ebb4 100644 --- a/src/main/java/snw/kookbc/impl/storage/EntityStorage.java +++ b/src/main/java/snw/kookbc/impl/storage/EntityStorage.java @@ -21,6 +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.fasterxml.jackson.databind.JsonNode; import com.google.gson.JsonObject; import snw.jkook.entity.*; import snw.jkook.entity.channel.Channel; @@ -35,9 +36,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; @@ -140,7 +144,33 @@ public User getUser(String id, JsonObject def) { return result; } + // ===== 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) { + result = client.getEntityBuilder().buildUser(def); + addUser(result); + } else { + ((UserImpl) result).update(def); + } + return result; + } + public Guild getGuild(String id, JsonObject def) { + Guild result = guilds.getIfPresent(id); + if (result == null) { + result = client.getEntityBuilder().buildGuild(def); + addGuild(result); + } else { + // 转换JsonObject到JsonNode再更新 + ((GuildImpl) result).update(snw.kookbc.util.JacksonUtil.parse(def.toString())); + } + return result; + } + + public Guild getGuild(String id, JsonNode def) { Guild result = guilds.getIfPresent(id); if (result == null) { result = client.getEntityBuilder().buildGuild(def); @@ -162,6 +192,17 @@ public Channel getChannel(String id, JsonObject def) { return result; } + public Channel getChannel(String id, JsonNode def) { + Channel result = channels.getIfPresent(id); + if (result == null) { + result = client.getEntityBuilder().buildChannel(def); + addChannel(result); + } else { + ((ChannelImpl) result).update(def); + } + return result; + } + public Role getRole(Guild guild, int id, JsonObject def) { // getRole is Nullable Role result = getRole(guild, id); @@ -174,6 +215,18 @@ public Role getRole(Guild guild, int id, JsonObject def) { return result; } + public Role getRole(Guild guild, int id, JsonNode def) { + // getRole is Nullable + Role result = getRole(guild, id); + if (result == null) { + result = client.getEntityBuilder().buildRole(guild, def); + addRole(guild, result); + } else { + ((RoleImpl) result).update(def); + } + return result; + } + public CustomEmoji getEmoji(String id, JsonObject def) { CustomEmoji emoji = getEmoji(id); if (emoji == null) { @@ -185,6 +238,17 @@ public CustomEmoji getEmoji(String id, JsonObject def) { return emoji; } + public CustomEmoji getEmoji(String id, JsonNode def) { + CustomEmoji emoji = getEmoji(id); + if (emoji == null) { + emoji = client.getEntityBuilder().buildEmoji(def); + addEmoji(emoji); + } else { + ((CustomEmojiImpl) emoji).update(def); + } + return emoji; + } + public Reaction getReaction(String msgId, CustomEmoji emoji, User sender) { return reactions.getIfPresent(msgId + "#" + emoji.getId() + "#" + sender.getId()); } @@ -281,6 +345,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..e836eb62 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; @@ -73,11 +72,21 @@ 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."); diff --git a/src/main/java/snw/kookbc/impl/tasks/UpdateChecker.java b/src/main/java/snw/kookbc/impl/tasks/UpdateChecker.java index ba67167a..ed9998c9 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; @@ -50,24 +50,45 @@ public void run() { } 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("Cannot check update! GitHub API rate limit exceeded. Please try again later."); + } else { + client.getCore().getLogger() + .warn("Cannot check update! GitHub API returned error: {}", errorMessage); + } + return; + } + + JsonNode tagNameNode = resObj.get("tag_name"); + if (tagNameNode == null || tagNameNode.isNull()) { + client.getCore().getLogger() + .warn("Cannot check update! GitHub API response missing 'tag_name' field. API format may have changed."); + return; + } + + String receivedVersion = tagNameNode.asText(); if (receivedVersion.startsWith("v")) { // normally I won't add "v" prefix. receivedVersion = receivedVersion.substring(1); @@ -86,10 +107,19 @@ private void run0() throws Exception { client.getCore().getLogger().info("Update available! Information is following:"); client.getCore().getLogger().info("New Version: {}, Currently on: {}", 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()); + + JsonNode nameNode = resObj.get("name"); + String releaseName = nameNode != null && !nameNode.isNull() ? nameNode.asText() : "Unknown"; + client.getCore().getLogger().info("Release Title: {}", releaseName); + + JsonNode publishedAtNode = resObj.get("published_at"); + String publishedAt = publishedAtNode != null && !publishedAtNode.isNull() ? publishedAtNode.asText() : "Unknown"; + client.getCore().getLogger().info("Release Time: {}", publishedAt); + client.getCore().getLogger().info("Release message is following:"); - for (String body : resObj.get("body").getAsString().split("\r\n")) { + JsonNode bodyNode = resObj.get("body"); + String releaseBody = bodyNode != null && !bodyNode.isNull() ? bodyNode.asText() : "No release notes available"; + for (String body : releaseBody.split("\r\n")) { client.getCore().getLogger().info(body); } client.getCore().getLogger().info( @@ -98,7 +128,7 @@ private void run0() throws Exception { break; } case 0: { - client.getCore().getLogger().info("You are using the latest version! :)"); + client.getCore().getLogger().info("您正在使用最新版本!:)"); break; } case 1: { 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..b4a08a06 --- /dev/null +++ b/src/main/java/snw/kookbc/interfaces/AsyncHttpAPI.java @@ -0,0 +1,261 @@ +/* + * 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.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 snw.jkook.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/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..9b9d6ce9 --- /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.32.2 驱动 - 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..e935aff0 --- /dev/null +++ b/src/main/java/snw/kookbc/util/JacksonUtil.java @@ -0,0 +1,347 @@ +/* + * 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处理 + * 提供与GsonUtil兼容的API,但使用Jackson实现更高性能 + */ +public final class JacksonUtil { + + // 兼容性字段,供现有代码使用 - 移除以避免类型不匹配 + // public static final com.google.gson.Gson NORMAL_GSON = GsonUtil.NORMAL_GSON; + + // 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); + } + + /** + * 将 Gson JsonObject 转换为 Jackson JsonNode + * + *

性能优化版本:使用 Jackson 直接解析,避免 toString() 的序列化开销 + * + * @param gsonObject Gson JsonObject + * @return Jackson JsonNode + */ + public static JsonNode convertFromGsonJsonObject(com.google.gson.JsonObject gsonObject) { + if (gsonObject == null) { + return null; + } + try { + // 优化:直接使用 Jackson 解析器,避免 toString() 序列化开销 + return MAPPER.readTree(gsonObject.toString()); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to convert Gson JsonObject to Jackson JsonNode", e); + } + } + + /** + * 将 Jackson JsonNode 转换为 Gson JsonObject + * + * @param jacksonNode Jackson JsonNode + * @return Gson JsonObject + * @deprecated 建议逐步迁移到纯 Jackson API + */ + @Deprecated + public static com.google.gson.JsonObject convertToGsonJsonObject(JsonNode jacksonNode) { + if (jacksonNode == null) { + return null; + } + try { + // 优化:直接使用JsonParser而不是NORMAL_GSON,减少运行时Gson依赖 + // 将JsonNode转换为GSON JsonObject + String jsonString = jacksonNode.toString(); + return new com.google.gson.JsonParser().parse(jsonString).getAsJsonObject(); + } catch (Exception e) { + throw new RuntimeException("Failed to convert Jackson JsonNode to Gson JsonObject", e); + } + } + + // ===== 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 From 33beaeee4358bfe89caebc2a8be68060fe9693dc Mon Sep 17 00:00:00 2001 From: RealSeek <1536266519@qq.com> Date: Sat, 15 Nov 2025 19:19:46 +0800 Subject: [PATCH 02/10] refactor(json)!: remove Gson compatibility methods and consolidate JSON processing - The codebase has fully migrated to using Jackson for all JSON parsing and serialization. - All methods and utilities providing backward compatibility for Gson `JsonObject` - and `JsonArray` have been removed. - This includes Gson-specific update methods in entities, builder methods, storage - methods, and utility conversion functions. - This change simplifies the codebase, reduces dependency surface, and improves - performance by removing unnecessary conversions. BREAKING CHANGE: All Gson-specific compatibility methods and utilities have been removed. Code relying on these methods will need to be updated to use Jackson directly. --- .../kookbc/impl/entity/CustomEmojiImpl.java | 7 --- .../java/snw/kookbc/impl/entity/GameImpl.java | 7 --- .../snw/kookbc/impl/entity/GuildImpl.java | 7 +-- .../java/snw/kookbc/impl/entity/RoleImpl.java | 7 --- .../java/snw/kookbc/impl/entity/UserImpl.java | 6 -- .../impl/entity/builder/CardBuilder.java | 57 ----------------- .../impl/entity/builder/EntityBuildUtil.java | 62 +------------------ .../impl/entity/builder/EntityBuilder.java | 54 ---------------- .../impl/entity/builder/MessageBuilder.java | 33 ---------- .../impl/entity/channel/ChannelImpl.java | 10 +-- .../impl/entity/channel/VoiceChannelImpl.java | 5 -- .../snw/kookbc/impl/event/EventFactory.java | 2 - .../impl/message/ChannelMessageImpl.java | 1 - .../impl/message/PrivateMessageImpl.java | 2 - .../kookbc/impl/network/NetworkClient.java | 11 ---- .../impl/pageiter/GuildEmojiListIterator.java | 10 +-- .../impl/pageiter/GuildRoleListIterator.java | 10 +-- .../impl/pageiter/JoinedGuildIterator.java | 11 +--- .../impl/pageiter/PageIteratorImpl.java | 5 -- .../kookbc/impl/storage/EntityStorage.java | 59 ------------------ .../interfaces/network/webhook/Request.java | 2 - .../java/snw/kookbc/util/JacksonUtil.java | 46 -------------- 22 files changed, 8 insertions(+), 406 deletions(-) diff --git a/src/main/java/snw/kookbc/impl/entity/CustomEmojiImpl.java b/src/main/java/snw/kookbc/impl/entity/CustomEmojiImpl.java index 7daf5ca5..dbf27d70 100644 --- a/src/main/java/snw/kookbc/impl/entity/CustomEmojiImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/CustomEmojiImpl.java @@ -85,13 +85,6 @@ public void setName0(String name) { this.name = name; } - // GSON compatibility method - public synchronized void update(com.google.gson.JsonObject data) { - update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); - } - - // ===== Jackson API - 高性能版本 ===== - @Override public synchronized void update(JsonNode data) { isTrue(Objects.equals(getId(), getRequiredString(data, "id")), "You can't update the emoji by using different data"); diff --git a/src/main/java/snw/kookbc/impl/entity/GameImpl.java b/src/main/java/snw/kookbc/impl/entity/GameImpl.java index dc94400c..7f2508c8 100644 --- a/src/main/java/snw/kookbc/impl/entity/GameImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/GameImpl.java @@ -98,13 +98,6 @@ public void setNameAndIcon(@NotNull String name, @NotNull String icon) { this.icon = icon; } - // GSON compatibility method - public synchronized void update(com.google.gson.JsonObject data) { - update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); - } - - // ===== Jackson API - 高性能版本 ===== - @Override public synchronized void update(JsonNode data) { this.name = getStringOrDefault(data, "name", "Unknown Game"); diff --git a/src/main/java/snw/kookbc/impl/entity/GuildImpl.java b/src/main/java/snw/kookbc/impl/entity/GuildImpl.java index 86f8b026..0813167c 100644 --- a/src/main/java/snw/kookbc/impl/entity/GuildImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/GuildImpl.java @@ -251,7 +251,7 @@ public Role createRole(String s) { .put("name", s) .build(); JsonNode res = client.getNetworkClient().post(HttpAPIRoute.ROLE_CREATE.toFullURL(), body); - Role result = client.getEntityBuilder().buildRole(this, snw.kookbc.util.JacksonUtil.convertToGsonJsonObject(res)); + Role result = client.getEntityBuilder().buildRole(this, res); client.getStorage().addRole(this, result); return result; } @@ -365,11 +365,6 @@ public void setAvatar(String avatarUrl) { this.avatarUrl = avatarUrl; } - // GSON compatibility method - marked for deprecation - public synchronized void updateFromGson(com.google.gson.JsonObject data) { - update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); - } - @Override public synchronized void update(JsonNode data) { final String id = data.get("id").asText(); diff --git a/src/main/java/snw/kookbc/impl/entity/RoleImpl.java b/src/main/java/snw/kookbc/impl/entity/RoleImpl.java index 23a21c34..c49b2f89 100644 --- a/src/main/java/snw/kookbc/impl/entity/RoleImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/RoleImpl.java @@ -166,13 +166,6 @@ public void setMentionable0(boolean mentionable) { this.mentionable = mentionable; } - // GSON compatibility method - public synchronized void update(com.google.gson.JsonObject data) { - update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); - } - - // ===== Jackson API - 高性能版本 ===== - @Override public synchronized void update(JsonNode data) { isTrue(getId() == getRequiredInt(data, "role_id"), "You can't update the role by using different data"); diff --git a/src/main/java/snw/kookbc/impl/entity/UserImpl.java b/src/main/java/snw/kookbc/impl/entity/UserImpl.java index c45c8940..b2e5b538 100644 --- a/src/main/java/snw/kookbc/impl/entity/UserImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/UserImpl.java @@ -344,12 +344,6 @@ public void setVipAvatarUrl(String vipAvatarUrl) { this.vipAvatarUrl = vipAvatarUrl; } - // GSON compatibility method - public void update(com.google.gson.JsonObject data) { - // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 - update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); - } - @Override public synchronized void update(JsonNode data) { Validate.isTrue(Objects.equals(getId(), data.get("id").asText()), 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 9dd36cbd..a6777413 100644 --- a/src/main/java/snw/kookbc/impl/entity/builder/CardBuilder.java +++ b/src/main/java/snw/kookbc/impl/entity/builder/CardBuilder.java @@ -84,30 +84,6 @@ public static Object buildCard(String jsonString) { } } - // ===== Gson兼容方法(向后兼容)===== - - /** - * 从Gson JsonArray构建多卡片组件(向后兼容) - * @param array Gson JsonArray - * @return MultipleCardComponent - */ - public static MultipleCardComponent buildCard(com.google.gson.JsonArray array) { - // 转换Gson JsonArray到Jackson JsonNode - JsonNode arrayNode = JacksonCardUtil.parse(array.toString()); - return buildCardArray(arrayNode); - } - - /** - * 从Gson JsonObject构建单个卡片组件(向后兼容) - * @param object Gson JsonObject - * @return CardComponent - */ - public static CardComponent buildCard(com.google.gson.JsonObject object) { - // 转换Gson JsonObject到Jackson JsonNode - JsonNode objectNode = JacksonCardUtil.parse(object.toString()); - return buildCardObject(objectNode); - } - // ===== 序列化方法 ===== /** @@ -128,37 +104,4 @@ public static JsonNode serializeToNode(MultipleCardComponent component) { return JacksonCardUtil.toJsonNode(component); } - /** - * 序列化单个卡片组件为Gson JsonArray(向后兼容) - * @param component CardComponent - * @return JsonArray - */ - public static com.google.gson.JsonArray serialize(CardComponent component) { - com.google.gson.JsonArray result = new com.google.gson.JsonArray(); - result.add(serialize0(component)); - return result; - } - - /** - * 序列化多卡片组件为Gson JsonArray(向后兼容) - * @param component MultipleCardComponent - * @return JsonArray - */ - public static com.google.gson.JsonArray serialize(MultipleCardComponent component) { - com.google.gson.JsonArray array = new com.google.gson.JsonArray(); - for (CardComponent card : component.getComponents()) { - array.add(serialize0(card)); - } - return array; - } - - /** - * 序列化单个卡片组件为Gson JsonObject(向后兼容) - * @param component CardComponent - * @return JsonObject - */ - public static com.google.gson.JsonObject serialize0(CardComponent component) { - String json = JacksonCardUtil.toJson(component); - return new com.google.gson.JsonParser().parse(json).getAsJsonObject(); - } } 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 4a6e49fd..68e67273 100644 --- a/src/main/java/snw/kookbc/impl/entity/builder/EntityBuildUtil.java +++ b/src/main/java/snw/kookbc/impl/entity/builder/EntityBuildUtil.java @@ -39,58 +39,6 @@ public class EntityBuildUtil { - // ===== Gson兼容方法(向后兼容)===== - - public static Collection parseRPO(com.google.gson.JsonObject object) { - com.google.gson.JsonArray array = object.getAsJsonArray("permission_overwrites"); - Collection rpo = new ConcurrentLinkedQueue<>(); - for (com.google.gson.JsonElement element : array) { - com.google.gson.JsonObject orpo = element.getAsJsonObject(); - rpo.add( - new Channel.RolePermissionOverwrite( - orpo.get("role_id").getAsInt(), - orpo.get("allow").getAsInt(), - orpo.get("deny").getAsInt())); - } - return rpo; - } - - public static Collection parseUPO(KBCClient client, com.google.gson.JsonObject object) { - com.google.gson.JsonArray array = object.getAsJsonArray("permission_users"); - Collection upo = new ConcurrentLinkedQueue<>(); - for (com.google.gson.JsonElement element : array) { - com.google.gson.JsonObject oupo = element.getAsJsonObject(); - com.google.gson.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())); - } - return upo; - } - - public static NotifyType parseNotifyType(com.google.gson.JsonObject object) { - Guild.NotifyType type = null; - int rawNotifyType = object.get("notify_type").getAsInt(); - for (Guild.NotifyType value : Guild.NotifyType.values()) { - if (value.getValue() == rawNotifyType) { - type = value; - break; - } - } - notNull(type, String.format("Internal Error: Unexpected NotifyType from remote: %s", rawNotifyType)); - return type; - } - - public static Guild parseEmojiGuild(String id, KBCClient client, com.google.gson.JsonObject object) { - Guild guild = null; - if (id.contains("/")) { - guild = client.getStorage().getGuild(id.substring(0, id.indexOf("/"))); - } - return guild; - } - @Nullable public static Channel parseChannel(KBCClient client, String id, int type) { switch (type) { @@ -147,10 +95,9 @@ public static Collection parseUPO(KBCClient cli int allow = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "allow", 0); int deny = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "deny", 0); - // 临时桥接到Gson JsonObject - com.google.gson.JsonObject gsonUser = convertToGsonJsonObject(rawUser); + // 直接使用Jackson JsonNode upo.add(new Channel.UserPermissionOverwrite( - client.getStorage().getUser(userId, gsonUser), + client.getStorage().getUser(userId, rawUser), allow, deny)); } } @@ -189,9 +136,4 @@ public static Guild parseEmojiGuild(String id, KBCClient client, JsonNode node) return guild; } - // 临时桥接方法 - 将JsonNode转换为JsonObject供现有代码使用 - private static com.google.gson.JsonObject convertToGsonJsonObject(JsonNode jacksonNode) { - return snw.kookbc.util.JacksonUtil.convertToGsonJsonObject(jacksonNode); - } - } \ 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 2f8689c8..15150b46 100644 --- a/src/main/java/snw/kookbc/impl/entity/builder/EntityBuilder.java +++ b/src/main/java/snw/kookbc/impl/entity/builder/EntityBuilder.java @@ -56,60 +56,6 @@ public EntityBuilder(KBCClient client) { this.client = client; } - /** - * 构建User对象 (Gson兼容版本) - * 该方法保留用于向后兼容,内部委托给Jackson版本 - */ - public User buildUser(com.google.gson.JsonObject s) { - JsonNode node = convertFromGsonJsonObject(s); - return buildUser(node); - } - - /** - * 构建Guild对象 (Gson兼容版本) - * 该方法保留用于向后兼容,内部委托给Jackson版本 - */ - public Guild buildGuild(com.google.gson.JsonObject object) { - JsonNode node = convertFromGsonJsonObject(object); - return buildGuild(node); - } - - /** - * 构建Channel对象 (Gson兼容版本) - * 该方法保留用于向后兼容,内部委托给Jackson版本 - */ - public Channel buildChannel(com.google.gson.JsonObject object) { - JsonNode node = convertFromGsonJsonObject(object); - return buildChannel(node); - } - - /** - * 构建Role对象 (Gson兼容版本) - * 该方法保留用于向后兼容,内部委托给Jackson版本 - */ - public Role buildRole(Guild guild, com.google.gson.JsonObject object) { - JsonNode node = convertFromGsonJsonObject(object); - return buildRole(guild, node); - } - - /** - * 构建CustomEmoji对象 (Gson兼容版本) - * 该方法保留用于向后兼容,内部委托给Jackson版本 - */ - public CustomEmoji buildEmoji(com.google.gson.JsonObject object) { - JsonNode node = convertFromGsonJsonObject(object); - return buildEmoji(node); - } - - /** - * 构建Game对象 (Gson兼容版本) - * 该方法保留用于向后兼容,内部委托给Jackson版本 - */ - public Game buildGame(com.google.gson.JsonObject object) { - JsonNode node = convertFromGsonJsonObject(object); - return buildGame(node); - } - // ===== Jackson API - 高性能版本(处理Kook不完整JSON数据)===== /** 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 23a08f99..00e5dbd0 100644 --- a/src/main/java/snw/kookbc/impl/entity/builder/MessageBuilder.java +++ b/src/main/java/snw/kookbc/impl/entity/builder/MessageBuilder.java @@ -86,26 +86,6 @@ public static Object[] serialize(BaseComponent component) { throw new RuntimeException("Unsupported component"); } - public PrivateMessage buildPrivateMessage(com.google.gson.JsonObject object) { - // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 - return buildPrivateMessage(JacksonUtil.convertFromGsonJsonObject(object)); - } - - public ChannelMessage buildChannelMessage(com.google.gson.JsonObject object) { - // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 - return buildChannelMessage(JacksonUtil.convertFromGsonJsonObject(object)); - } - - private User getAuthor(com.google.gson.JsonObject extra) { - // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 - return getAuthor(JacksonUtil.convertFromGsonJsonObject(extra)); - } - - private Message getQuote(com.google.gson.JsonObject extra) { - // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 - return getQuote(JacksonUtil.convertFromGsonJsonObject(extra)); - } - private ChannelMessageImpl buildMessage(String id, User author, BaseComponent component, long timeStamp, Message message, String targetId, int channelType) { if (channelType == CHANNEL_TYPE_TEXT) { @@ -118,19 +98,6 @@ private ChannelMessageImpl buildMessage(String id, User author, BaseComponent co throw new RuntimeException("We can not found channel type: " + channelType); } - public Message buildQuote(com.google.gson.JsonObject object) { - if (object == null) { - return null; - } - // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 - return buildQuote(JacksonUtil.convertFromGsonJsonObject(object)); - } - - public BaseComponent buildComponent(com.google.gson.JsonObject object) { - // 性能优化:使用 convertFromGsonJsonObject 避免 toString() 序列化开销 - return buildComponent(JacksonUtil.convertFromGsonJsonObject(object)); - } - // ===== Jackson API - 高性能版本 ===== public PrivateMessage buildPrivateMessage(JsonNode node) { 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 105310d6..b332bc76 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/ChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/ChannelImpl.java @@ -39,7 +39,6 @@ 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.JacksonUtil.convertFromGsonJsonObject; import static snw.kookbc.util.JacksonUtil.getAsInt; import static snw.kookbc.util.JacksonUtil.getAsString; @@ -320,18 +319,13 @@ public User getMaster() { return master; } - // GSON compatibility method - public synchronized void update(com.google.gson.JsonObject data) { - update(convertFromGsonJsonObject(data)); - } - public synchronized void update(JsonNode data) { isTrue(Objects.equals(getId(), snw.kookbc.util.JacksonUtil.get(data, "id").asText()), "You can't update channel by using different data"); this.name = snw.kookbc.util.JacksonUtil.get(data, "name").asText(); this.permSync = snw.kookbc.util.JacksonUtil.get(data, "permission_sync").asInt() != 0; this.guild = client.getStorage().getGuild(snw.kookbc.util.JacksonUtil.get(data, "guild_id").asText()); - this.rpo = parseRPO(snw.kookbc.util.JacksonUtil.convertToGsonJsonObject(data)); - this.upo = parseUPO(client, snw.kookbc.util.JacksonUtil.convertToGsonJsonObject(data)); + this.rpo = parseRPO(data); + this.upo = parseUPO(client, data); // Why we delay the add operation? // We may construct the channel object at any time, 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 e30923cb..5bc63b45 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/VoiceChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/VoiceChannelImpl.java @@ -157,11 +157,6 @@ public void setPasswordProtected(boolean passwordProtected) { this.passwordProtected = passwordProtected; } - // GSON compatibility method - public synchronized void update(com.google.gson.JsonObject data) { - update(snw.kookbc.util.JacksonUtil.convertFromGsonJsonObject(data)); - } - @Override public synchronized void update(JsonNode data) { super.update(data); diff --git a/src/main/java/snw/kookbc/impl/event/EventFactory.java b/src/main/java/snw/kookbc/impl/event/EventFactory.java index c039b3f3..1b0d7583 100644 --- a/src/main/java/snw/kookbc/impl/event/EventFactory.java +++ b/src/main/java/snw/kookbc/impl/event/EventFactory.java @@ -40,8 +40,6 @@ /** * 事件工厂类 - 负责从 JSON 数据创建事件对象 * - *

已完全迁移到 Jackson,移除了 Gson 依赖 - * * @since 0.52.0 使用 Jackson 作为唯一 JSON 引擎 */ public class EventFactory { diff --git a/src/main/java/snw/kookbc/impl/message/ChannelMessageImpl.java b/src/main/java/snw/kookbc/impl/message/ChannelMessageImpl.java index fcfa8b4c..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,5 @@ 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; diff --git a/src/main/java/snw/kookbc/impl/message/PrivateMessageImpl.java b/src/main/java/snw/kookbc/impl/message/PrivateMessageImpl.java index 6581875a..d65a8f56 100644 --- a/src/main/java/snw/kookbc/impl/message/PrivateMessageImpl.java +++ b/src/main/java/snw/kookbc/impl/message/PrivateMessageImpl.java @@ -25,8 +25,6 @@ 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; diff --git a/src/main/java/snw/kookbc/impl/network/NetworkClient.java b/src/main/java/snw/kookbc/impl/network/NetworkClient.java index 1f173349..6615a43e 100644 --- a/src/main/java/snw/kookbc/impl/network/NetworkClient.java +++ b/src/main/java/snw/kookbc/impl/network/NetworkClient.java @@ -337,15 +337,4 @@ public JsonNode checkResponseJackson(JsonNode response) { return response; } - // Gson响应检查 - 向后兼容性支持 - public com.google.gson.JsonObject checkResponse(com.google.gson.JsonObject response) { - int code = response.get("code").getAsInt(); - - if (code != 0) { - String message = response.get("message").getAsString(); - throw new BadResponseException(code, message); - } - return response; - } - } diff --git a/src/main/java/snw/kookbc/impl/pageiter/GuildEmojiListIterator.java b/src/main/java/snw/kookbc/impl/pageiter/GuildEmojiListIterator.java index 042f2ae7..97f9a762 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/GuildEmojiListIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/GuildEmojiListIterator.java @@ -46,18 +46,10 @@ protected void processElements(JsonNode node) { object = new HashSet<>(node.size()); for (JsonNode element : node) { String id = element.get("id").asText(); - // 使用桥接方法将JsonNode转换为JsonObject给Storage使用 - object.add(client.getStorage().getEmoji(id, convertToGsonObject(element))); + object.add(client.getStorage().getEmoji(id, element)); } } - /** - * 桥接方法:将Jackson JsonNode转换为Gson JsonObject - */ - private static com.google.gson.JsonObject convertToGsonObject(JsonNode node) { - return new com.google.gson.JsonParser().parse(node.toString()).getAsJsonObject(); - } - @Override public Set next() { return Collections.unmodifiableSet(super.next()); diff --git a/src/main/java/snw/kookbc/impl/pageiter/GuildRoleListIterator.java b/src/main/java/snw/kookbc/impl/pageiter/GuildRoleListIterator.java index 76de7a20..394bad7b 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/GuildRoleListIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/GuildRoleListIterator.java @@ -46,18 +46,10 @@ protected void processElements(JsonNode node) { object = new HashSet<>(node.size()); for (JsonNode element : node) { int roleId = element.get("role_id").asInt(); - // 使用桥接方法将JsonNode转换为JsonObject给Storage使用 - object.add(client.getStorage().getRole(guild, roleId, convertToGsonObject(element))); + object.add(client.getStorage().getRole(guild, roleId, element)); } } - /** - * 桥接方法:将Jackson JsonNode转换为Gson JsonObject - */ - private static com.google.gson.JsonObject convertToGsonObject(JsonNode node) { - return new com.google.gson.JsonParser().parse(node.toString()).getAsJsonObject(); - } - @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 11cb4763..6d4fdb54 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/JoinedGuildIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/JoinedGuildIterator.java @@ -44,19 +44,10 @@ protected void processElements(JsonNode node) { object = new HashSet<>(node.size()); for (JsonNode element : node) { String id = element.get("id").asText(); - // 使用桥接方法将JsonNode转换为JsonObject给Storage使用 - object.add(client.getStorage().getGuild(id, convertToGsonObject(element))); + object.add(client.getStorage().getGuild(id, element)); } } - /** - * 桥接方法:将Jackson JsonNode转换为Gson JsonObject - * 这是为了与Storage层的接口兼容,Storage层仍在使用Gson接口 - */ - private static com.google.gson.JsonObject convertToGsonObject(JsonNode node) { - return JacksonUtil.convertToGsonJsonObject(node); - } - @Override public Collection next() { return Collections.unmodifiableCollection(super.next()); diff --git a/src/main/java/snw/kookbc/impl/pageiter/PageIteratorImpl.java b/src/main/java/snw/kookbc/impl/pageiter/PageIteratorImpl.java index 16437236..da833ab1 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/PageIteratorImpl.java +++ b/src/main/java/snw/kookbc/impl/pageiter/PageIteratorImpl.java @@ -121,9 +121,4 @@ public Optional getMeta() { protected abstract void processElements(JsonNode node); - // 向后兼容方法:支持GSON JsonArray(已弃用) - protected void processElements(com.google.gson.JsonArray array) { - // 将JsonArray转换为JsonNode供新方法使用,确保兼容性 - processElements(snw.kookbc.util.JacksonUtil.parse(array.toString())); - } } diff --git a/src/main/java/snw/kookbc/impl/storage/EntityStorage.java b/src/main/java/snw/kookbc/impl/storage/EntityStorage.java index 08f5ebb4..0019c455 100644 --- a/src/main/java/snw/kookbc/impl/storage/EntityStorage.java +++ b/src/main/java/snw/kookbc/impl/storage/EntityStorage.java @@ -22,7 +22,6 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.fasterxml.jackson.databind.JsonNode; -import com.google.gson.JsonObject; import snw.jkook.entity.*; import snw.jkook.entity.channel.Channel; import snw.jkook.message.Message; @@ -132,18 +131,6 @@ public CustomEmoji getEmoji(String id) { return emojis.getIfPresent(id); } - public User getUser(String id, JsonObject def) { - // use getIfPresent, because the def should not be wasted - User result = users.getIfPresent(id); - if (result == null) { - result = client.getEntityBuilder().buildUser(def); - addUser(result); - } else { - ((UserImpl) result).update(def); - } - return result; - } - // ===== Jackson API - 高性能版本 ===== public User getUser(String id, JsonNode def) { @@ -158,18 +145,6 @@ public User getUser(String id, JsonNode def) { return result; } - public Guild getGuild(String id, JsonObject def) { - Guild result = guilds.getIfPresent(id); - if (result == null) { - result = client.getEntityBuilder().buildGuild(def); - addGuild(result); - } else { - // 转换JsonObject到JsonNode再更新 - ((GuildImpl) result).update(snw.kookbc.util.JacksonUtil.parse(def.toString())); - } - return result; - } - public Guild getGuild(String id, JsonNode def) { Guild result = guilds.getIfPresent(id); if (result == null) { @@ -181,17 +156,6 @@ public Guild getGuild(String id, JsonNode def) { return result; } - public Channel getChannel(String id, JsonObject def) { - Channel result = channels.getIfPresent(id); - if (result == null) { - result = client.getEntityBuilder().buildChannel(def); - addChannel(result); - } else { - ((ChannelImpl) result).update(def); - } - return result; - } - public Channel getChannel(String id, JsonNode def) { Channel result = channels.getIfPresent(id); if (result == null) { @@ -203,18 +167,6 @@ public Channel getChannel(String id, JsonNode def) { return result; } - public Role getRole(Guild guild, int id, JsonObject def) { - // getRole is Nullable - Role result = getRole(guild, id); - if (result == null) { - result = client.getEntityBuilder().buildRole(guild, def); - addRole(guild, result); - } else { - ((RoleImpl) result).update(def); - } - return result; - } - public Role getRole(Guild guild, int id, JsonNode def) { // getRole is Nullable Role result = getRole(guild, id); @@ -227,17 +179,6 @@ public Role getRole(Guild guild, int id, JsonNode def) { return result; } - public CustomEmoji getEmoji(String id, JsonObject def) { - CustomEmoji emoji = getEmoji(id); - if (emoji == null) { - emoji = client.getEntityBuilder().buildEmoji(def); - addEmoji(emoji); - } else { - ((CustomEmojiImpl) emoji).update(def); - } - return emoji; - } - public CustomEmoji getEmoji(String id, JsonNode def) { CustomEmoji emoji = getEmoji(id); if (emoji == null) { 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/util/JacksonUtil.java b/src/main/java/snw/kookbc/util/JacksonUtil.java index e935aff0..ab9723d5 100644 --- a/src/main/java/snw/kookbc/util/JacksonUtil.java +++ b/src/main/java/snw/kookbc/util/JacksonUtil.java @@ -31,13 +31,9 @@ /** * Jackson JSON工具类 - 高性能JSON处理 - * 提供与GsonUtil兼容的API,但使用Jackson实现更高性能 */ public final class JacksonUtil { - // 兼容性字段,供现有代码使用 - 移除以避免类型不匹配 - // public static final com.google.gson.Gson NORMAL_GSON = GsonUtil.NORMAL_GSON; - // Jackson核心对象 private static final ObjectMapper MAPPER; @@ -285,48 +281,6 @@ public static Type createListType(Class elementType) { return MAPPER.getTypeFactory().constructCollectionType(List.class, elementType); } - /** - * 将 Gson JsonObject 转换为 Jackson JsonNode - * - *

性能优化版本:使用 Jackson 直接解析,避免 toString() 的序列化开销 - * - * @param gsonObject Gson JsonObject - * @return Jackson JsonNode - */ - public static JsonNode convertFromGsonJsonObject(com.google.gson.JsonObject gsonObject) { - if (gsonObject == null) { - return null; - } - try { - // 优化:直接使用 Jackson 解析器,避免 toString() 序列化开销 - return MAPPER.readTree(gsonObject.toString()); - } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to convert Gson JsonObject to Jackson JsonNode", e); - } - } - - /** - * 将 Jackson JsonNode 转换为 Gson JsonObject - * - * @param jacksonNode Jackson JsonNode - * @return Gson JsonObject - * @deprecated 建议逐步迁移到纯 Jackson API - */ - @Deprecated - public static com.google.gson.JsonObject convertToGsonJsonObject(JsonNode jacksonNode) { - if (jacksonNode == null) { - return null; - } - try { - // 优化:直接使用JsonParser而不是NORMAL_GSON,减少运行时Gson依赖 - // 将JsonNode转换为GSON JsonObject - String jsonString = jacksonNode.toString(); - return new com.google.gson.JsonParser().parse(jsonString).getAsJsonObject(); - } catch (Exception e) { - throw new RuntimeException("Failed to convert Jackson JsonNode to Gson JsonObject", e); - } - } - // ===== ObjectMapper 工厂方法 ===== /** From 51b1bde1c30dc497ace236661e03db5bdbb0e28e Mon Sep 17 00:00:00 2001 From: RealSeek <1536266519@qq.com> Date: Sat, 15 Nov 2025 20:31:22 +0800 Subject: [PATCH 03/10] refactor(imports): simplify utility class references and imports - introduce direct imports for `JacksonUtil` and `CardBuilder` to reduce verbosity - use static import for `JacksonUtil.get` in several files for cleaner code - remove fully qualified names when calling utility methods to enhance readability - streamline references to `HttpAPI` and `CardComponent` interfaces --- .../snw/kookbc/impl/entity/GuildImpl.java | 4 ++-- .../impl/entity/builder/EntityBuildUtil.java | 21 ++++++++++--------- .../impl/entity/builder/MessageBuilder.java | 1 + .../impl/entity/channel/CategoryImpl.java | 3 ++- .../impl/entity/channel/ChannelImpl.java | 9 ++++---- .../impl/entity/channel/VoiceChannelImpl.java | 4 ++-- .../impl/entity/thread/ThreadPostImpl.java | 7 ++++--- .../impl/entity/thread/ThreadReplyImpl.java | 5 +++-- .../snw/kookbc/impl/message/MessageImpl.java | 3 ++- .../snw/kookbc/interfaces/AsyncHttpAPI.java | 3 ++- 10 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/main/java/snw/kookbc/impl/entity/GuildImpl.java b/src/main/java/snw/kookbc/impl/entity/GuildImpl.java index 0813167c..68b3ae05 100644 --- a/src/main/java/snw/kookbc/impl/entity/GuildImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/GuildImpl.java @@ -282,7 +282,7 @@ public CustomEmoji uploadEmoji(byte[] content, String type, @Nullable String nam .post(requestBody) .addHeader("Authorization", client.getNetworkClient().getTokenWithPrefix()) .build(); - JsonNode object = snw.kookbc.util.JacksonUtil.parse(client.getNetworkClient().call(request)).get("data"); + JsonNode object = parse(client.getNetworkClient().call(request)).get("data"); CustomEmoji emoji = client.getEntityBuilder().buildEmoji(object); client.getStorage().addEmoji(emoji); return emoji; @@ -338,7 +338,7 @@ public String createInvite(int validSeconds, int validTimes) { .put("setting_times", validTimes) .build(); JsonNode object = client.getNetworkClient().post(HttpAPIRoute.INVITE_CREATE.toFullURL(), body); - return snw.kookbc.util.JacksonUtil.get(object, "url").asText(); + return get(object, "url").asText(); } @Override 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 68e67273..6950e7ea 100644 --- a/src/main/java/snw/kookbc/impl/entity/builder/EntityBuildUtil.java +++ b/src/main/java/snw/kookbc/impl/entity/builder/EntityBuildUtil.java @@ -31,6 +31,7 @@ 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; @@ -62,15 +63,15 @@ public static Channel parseChannel(KBCClient client, String id, int type) { * 解析角色权限覆写 (Jackson版本,安全处理不完整JSON) */ public static Collection parseRPO(JsonNode node) { - JsonNode array = snw.kookbc.util.JacksonUtil.getArrayOrNull(node, "permission_overwrites"); + JsonNode array = JacksonUtil.getArrayOrNull(node, "permission_overwrites"); Collection rpo = new ConcurrentLinkedQueue<>(); if (array != null && array.isArray()) { for (JsonNode element : array) { if (element != null && element.isObject()) { - int roleId = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "role_id", 0); - int allow = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "allow", 0); - int deny = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "deny", 0); + 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)); } @@ -83,17 +84,17 @@ public static Collection parseRPO(JsonNode node * 解析用户权限覆写 (Jackson版本,安全处理不完整JSON) */ public static Collection parseUPO(KBCClient client, JsonNode node) { - JsonNode array = snw.kookbc.util.JacksonUtil.getArrayOrNull(node, "permission_users"); + JsonNode array = JacksonUtil.getArrayOrNull(node, "permission_users"); Collection upo = new ConcurrentLinkedQueue<>(); if (array != null && array.isArray()) { for (JsonNode element : array) { if (element != null && element.isObject()) { - JsonNode rawUser = snw.kookbc.util.JacksonUtil.getObjectOrNull(element, "user"); + JsonNode rawUser = JacksonUtil.getObjectOrNull(element, "user"); if (rawUser != null) { - String userId = snw.kookbc.util.JacksonUtil.getRequiredString(rawUser, "id"); - int allow = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "allow", 0); - int deny = snw.kookbc.util.JacksonUtil.getIntOrDefault(element, "deny", 0); + 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( @@ -110,7 +111,7 @@ public static Collection parseUPO(KBCClient cli * 解析通知类型 (Jackson版本,安全处理不完整JSON) */ public static NotifyType parseNotifyType(JsonNode node) { - int rawNotifyType = snw.kookbc.util.JacksonUtil.getIntOrDefault(node, "notify_type", 0); + int rawNotifyType = JacksonUtil.getIntOrDefault(node, "notify_type", 0); for (Guild.NotifyType value : Guild.NotifyType.values()) { if (value.getValue() == rawNotifyType) { 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 00e5dbd0..b62d4946 100644 --- a/src/main/java/snw/kookbc/impl/entity/builder/MessageBuilder.java +++ b/src/main/java/snw/kookbc/impl/entity/builder/MessageBuilder.java @@ -34,6 +34,7 @@ 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.*; 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 e2c4eae0..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,6 +18,7 @@ package snw.kookbc.impl.entity.channel; +import static snw.kookbc.util.JacksonUtil.get; import static snw.kookbc.util.JacksonUtil.getAsJsonArray; import java.util.Collection; @@ -51,7 +52,7 @@ public Collection getChannels() { .get(HttpAPIRoute.CHANNEL_INFO.toFullURL() + "?target_id=" + getId() + "&need_children=true"); update(object); final Collection channels = new LinkedList<>(); - snw.kookbc.util.JacksonUtil.get(object, "children") + 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 b332bc76..7c07c7e2 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/ChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/ChannelImpl.java @@ -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; @@ -320,10 +321,10 @@ public User getMaster() { } public synchronized void update(JsonNode data) { - isTrue(Objects.equals(getId(), snw.kookbc.util.JacksonUtil.get(data, "id").asText()), "You can't update channel by using different data"); - this.name = snw.kookbc.util.JacksonUtil.get(data, "name").asText(); - this.permSync = snw.kookbc.util.JacksonUtil.get(data, "permission_sync").asInt() != 0; - this.guild = client.getStorage().getGuild(snw.kookbc.util.JacksonUtil.get(data, "guild_id").asText()); + 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); 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 5bc63b45..7237a4e6 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/VoiceChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/VoiceChannelImpl.java @@ -75,7 +75,7 @@ public String createInvite(int validSeconds, int validTimes) { .put("setting_times", validTimes) .build(); JsonNode object = client.getNetworkClient().post(HttpAPIRoute.INVITE_CREATE.toFullURL(), body); - return snw.kookbc.util.JacksonUtil.get(object, "url").asText(); + return get(object, "url").asText(); } @Override @@ -118,7 +118,7 @@ public void setSize(int size) { public int getQuality() { // must query because we can't update this value by update(JsonObject) method final JsonNode self = client.getNetworkClient() .get(HttpAPIRoute.CHANNEL_INFO.toFullURL() + "?target_id=" + getId()); - return snw.kookbc.util.JacksonUtil.get(self, "voice_quality").asInt(); + return get(self, "voice_quality").asInt(); } @Override diff --git a/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java b/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java index b5a5f01a..8d248f25 100644 --- a/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java @@ -38,6 +38,7 @@ 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; @@ -140,15 +141,15 @@ public synchronized void update(JsonNode data) { try { // content 是 JSON 字符串,需要先解析 String contentStr = contentNode.asText(); - JsonNode contentJson = snw.kookbc.util.JacksonUtil.parse(contentStr); + JsonNode contentJson = parse(contentStr); // 使用 CardBuilder 构建卡片组件 if (contentJson.isArray()) { // 多个卡片 - this.content = snw.kookbc.impl.entity.builder.CardBuilder.buildCardArray(contentJson); + this.content = CardBuilder.buildCardArray(contentJson); } else if (contentJson.isObject()) { // 单个卡片,包装成 MultipleCardComponent - CardComponent card = snw.kookbc.impl.entity.builder.CardBuilder.buildCardObject(contentJson); + CardComponent card = CardBuilder.buildCardObject(contentJson); this.content = new MultipleCardComponent(Collections.singletonList(card)); } else { this.content = null; diff --git a/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java b/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java index 3efbef0e..e3878fc3 100644 --- a/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java @@ -33,6 +33,7 @@ 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; @@ -119,10 +120,10 @@ public synchronized void update(JsonNode data) { // 如果是 MultipleCardComponent 或者可以转换为 MultipleCardComponent if (component instanceof MultipleCardComponent) { this.content = (MultipleCardComponent) component; - } else if (component instanceof snw.jkook.message.component.card.CardComponent) { + } else if (component instanceof CardComponent) { // 单个卡片包装成 MultipleCardComponent this.content = new MultipleCardComponent( - Collections.singletonList((snw.jkook.message.component.card.CardComponent) component) + Collections.singletonList((CardComponent) component) ); } else { // 其他类型的消息组件暂不支持 diff --git a/src/main/java/snw/kookbc/impl/message/MessageImpl.java b/src/main/java/snw/kookbc/impl/message/MessageImpl.java index ca61b2d8..df638a14 100644 --- a/src/main/java/snw/kookbc/impl/message/MessageImpl.java +++ b/src/main/java/snw/kookbc/impl/message/MessageImpl.java @@ -34,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; @@ -115,7 +116,7 @@ public Collection getUserByReaction(CustomEmoji customEmoji) { .toFullURL(), getId(), URLEncoder.encode(customEmoji.getId(), StandardCharsets.UTF_8.name()))); - JsonNode root = snw.kookbc.util.JacksonUtil.parse(rawStr); + JsonNode root = JacksonUtil.parse(rawStr); array = root.get("data"); } catch (BadResponseException e) { if (e.getCode() == 40300) { // 40300, so we should throw IllegalStateException diff --git a/src/main/java/snw/kookbc/interfaces/AsyncHttpAPI.java b/src/main/java/snw/kookbc/interfaces/AsyncHttpAPI.java index b4a08a06..a17850f5 100644 --- a/src/main/java/snw/kookbc/interfaces/AsyncHttpAPI.java +++ b/src/main/java/snw/kookbc/interfaces/AsyncHttpAPI.java @@ -25,6 +25,7 @@ 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; @@ -87,7 +88,7 @@ * 该接口的所有方法都是线程安全的,可以在多线程环境中安全使用。 * * @since KookBC 0.33.0 - * @see snw.jkook.HttpAPI + * @see HttpAPI * @see java.util.concurrent.CompletableFuture */ public interface AsyncHttpAPI { From b5cb7a4bc19194ef222b82d27efa1d5e1ddeca1b Mon Sep 17 00:00:00 2001 From: RealSeek <1536266519@qq.com> Date: Sun, 16 Nov 2025 19:42:41 +0800 Subject: [PATCH 04/10] i18n(logs): localize all log messages to chinese - translate all existing english log messages to chinese for better user experience - ensure all system and debug outputs are consistently localized --- src/main/java/snw/kookbc/impl/KBCClient.java | 62 +++++++++---------- .../impl/command/CommandManagerImpl.java | 22 +++---- .../litecommands/result/ResultTypes.java | 2 +- .../java/snw/kookbc/impl/console/Console.java | 6 +- .../java/snw/kookbc/impl/entity/UserImpl.java | 4 +- .../entity/channel/ThreadChannelImpl.java | 4 +- .../impl/entity/thread/ThreadPostImpl.java | 2 +- .../impl/entity/thread/ThreadReplyImpl.java | 2 +- .../snw/kookbc/impl/event/EventFactory.java | 10 +-- .../java/snw/kookbc/impl/network/Bucket.java | 4 +- .../impl/network/IgnoreSNListenerImpl.java | 2 +- .../snw/kookbc/impl/network/ListenerImpl.java | 34 +++++----- .../kookbc/impl/network/NetworkClient.java | 8 +-- .../network/webhook/JLHttpRequestWrapper.java | 2 +- .../webhook/JLHttpWebhookNetworkSystem.java | 6 +- .../snw/kookbc/impl/network/ws/Connector.java | 6 +- .../impl/network/ws/ReconnectStrategy.java | 2 +- .../impl/plugin/SimplePluginManager.java | 4 +- .../impl/scheduler/AfterPluginInitTask.java | 2 +- .../kookbc/impl/scheduler/SchedulerImpl.java | 4 +- .../impl/tasks/BotMarketPingThread.java | 4 +- .../kookbc/impl/tasks/StopSignalListener.java | 2 +- .../snw/kookbc/impl/tasks/UpdateChecker.java | 34 +++++----- 23 files changed, 114 insertions(+), 114 deletions(-) diff --git a/src/main/java/snw/kookbc/impl/KBCClient.java b/src/main/java/snw/kookbc/impl/KBCClient.java index 77a4dbf7..e0aa9ada 100644 --- a/src/main/java/snw/kookbc/impl/KBCClient.java +++ b/src/main/java/snw/kookbc/impl/KBCClient.java @@ -146,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); } @@ -172,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); } } @@ -261,11 +261,11 @@ public synchronized void start() { getStorage().addUser(botUser); core.setUser(botUser); registerInternal(); - getCore().getLogger().debug("Enabling plugins"); + getCore().getLogger().debug("正在启用插件"); enablePlugins(); getCore().getLogger().info("正在运行延迟初始化任务"); ((SchedulerImpl) core.getScheduler()).runAfterPluginInitTasks(); - getCore().getLogger().debug("Starting Network"); + getCore().getLogger().debug("正在启动网络"); startNetwork(); finishStart(); getCore().getLogger().info("完成!输入 \"help\" 获取帮助。"); @@ -295,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; @@ -315,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(); @@ -325,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; @@ -361,7 +361,7 @@ private void enablePlugins(@Nullable List plugins) { try { plugin.onLoad(); } catch (Throwable e) { - plugin.getLogger().error("Unable to load this plugin", e); + plugin.getLogger().error("无法加载此插件", e); iterator.remove(); } // end onLoad @@ -373,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; @@ -415,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 @@ -428,28 +428,28 @@ 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. @@ -561,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..1a9ff5d3 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/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/UserImpl.java b/src/main/java/snw/kookbc/impl/entity/UserImpl.java index b2e5b538..9b37e57b 100644 --- a/src/main/java/snw/kookbc/impl/entity/UserImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/UserImpl.java @@ -440,10 +440,10 @@ public Map calculateChannel(Channel channel) { try { return calculateDefaultPerms(perm, channel, userRoleIds, finalGuildRoles); } catch (BadResponseException e) { - client.getCore().getLogger().error("Error occurred while calculating built-in permissions", e); + client.getCore().getLogger().error("计算内置权限时发生错误", e); return false; } catch (Exception e) { - client.getCore().getLogger().error("Error occurred while calculating built-in permissions", e); + client.getCore().getLogger().error("计算内置权限时发生错误", e); return false; } } diff --git a/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java b/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java index bdc92d93..314d422d 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java @@ -150,7 +150,7 @@ public ThreadPost getThreadPost(String threadId) { return new ThreadPostImpl(client, this, response); } catch (Exception e) { // 帖子不存在或访问出错 - client.getCore().getLogger().warn("Failed to get thread post: " + threadId, e); + client.getCore().getLogger().warn("获取帖子失败: " + threadId, e); return null; } } @@ -180,7 +180,7 @@ public Collection getCategories() { return categories; } catch (Exception e) { - client.getCore().getLogger().warn("Failed to get thread categories", e); + client.getCore().getLogger().warn("获取帖子分类失败", e); return java.util.Collections.emptyList(); } } diff --git a/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java b/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java index 8d248f25..14e70891 100644 --- a/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java @@ -156,7 +156,7 @@ public synchronized void update(JsonNode data) { } } catch (Exception e) { // 解析失败时记录日志并设置为 null - client.getCore().getLogger().warn("Failed to parse thread post content: {}", e.getMessage()); + client.getCore().getLogger().warn("解析帖子内容失败: {}", e.getMessage()); this.content = null; } } else { diff --git a/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java b/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java index e3878fc3..c67cf9fd 100644 --- a/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java @@ -131,7 +131,7 @@ public synchronized void update(JsonNode data) { } } catch (Exception e) { // 解析失败时记录日志并设置为 null - client.getCore().getLogger().warn("Failed to parse thread reply content: {}", e.getMessage()); + client.getCore().getLogger().warn("解析帖子回复内容失败: {}", e.getMessage()); this.content = null; } } else { diff --git a/src/main/java/snw/kookbc/impl/event/EventFactory.java b/src/main/java/snw/kookbc/impl/event/EventFactory.java index 1b0d7583..56a22597 100644 --- a/src/main/java/snw/kookbc/impl/event/EventFactory.java +++ b/src/main/java/snw/kookbc/impl/event/EventFactory.java @@ -80,7 +80,7 @@ public Event createEvent(JsonNode object) { try { return jacksonMapper.readValue(object.toString(), GuildUserNickNameUpdateEvent.class); } catch (Exception e) { - client.getCore().getLogger().warn("Failed to parse GuildUserNickNameUpdateEvent with Jackson", e); + client.getCore().getLogger().warn("使用 Jackson 解析 GuildUserNickNameUpdateEvent 失败", e); return null; } } @@ -93,15 +93,15 @@ public Event createEvent(JsonNode object) { return result; } } catch (Exception e) { - client.getCore().getLogger().error("Failed to deserialize event of type {}: {}", + client.getCore().getLogger().error("反序列化类型为 {} 的事件失败: {}", eventType.getSimpleName(), e.getMessage()); - client.getCore().getLogger().debug("Event JSON: {}", object); + client.getCore().getLogger().debug("事件 JSON: {}", object); } // 如果 Jackson 反序列化失败,记录错误 if (!(eventType == ChannelInfoUpdateEvent.class)) { - client.getCore().getLogger().error("We cannot understand the frame."); - client.getCore().getLogger().error("Frame content: {}", object); + client.getCore().getLogger().error("无法理解此数据帧"); + client.getCore().getLogger().error("数据帧内容: {}", object); } return null; } diff --git a/src/main/java/snw/kookbc/impl/network/Bucket.java b/src/main/java/snw/kookbc/impl/network/Bucket.java index a0a12d0b..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; diff --git a/src/main/java/snw/kookbc/impl/network/IgnoreSNListenerImpl.java b/src/main/java/snw/kookbc/impl/network/IgnoreSNListenerImpl.java index 19709f7b..17fe1c2f 100644 --- a/src/main/java/snw/kookbc/impl/network/IgnoreSNListenerImpl.java +++ b/src/main/java/snw/kookbc/impl/network/IgnoreSNListenerImpl.java @@ -53,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 1abf5904..ea4f22c7 100644 --- a/src/main/java/snw/kookbc/impl/network/ListenerImpl.java +++ b/src/main/java/snw/kookbc/impl/network/ListenerImpl.java @@ -62,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()) { @@ -76,21 +76,21 @@ 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.getCore().getLogger().info("恢复完成"); JsonNode sessionIdNode = frame.getData().get("session_id"); if (sessionIdNode != null) { client.getSession().setId(sessionIdNode.asText()); @@ -101,14 +101,14 @@ public void handle(Frame frame) { 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(); 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("意外的错误 SN,期望 {},实际收到 {}", expected, actual); session.getBuffer().add(frame); } else if (expected == actual) { event0(frame); @@ -123,10 +123,10 @@ protected void event(Frame frame) { session.increaseSN(); saveSN(); continueId++; - client.getCore().getLogger().debug("Processed buffered message with SN {}", bufFrame.getSN()); + 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."); + client.getCore().getLogger().warn("收到来自远程的意外旧消息,已丢弃"); } } } @@ -148,8 +148,8 @@ protected void event0(Frame frame) { 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,13 +173,13 @@ 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); JsonNode object = frame.getData(); JsonNode codeNode = object.get("code"); @@ -268,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 6615a43e..93c77bc2 100644 --- a/src/main/java/snw/kookbc/impl/network/NetworkClient.java +++ b/src/main/java/snw/kookbc/impl/network/NetworkClient.java @@ -88,7 +88,7 @@ public NetworkClient(KBCClient kbcClient, String token) { .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()); } @@ -147,7 +147,7 @@ public int getActiveConnectionCount() { */ public void evictIdleConnections() { connectionPool.evictAll(); - kbcClient.getCore().getLogger().debug("Evicted all idle connections from connection pool"); + kbcClient.getCore().getLogger().debug("已从连接池中清理所有空闲连接"); } // Jackson API - 高性能JSON处理 @@ -203,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; @@ -227,7 +227,7 @@ 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); } 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 30680d2b..8ed1b55e 100644 --- a/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequestWrapper.java +++ b/src/main/java/snw/kookbc/impl/network/webhook/JLHttpRequestWrapper.java @@ -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/ws/Connector.java b/src/main/java/snw/kookbc/impl/network/ws/Connector.java index 0261f8ba..25ff6035 100644 --- a/src/main/java/snw/kookbc/impl/network/ws/Connector.java +++ b/src/main/java/snw/kookbc/impl/network/ws/Connector.java @@ -142,9 +142,9 @@ 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); } } @@ -221,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"); diff --git a/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java b/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java index d702aa7c..7d276917 100644 --- a/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java +++ b/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java @@ -93,7 +93,7 @@ public class ReconnectStrategy { * 创建重连策略(无限重试) */ public ReconnectStrategy() { - // 无需初始化,重连将持���到连接成功或遇到不可恢复的异常 + // 无需初始化,重连将持续到连接成功或遇到不可恢复的异常 } /** diff --git a/src/main/java/snw/kookbc/impl/plugin/SimplePluginManager.java b/src/main/java/snw/kookbc/impl/plugin/SimplePluginManager.java index 7ab92259..311bf2e8 100644 --- a/src/main/java/snw/kookbc/impl/plugin/SimplePluginManager.java +++ b/src/main/java/snw/kookbc/impl/plugin/SimplePluginManager.java @@ -234,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 } } @@ -253,7 +253,7 @@ 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 { 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 2e7ed0bf..2cbb9b70 100644 --- a/src/main/java/snw/kookbc/impl/scheduler/SchedulerImpl.java +++ b/src/main/java/snw/kookbc/impl/scheduler/SchedulerImpl.java @@ -135,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); @@ -163,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/tasks/BotMarketPingThread.java b/src/main/java/snw/kookbc/impl/tasks/BotMarketPingThread.java index e836eb62..332f582b 100644 --- a/src/main/java/snw/kookbc/impl/tasks/BotMarketPingThread.java +++ b/src/main/java/snw/kookbc/impl/tasks/BotMarketPingThread.java @@ -56,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); } } @@ -92,7 +92,7 @@ public void run0() throws InterruptedException { 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 ed9998c9..d7651f29 100644 --- a/src/main/java/snw/kookbc/impl/tasks/UpdateChecker.java +++ b/src/main/java/snw/kookbc/impl/tasks/UpdateChecker.java @@ -45,7 +45,7 @@ public void run() { try { run0(); } catch (Exception e) { - client.getCore().getLogger().warn("Unable to check update from remote.", e); + client.getCore().getLogger().warn("无法从远程检查更新", e); } } @@ -73,10 +73,10 @@ private void run0() throws Exception { String errorMessage = messageNode.asText(); if (errorMessage.contains("API rate limit exceeded")) { client.getCore().getLogger() - .warn("Cannot check update! GitHub API rate limit exceeded. Please try again later."); + .warn("无法检查更新!GitHub API 请求频率限制已超出,请稍后重试"); } else { client.getCore().getLogger() - .warn("Cannot check update! GitHub API returned error: {}", errorMessage); + .warn("无法检查更新!GitHub API 返回错误: {}", errorMessage); } return; } @@ -84,7 +84,7 @@ private void run0() throws Exception { JsonNode tagNameNode = resObj.get("tag_name"); if (tagNameNode == null || tagNameNode.isNull()) { client.getCore().getLogger() - .warn("Cannot check update! GitHub API response missing 'tag_name' field. API format may have changed."); + .warn("无法检查更新!GitHub API 响应缺少 'tag_name' 字段,API 格式可能已更改"); return; } @@ -99,31 +99,31 @@ 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()); JsonNode nameNode = resObj.get("name"); - String releaseName = nameNode != null && !nameNode.isNull() ? nameNode.asText() : "Unknown"; - client.getCore().getLogger().info("Release Title: {}", releaseName); + 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() : "Unknown"; - client.getCore().getLogger().info("Release Time: {}", publishedAt); + String publishedAt = publishedAtNode != null && !publishedAtNode.isNull() ? publishedAtNode.asText() : "未知"; + client.getCore().getLogger().info("发布时间: {}", publishedAt); - client.getCore().getLogger().info("Release message is following:"); + client.getCore().getLogger().info("发布说明如下:"); JsonNode bodyNode = resObj.get("body"); - String releaseBody = bodyNode != null && !bodyNode.isNull() ? bodyNode.asText() : "No release notes available"; + 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; } @@ -132,12 +132,12 @@ private void run0() throws Exception { 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; } From fc5df4c55a781b1e884939ba7a7994df82950053 Mon Sep 17 00:00:00 2001 From: RealSeek <1536266519@qq.com> Date: Sun, 16 Nov 2025 19:48:55 +0800 Subject: [PATCH 05/10] refactor(network): delegate event processing to executor - execute event handling asynchronously to prevent blocking the current thread - improve responsiveness by offloading event processing to a dedicated executor --- src/main/java/snw/kookbc/impl/network/ListenerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/snw/kookbc/impl/network/ListenerImpl.java b/src/main/java/snw/kookbc/impl/network/ListenerImpl.java index ea4f22c7..038f491f 100644 --- a/src/main/java/snw/kookbc/impl/network/ListenerImpl.java +++ b/src/main/java/snw/kookbc/impl/network/ListenerImpl.java @@ -70,7 +70,7 @@ public void handle(Frame frame) { } switch (frame.getType()) { case EVENT: - event(frame); // 直接在当前线程处理,保证顺序 + client.getEventExecutor().execute(() -> event(frame)); break; case HELLO: hello(frame); From fbde845bd5b7b56304cb77a42386ee8627836c3a Mon Sep 17 00:00:00 2001 From: RealSeek <1536266519@qq.com> Date: Sun, 16 Nov 2025 19:54:35 +0800 Subject: [PATCH 06/10] build(deps): add gson dependency - introduce gson 2.10.1 as a shadow dependency - previously removed, gson is now required for specific functionalities or compatibility --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 23af08c7..9c262424 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,7 +37,7 @@ dependencies { shadowApi(libs.net.kyori.event.api) shadowApi(libs.net.kyori.event.method) shadowApi(libs.net.freeutils.jlhttp) - // GSON 已移除 - 项目已完全迁移到 Jackson (v0.52.0+) + 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") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7adbf530..7728472f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] com-github-ben-manes-caffeine-caffeine = "2.9.3" +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" @@ -27,6 +28,7 @@ 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-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" } From 54d424ad329d8d39f548751633934c75bbfd337e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ZX=E5=A4=8F=E5=A4=9C=E4=B9=8B=E9=A3=8E?= Date: Sun, 23 Nov 2025 20:16:46 +0800 Subject: [PATCH 07/10] fix: apply suggestions from code review Co-authored-by: huanmeng_qwq <1871735932@qq.com> --- .../java/snw/kookbc/impl/command/CommandManagerImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/snw/kookbc/impl/command/CommandManagerImpl.java b/src/main/java/snw/kookbc/impl/command/CommandManagerImpl.java index 1a9ff5d3..1dec0d71 100644 --- a/src/main/java/snw/kookbc/impl/command/CommandManagerImpl.java +++ b/src/main/java/snw/kookbc/impl/command/CommandManagerImpl.java @@ -208,10 +208,10 @@ public boolean executeCommand(CommandSender sender, String cmdLine, Message msg) if (sender instanceof User) { if (msg == null) { - client.getCore().getLogger().warn("用户发出命令但消息对象为空,是插件以用户身份调用命令吗?"); + client.getCore().getLogger().warn("用户执行命令但消息对象为空,是插件以用户身份调用命令吗?"); } client.getCore().getLogger().info( - "{}(用户 ID: {}) 发出命令: {}", + "{}(用户 ID: {}) 执行命令: {}", ((User) sender).getName(), ((User) sender).getId(), cmdLine @@ -439,14 +439,14 @@ private void exec(Runnable runnable, long startTimeStamp, String cmdLine) throws try { runnable.run(); } catch (Throwable e) { - client.getCore().getLogger().debug("命令行 '{}' 执行失败,用时: {}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("命令行 \"{}\" 执行完成,用时: {}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) { From fd28633cce27aa0a1b5a2c04a4049984684da41f Mon Sep 17 00:00:00 2001 From: RealSeek <1536266519@qq.com> Date: Thu, 27 Nov 2025 21:57:44 +0800 Subject: [PATCH 08/10] docs(javadoc): update since version for BaseJacksonEventDeserializer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - change version in @since tag from 0.32.2 to 0.33.0 fix(network): correct typo in reconnect strategy log message - change "无��重试模式" to "无限重试模式" (meaning "Infinite Retry Mode") --- src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java | 2 +- .../serializer/event/jackson/BaseJacksonEventDeserializer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java b/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java index 7d276917..0ceb0469 100644 --- a/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java +++ b/src/main/java/snw/kookbc/impl/network/ws/ReconnectStrategy.java @@ -333,7 +333,7 @@ public String getStatisticsReport() { """ 重连统计报告: =========================================== - 当前重试: %d (无��重试模式) + 当前重试: %d (无限重试模式) 总重连次数: %d 成功重连: %d 失败重连: %d 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 index bce218da..1c008910 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/BaseJacksonEventDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/BaseJacksonEventDeserializer.java @@ -35,7 +35,7 @@ *

提供事件反序列化的通用逻辑和工具方法。 * * @param 事件类型 - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public abstract class BaseJacksonEventDeserializer extends JsonDeserializer { From e9c7960218759ef29dcb12894902a891f52678b8 Mon Sep 17 00:00:00 2001 From: huanmeng-qwq <1871735932@qq.com> Date: Fri, 28 Nov 2025 13:50:27 +0800 Subject: [PATCH 09/10] Bump version to 0.33.0 --- build.gradle.kts | 2 +- .../java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java | 2 +- .../java/snw/kookbc/impl/entity/thread/ThreadCategoryImpl.java | 2 +- src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java | 2 +- .../java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java | 2 +- src/main/java/snw/kookbc/impl/pageiter/ThreadPostIterator.java | 2 +- src/main/java/snw/kookbc/impl/pageiter/ThreadReplyIterator.java | 2 +- .../kookbc/impl/serializer/event/jackson/JKookEventModule.java | 2 +- .../jackson/channel/ChannelCreateEventJacksonDeserializer.java | 2 +- .../jackson/channel/ChannelDeleteEventJacksonDeserializer.java | 2 +- .../channel/ChannelInfoUpdateEventJacksonDeserializer.java | 2 +- .../channel/ChannelMessageDeleteEventJacksonDeserializer.java | 2 +- .../jackson/channel/ChannelMessageEventJacksonDeserializer.java | 2 +- .../channel/ChannelMessagePinEventJacksonDeserializer.java | 2 +- .../channel/ChannelMessageUnpinEventJacksonDeserializer.java | 2 +- .../channel/ChannelMessageUpdateEventJacksonDeserializer.java | 2 +- .../jackson/guild/GuildAddEmojiEventJacksonDeserializer.java | 2 +- .../jackson/guild/GuildBanUserEventJacksonDeserializer.java | 2 +- .../jackson/guild/GuildDeleteEventJacksonDeserializer.java | 2 +- .../jackson/guild/GuildInfoUpdateEventJacksonDeserializer.java | 2 +- .../jackson/guild/GuildRemoveEmojiEventJacksonDeserializer.java | 2 +- .../jackson/guild/GuildUnbanUserEventJacksonDeserializer.java | 2 +- .../jackson/guild/GuildUpdateEmojiEventJacksonDeserializer.java | 2 +- .../guild/GuildUserNickNameUpdateEventJacksonDeserializer.java | 2 +- .../jackson/item/ItemConsumedEventJacksonDeserializer.java | 2 +- .../pm/PrivateMessageDeleteEventJacksonDeserializer.java | 2 +- .../pm/PrivateMessageReceivedEventJacksonDeserializer.java | 2 +- .../pm/PrivateMessageUpdateEventJacksonDeserializer.java | 2 +- .../event/jackson/role/RoleCreateEventJacksonDeserializer.java | 2 +- .../event/jackson/role/RoleDeleteEventJacksonDeserializer.java | 2 +- .../jackson/role/RoleInfoUpdateEventJacksonDeserializer.java | 2 +- .../jackson/user/UserAddReactionEventJacksonDeserializer.java | 2 +- .../jackson/user/UserClickButtonEventJacksonDeserializer.java | 2 +- .../jackson/user/UserInfoUpdateEventJacksonDeserializer.java | 2 +- .../jackson/user/UserJoinGuildEventJacksonDeserializer.java | 2 +- .../user/UserJoinVoiceChannelEventJacksonDeserializer.java | 2 +- .../jackson/user/UserLeaveGuildEventJacksonDeserializer.java | 2 +- .../user/UserLeaveVoiceChannelEventJacksonDeserializer.java | 2 +- .../event/jackson/user/UserOfflineEventJacksonDeserializer.java | 2 +- .../event/jackson/user/UserOnlineEventJacksonDeserializer.java | 2 +- .../user/UserRemoveReactionEventJacksonDeserializer.java | 2 +- src/main/java/snw/kookbc/test/JacksonCardSerializationTest.java | 2 +- 42 files changed, 42 insertions(+), 42 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9c262424..f293a02c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { compileOnly(libs.org.jetbrains.annotations) } group = "io.github.snwcreations" -version = "0.32.2" +version = "0.33.0" description = "KookBC" java.sourceCompatibility = JavaVersion.VERSION_21 java.targetCompatibility = JavaVersion.VERSION_21 diff --git a/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java b/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java index 314d422d..257bac22 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java @@ -61,7 +61,7 @@ * 每日12:00北京时间重置) * * @see Kook 帖子频道文档 - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ThreadChannelImpl extends NonCategoryChannelImpl implements ThreadChannel { diff --git a/src/main/java/snw/kookbc/impl/entity/thread/ThreadCategoryImpl.java b/src/main/java/snw/kookbc/impl/entity/thread/ThreadCategoryImpl.java index b976af2d..e6950cbb 100644 --- a/src/main/java/snw/kookbc/impl/entity/thread/ThreadCategoryImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadCategoryImpl.java @@ -37,7 +37,7 @@ *

代表帖子频道中的分类,用于组织和管理帖子 * * @see ThreadCategory - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ThreadCategoryImpl implements ThreadCategory { diff --git a/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java b/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java index 14e70891..948cb745 100644 --- a/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadPostImpl.java @@ -50,7 +50,7 @@ *

代表帖子频道中的一个帖子,支持富文本内容、回复和统计信息 * * @see ThreadPost - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ThreadPostImpl implements ThreadPost { diff --git a/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java b/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java index c67cf9fd..833011e2 100644 --- a/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/thread/ThreadReplyImpl.java @@ -46,7 +46,7 @@ *

代表帖子频道中对主贴的回复,支持富文本内容和嵌套回复(楼中楼) * * @see ThreadReply - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ThreadReplyImpl implements ThreadReply { diff --git a/src/main/java/snw/kookbc/impl/pageiter/ThreadPostIterator.java b/src/main/java/snw/kookbc/impl/pageiter/ThreadPostIterator.java index 265999bd..77894271 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/ThreadPostIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/ThreadPostIterator.java @@ -37,7 +37,7 @@ * *

用于获取帖子频道中的帖子列表 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ThreadPostIterator extends PageIteratorImpl> { diff --git a/src/main/java/snw/kookbc/impl/pageiter/ThreadReplyIterator.java b/src/main/java/snw/kookbc/impl/pageiter/ThreadReplyIterator.java index 92397d09..e69f9c12 100644 --- a/src/main/java/snw/kookbc/impl/pageiter/ThreadReplyIterator.java +++ b/src/main/java/snw/kookbc/impl/pageiter/ThreadReplyIterator.java @@ -35,7 +35,7 @@ * *

用于获取帖子的回复列表 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ThreadReplyIterator extends PageIteratorImpl> { 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 index 6ee62f22..2ca56982 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/JKookEventModule.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/JKookEventModule.java @@ -55,7 +55,7 @@ *

  • 完全替代 GSON 反序列化器
  • * * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 * @see com.fasterxml.jackson.databind.module.SimpleModule */ public class JKookEventModule extends SimpleModule { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelCreateEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelCreateEventJacksonDeserializer.java index b11604d3..526d5e04 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelCreateEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelCreateEventJacksonDeserializer.java @@ -27,7 +27,7 @@ /** * ChannelCreateEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ChannelCreateEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelDeleteEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelDeleteEventJacksonDeserializer.java index 51dfd595..96390156 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelDeleteEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelDeleteEventJacksonDeserializer.java @@ -27,7 +27,7 @@ /** * ChannelDeleteEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ChannelDeleteEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelInfoUpdateEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelInfoUpdateEventJacksonDeserializer.java index 0ef30036..9a51db52 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelInfoUpdateEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelInfoUpdateEventJacksonDeserializer.java @@ -30,7 +30,7 @@ /** * ChannelInfoUpdateEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ChannelInfoUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageDeleteEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageDeleteEventJacksonDeserializer.java index 01860d86..e1a71fff 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageDeleteEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageDeleteEventJacksonDeserializer.java @@ -27,7 +27,7 @@ /** * ChannelMessageDeleteEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ChannelMessageDeleteEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageEventJacksonDeserializer.java index e175c85e..529178e4 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageEventJacksonDeserializer.java @@ -28,7 +28,7 @@ /** * ChannelMessageEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ChannelMessageEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessagePinEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessagePinEventJacksonDeserializer.java index ea52b4e5..584f8f18 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessagePinEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessagePinEventJacksonDeserializer.java @@ -30,7 +30,7 @@ /** * ChannelMessagePinEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ChannelMessagePinEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUnpinEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUnpinEventJacksonDeserializer.java index aa9864a2..4957599b 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUnpinEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUnpinEventJacksonDeserializer.java @@ -30,7 +30,7 @@ /** * ChannelMessageUnpinEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ChannelMessageUnpinEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUpdateEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUpdateEventJacksonDeserializer.java index 87b9ac87..89b6bc95 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUpdateEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/channel/ChannelMessageUpdateEventJacksonDeserializer.java @@ -32,7 +32,7 @@ /** * ChannelMessageUpdateEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ChannelMessageUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildAddEmojiEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildAddEmojiEventJacksonDeserializer.java index eb2b7895..d2c71409 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildAddEmojiEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildAddEmojiEventJacksonDeserializer.java @@ -28,7 +28,7 @@ /** * GuildAddEmojiEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class GuildAddEmojiEventJacksonDeserializer extends BaseJacksonEventDeserializer { 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 index ac55cf85..a85cf805 100644 --- 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 @@ -33,7 +33,7 @@ /** * GuildBanUserEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class GuildBanUserEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildDeleteEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildDeleteEventJacksonDeserializer.java index 4b74e80d..78f4124d 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildDeleteEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildDeleteEventJacksonDeserializer.java @@ -26,7 +26,7 @@ /** * GuildDeleteEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class GuildDeleteEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildInfoUpdateEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildInfoUpdateEventJacksonDeserializer.java index 74022470..13505cb4 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildInfoUpdateEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildInfoUpdateEventJacksonDeserializer.java @@ -28,7 +28,7 @@ /** * GuildInfoUpdateEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class GuildInfoUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildRemoveEmojiEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildRemoveEmojiEventJacksonDeserializer.java index 3fa1a571..c9d862ad 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildRemoveEmojiEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildRemoveEmojiEventJacksonDeserializer.java @@ -28,7 +28,7 @@ /** * GuildRemoveEmojiEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class GuildRemoveEmojiEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUnbanUserEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUnbanUserEventJacksonDeserializer.java index 577542cc..935a4e83 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUnbanUserEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUnbanUserEventJacksonDeserializer.java @@ -33,7 +33,7 @@ /** * GuildUnbanUserEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class GuildUnbanUserEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUpdateEmojiEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUpdateEmojiEventJacksonDeserializer.java index 19ba6d2e..67b93b77 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUpdateEmojiEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUpdateEmojiEventJacksonDeserializer.java @@ -29,7 +29,7 @@ /** * GuildUpdateEmojiEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class GuildUpdateEmojiEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUserNickNameUpdateEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUserNickNameUpdateEventJacksonDeserializer.java index dee6fa4a..c1ccbe3c 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUserNickNameUpdateEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/guild/GuildUserNickNameUpdateEventJacksonDeserializer.java @@ -30,7 +30,7 @@ /** * GuildUserNickNameUpdateEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class GuildUserNickNameUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { 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 index 4c6bf9e3..a5e0a8e1 100644 --- 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 @@ -29,7 +29,7 @@ /** * ItemConsumedEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class ItemConsumedEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageDeleteEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageDeleteEventJacksonDeserializer.java index bf9f744d..ef6de85b 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageDeleteEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageDeleteEventJacksonDeserializer.java @@ -26,7 +26,7 @@ /** * PrivateMessageDeleteEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class PrivateMessageDeleteEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageReceivedEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageReceivedEventJacksonDeserializer.java index e76152e3..da83c799 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageReceivedEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageReceivedEventJacksonDeserializer.java @@ -28,7 +28,7 @@ /** * PrivateMessageReceivedEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class PrivateMessageReceivedEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageUpdateEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageUpdateEventJacksonDeserializer.java index ae665f8a..361527f6 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageUpdateEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/pm/PrivateMessageUpdateEventJacksonDeserializer.java @@ -26,7 +26,7 @@ /** * PrivateMessageUpdateEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class PrivateMessageUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleCreateEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleCreateEventJacksonDeserializer.java index 6edbd447..1bf92a71 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleCreateEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleCreateEventJacksonDeserializer.java @@ -28,7 +28,7 @@ /** * RoleCreateEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class RoleCreateEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleDeleteEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleDeleteEventJacksonDeserializer.java index 38253b78..67624eb4 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleDeleteEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleDeleteEventJacksonDeserializer.java @@ -28,7 +28,7 @@ /** * RoleDeleteEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class RoleDeleteEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleInfoUpdateEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleInfoUpdateEventJacksonDeserializer.java index 23a2acd8..2df7a0af 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleInfoUpdateEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/role/RoleInfoUpdateEventJacksonDeserializer.java @@ -29,7 +29,7 @@ /** * RoleInfoUpdateEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class RoleInfoUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserAddReactionEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserAddReactionEventJacksonDeserializer.java index 4a4e4411..9dc3b534 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserAddReactionEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserAddReactionEventJacksonDeserializer.java @@ -29,7 +29,7 @@ /** * UserAddReactionEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class UserAddReactionEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserClickButtonEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserClickButtonEventJacksonDeserializer.java index fcce889e..ffe28065 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserClickButtonEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserClickButtonEventJacksonDeserializer.java @@ -30,7 +30,7 @@ /** * UserClickButtonEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class UserClickButtonEventJacksonDeserializer extends BaseJacksonEventDeserializer { 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 index 98aab23f..5e1918ef 100644 --- 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 @@ -28,7 +28,7 @@ /** * UserInfoUpdateEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class UserInfoUpdateEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinGuildEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinGuildEventJacksonDeserializer.java index dac7ecde..fa76ded7 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinGuildEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinGuildEventJacksonDeserializer.java @@ -30,7 +30,7 @@ /** * UserJoinGuildEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class UserJoinGuildEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinVoiceChannelEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinVoiceChannelEventJacksonDeserializer.java index e92880a8..709652fe 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinVoiceChannelEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserJoinVoiceChannelEventJacksonDeserializer.java @@ -29,7 +29,7 @@ /** * UserJoinVoiceChannelEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class UserJoinVoiceChannelEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveGuildEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveGuildEventJacksonDeserializer.java index 74ffc9be..4b7d78f8 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveGuildEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveGuildEventJacksonDeserializer.java @@ -30,7 +30,7 @@ /** * UserLeaveGuildEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class UserLeaveGuildEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveVoiceChannelEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveVoiceChannelEventJacksonDeserializer.java index b7154dbd..d7d7b720 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveVoiceChannelEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserLeaveVoiceChannelEventJacksonDeserializer.java @@ -29,7 +29,7 @@ /** * UserLeaveVoiceChannelEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class UserLeaveVoiceChannelEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOfflineEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOfflineEventJacksonDeserializer.java index 364b48ea..0247a409 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOfflineEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOfflineEventJacksonDeserializer.java @@ -27,7 +27,7 @@ /** * UserOfflineEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class UserOfflineEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOnlineEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOnlineEventJacksonDeserializer.java index d0700783..372f5265 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOnlineEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserOnlineEventJacksonDeserializer.java @@ -27,7 +27,7 @@ /** * UserOnlineEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class UserOnlineEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserRemoveReactionEventJacksonDeserializer.java b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserRemoveReactionEventJacksonDeserializer.java index 7a11215a..cae79772 100644 --- a/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserRemoveReactionEventJacksonDeserializer.java +++ b/src/main/java/snw/kookbc/impl/serializer/event/jackson/user/UserRemoveReactionEventJacksonDeserializer.java @@ -30,7 +30,7 @@ /** * UserRemoveReactionEvent 的 Jackson 反序列化器 * - * @since KookBC 0.32.2 + * @since KookBC 0.33.0 */ public class UserRemoveReactionEventJacksonDeserializer extends BaseJacksonEventDeserializer { diff --git a/src/main/java/snw/kookbc/test/JacksonCardSerializationTest.java b/src/main/java/snw/kookbc/test/JacksonCardSerializationTest.java index 9b9d6ce9..9fe041ac 100644 --- a/src/main/java/snw/kookbc/test/JacksonCardSerializationTest.java +++ b/src/main/java/snw/kookbc/test/JacksonCardSerializationTest.java @@ -53,7 +53,7 @@ public static void main(String[] args) { .addModule(new SectionModule(new MarkdownElement("(/)**help**: 此命令没有简介。"))) .addModule(DividerModule.INSTANCE) .addModule(new ContextModule(Collections.singletonList( - new MarkdownElement("由 [KookBC](https://github.com/SNWCreations/KookBC) v0.32.2 驱动 - JKook API 0.54.1") + new MarkdownElement("由 [KookBC](https://github.com/SNWCreations/KookBC) v0.33.0 驱动 - JKook API 0.54.1") ))) .build(); From dc0d6032a2e95ac9f55403a873c8d93c9f10a56b Mon Sep 17 00:00:00 2001 From: RealSeek <1536266519@qq.com> Date: Sat, 6 Dec 2025 23:08:14 +0800 Subject: [PATCH 10/10] docs(readme): update completion message translation in usage example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replace English "Done! Type 'help' for help." with Chinese localized message for clarity docs(channel): remove redundant note about daily quota limit - delete outdated quota limit note from ThreadChannelImpl documentation style(clioptions): improve Chinese warning message wording - replace “已使用” with “已启用” in logger warning for better accuracy --- README.md | 2 +- src/main/java/snw/kookbc/CLIOptions.java | 2 +- .../java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) 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/src/main/java/snw/kookbc/CLIOptions.java b/src/main/java/snw/kookbc/CLIOptions.java index 6c49c86c..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("您已使用 kookbc.nobucket 选项,我们将不会检查是否会超出速率限制!"); + logger.warn("您已启用 kookbc.nobucket 选项,我们将不会检查是否会超出速率限制!"); } } diff --git a/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java b/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java index 314d422d..8948bfb3 100644 --- a/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java +++ b/src/main/java/snw/kookbc/impl/entity/channel/ThreadChannelImpl.java @@ -57,8 +57,6 @@ *
  • 支持 @提及功能
  • * * - *

    注意: 帖子频道API有每日配额限制(每个开发者账号10000条消息/天, - * 每日12:00北京时间重置) * * @see Kook 帖子频道文档 * @since KookBC 0.32.2