diff --git a/build.gradle.kts b/build.gradle.kts index 72300f4e..fd4e3647 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -79,7 +79,9 @@ apiValidation { "love.forte.simbot.annotations.InternalSimbotAPI", "love.forte.simbot.kook.ExperimentalKookApi", "love.forte.simbot.kook.InternalKookApi", - "love.forte.simbot.kook.api.template.ExperimentalTemplateApi" + "love.forte.simbot.kook.api.template.ExperimentalTemplateApi", + // blacklist APIs + "love.forte.simbot.component.kook.blacklist.ExperimentalBlacklistApi" ), ) diff --git a/buildSrc/src/main/kotlin/P.kt b/buildSrc/src/main/kotlin/P.kt index 830caff4..41005dd3 100644 --- a/buildSrc/src/main/kotlin/P.kt +++ b/buildSrc/src/main/kotlin/P.kt @@ -35,7 +35,7 @@ object P : ProjectDetail() { get() = HOMEPAGE const val VERSION = "4.3.0" - const val NEXT_VERSION = "4.3.1" + const val NEXT_VERSION = "4.4.0" override val snapshotVersion = "$NEXT_VERSION-SNAPSHOT" override val version = if (isSnapshot()) snapshotVersion else VERSION diff --git a/simbot-component-kook-api/src/jvmMain/java/module-info.java b/simbot-component-kook-api/src/jvmMain/java/module-info.java index 3fd909c1..dc2f39d5 100644 --- a/simbot-component-kook-api/src/jvmMain/java/module-info.java +++ b/simbot-component-kook-api/src/jvmMain/java/module-info.java @@ -16,14 +16,19 @@ exports love.forte.simbot.kook; exports love.forte.simbot.kook.api; exports love.forte.simbot.kook.api.asset; + exports love.forte.simbot.kook.api.blacklist; + exports love.forte.simbot.kook.api.category; exports love.forte.simbot.kook.api.channel; exports love.forte.simbot.kook.api.guild; exports love.forte.simbot.kook.api.invite; exports love.forte.simbot.kook.api.member; exports love.forte.simbot.kook.api.message; exports love.forte.simbot.kook.api.role; + exports love.forte.simbot.kook.api.template; + exports love.forte.simbot.kook.api.thread; exports love.forte.simbot.kook.api.user; exports love.forte.simbot.kook.api.userchat; + exports love.forte.simbot.kook.api.voice; exports love.forte.simbot.kook.event; exports love.forte.simbot.kook.messages; exports love.forte.simbot.kook.objects; diff --git a/simbot-component-kook-core/api/simbot-component-kook-core.api b/simbot-component-kook-core/api/simbot-component-kook-core.api index 7312d871..bef2274e 100644 --- a/simbot-component-kook-core/api/simbot-component-kook-core.api +++ b/simbot-component-kook-core/api/simbot-component-kook-core.api @@ -318,6 +318,9 @@ public abstract interface class love/forte/simbot/component/kook/KookVoiceMember public fun moveReserve (Llove/forte/simbot/common/id/ID;)Llove/forte/simbot/suspendrunner/reserve/SuspendReserve; } +public abstract interface annotation class love/forte/simbot/component/kook/blacklist/ExperimentalBlacklistApi : java/lang/annotation/Annotation { +} + public abstract interface class love/forte/simbot/component/kook/bot/KookBot : kotlinx/coroutines/CoroutineScope, love/forte/simbot/bot/Bot { public fun cancel (Ljava/lang/Throwable;)V public fun getAvatar ()Ljava/lang/String; diff --git a/simbot-component-kook-core/build.gradle.kts b/simbot-component-kook-core/build.gradle.kts index 25b58ece..31032fa7 100644 --- a/simbot-component-kook-core/build.gradle.kts +++ b/simbot-component-kook-core/build.gradle.kts @@ -48,6 +48,7 @@ kotlin { optIn.addAll( "love.forte.simbot.kook.ExperimentalKookApi", "love.forte.simbot.kook.InternalKookApi", + "love.forte.simbot.component.kook.blacklist.ExperimentalBlacklistApi" ) } @@ -80,6 +81,7 @@ kotlin { implementation(libs.simbot.api) implementation(libs.simbot.core) implementation(libs.simbot.common.core) + implementation(libs.ktor.client.mock) } jvmTest.dependencies { diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/KookGuild.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/KookGuild.kt index bdb771d2..64e173b0 100644 --- a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/KookGuild.kt +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/KookGuild.kt @@ -25,6 +25,8 @@ import love.forte.simbot.annotations.ExperimentalSimbotAPI import love.forte.simbot.common.collectable.Collectable import love.forte.simbot.common.id.ID import love.forte.simbot.common.id.StringID.Companion.ID +import love.forte.simbot.component.kook.blacklist.ExperimentalBlacklistApi +import love.forte.simbot.component.kook.blacklist.KookGuildBlacklistOperator import love.forte.simbot.component.kook.role.KookGuildRole import love.forte.simbot.component.kook.role.KookGuildRoleCreator import love.forte.simbot.component.kook.role.KookRole @@ -207,4 +209,15 @@ public interface KookGuild : Guild, CoroutineScope, KookRoleOperator { @ExperimentalSimbotAPI override fun roleCreator(): KookGuildRoleCreator //endregion + + // Blacklist + + /** + * 获取针对当前频道服务器的黑名单操作器。 + * + * @since 4.4.0 + * @see KookGuildBlacklistOperator + */ + @ExperimentalBlacklistApi + public val blacklist: KookGuildBlacklistOperator } diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/ExperimentalBlacklistApi.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/ExperimentalBlacklistApi.kt new file mode 100644 index 00000000..9d6f8b34 --- /dev/null +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/ExperimentalBlacklistApi.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025. ForteScarlet. + * + * This file is part of simbot-component-kook. + * + * simbot-component-kook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * simbot-component-kook 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . + */ + +package love.forte.simbot.component.kook.blacklist + +/** + * 与频道服务器黑名单列表相关的试验性 API。 + * 这些 API 仍处于试验性阶段,可能会随时被更改、删除,不保证稳定性。 + * + * @since 4.4.0 + * @author ForteScarlet + */ +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) +@MustBeDocumented +@RequiresOptIn( + message = "与频道服务器黑名单列表相关的试验性 API。" + + "这些 API 仍处于试验性阶段,可能会随时被更改、删除,不保证稳定性。", + level = RequiresOptIn.Level.WARNING +) +public annotation class ExperimentalBlacklistApi diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/KookBlacklistItem.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/KookBlacklistItem.kt new file mode 100644 index 00000000..55a49149 --- /dev/null +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/KookBlacklistItem.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025. ForteScarlet. + * + * This file is part of simbot-component-kook. + * + * simbot-component-kook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * simbot-component-kook 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . + */ + +package love.forte.simbot.component.kook.blacklist + +import love.forte.simbot.ability.DeleteOption +import love.forte.simbot.ability.DeleteSupport +import love.forte.simbot.ability.StandardDeleteOption +import love.forte.simbot.common.id.ID +import love.forte.simbot.common.time.Timestamp +import love.forte.simbot.kook.objects.User +import love.forte.simbot.suspendrunner.ST + +/** + * KOOK 服务器黑名单列表中的一个元素。 + * + * @since 4.4.0 + * + * @author ForteScarlet + */ +@ExperimentalBlacklistApi +public interface KookBlacklistItem : DeleteSupport { + /** + * 用户 ID + */ + public val userId: ID + + /** + * 服务器 ID + */ + public val guildId: ID + + /** + * 加入黑名单的时间 + */ + public val createdTime: Timestamp + + /** + * 加入黑名单的原因 + */ + public val remark: String + + /** + * 用户信息。 + */ + public val userInfo: User + + /** + * 将此人移出黑名单列表。 + * + * @throws RuntimeException 如果在请求API过程中出现任何非预期异常, + * 并且没有提供 [StandardDeleteOption.IGNORE_ON_FAILURE] + */ + @ST + override suspend fun delete(vararg options: DeleteOption) +} \ No newline at end of file diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/KookGuildBlacklistOperator.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/KookGuildBlacklistOperator.kt new file mode 100644 index 00000000..c8875ccb --- /dev/null +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/KookGuildBlacklistOperator.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025. ForteScarlet. + * + * This file is part of simbot-component-kook. + * + * simbot-component-kook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * simbot-component-kook 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . + */ + +package love.forte.simbot.component.kook.blacklist + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.toList +import love.forte.simbot.ability.DeleteOption +import love.forte.simbot.ability.StandardDeleteOption +import love.forte.simbot.common.id.ID +import love.forte.simbot.kook.api.ListData +import love.forte.simbot.kook.api.blacklist.CreateBlacklistApi +import love.forte.simbot.suspendrunner.ST + +/** + * KOOK 服务器黑名单相关内容的操作器。 + * + * @since 4.4.0 + * + * @author ForteScarlet + */ +@ExperimentalBlacklistApi +public interface KookGuildBlacklistOperator { + /** + * 服务器ID + */ + public val guildId: ID + + /** + * 获取黑名单的分页列表。 + * + * @param page 页码 + * @param size 每页大小 + * @return 分页列表 + */ + @ST + public suspend fun list(page: Int?, size: Int?): ListData + + /** + * 获取全量列表数据。 + */ + @ST + public suspend fun all(): List = flow().toList() + + /** + * 获取黑名单列表元素的 Flow。 + * + * @param batchSize 每批次大小 + */ + public fun flow(batchSize: Int? = null): Flow + + /** + * 添加一个目标到黑名单。 + * + * @param guildId 服务器id + * @param targetId 目标用户id + * @param remark 加入黑名单的原因 + * @param delMsgDays 删除最近几天的消息,最大 7 天, 默认 0 + * @see CreateBlacklistApi + */ + @ST + public suspend fun add(targetId: ID, remark: String?, delMsgDays: Int?) + + /** + * 添加一个目标到黑名单。 + * + * @param guildId 服务器id + * @param targetId 目标用户id + * @see CreateBlacklistApi + */ + @ST + public suspend fun add(targetId: ID) { + add(targetId, null, null) + } + + /** + * 删除指定黑名单内的目标。 + * + * @throws RuntimeException 如果在请求API过程中出现任何非预期异常, + * 并且没有提供 [StandardDeleteOption.IGNORE_ON_FAILURE] + */ + @ST + public suspend fun delete(targetId: ID, vararg options: DeleteOption) +} \ No newline at end of file diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/internal/KookBlacklistItemImpl.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/internal/KookBlacklistItemImpl.kt new file mode 100644 index 00000000..e561b4a4 --- /dev/null +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/internal/KookBlacklistItemImpl.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025. ForteScarlet. + * + * This file is part of simbot-component-kook. + * + * simbot-component-kook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * simbot-component-kook 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . + */ + +package love.forte.simbot.component.kook.blacklist.internal + +import love.forte.simbot.ability.DeleteOption +import love.forte.simbot.common.id.ID +import love.forte.simbot.common.id.StringID.Companion.ID +import love.forte.simbot.common.time.Timestamp +import love.forte.simbot.component.kook.blacklist.KookBlacklistItem +import love.forte.simbot.component.kook.blacklist.KookGuildBlacklistOperator +import love.forte.simbot.kook.api.blacklist.BlacklistItem +import love.forte.simbot.kook.objects.User + +/** + * + * @author ForteScarlet + */ +internal class KookBlacklistItemImpl( + private val source: BlacklistItem, + override val guildId: ID, + private val operator: KookGuildBlacklistOperator +) : KookBlacklistItem { + override val userId: ID + get() = source.userId.ID + override val remark: String + get() = source.remark + override val userInfo: User + get() = source.user + + override val createdTime: Timestamp = Timestamp.ofMilliseconds(source.createdTime) + + override suspend fun delete(vararg options: DeleteOption) { + operator.delete(targetId = userId, options = options) + } + + override fun toString(): String { + return "KookBlacklistItemImpl(source=$source, guildId=$guildId, userId=$userId)" + } +} + +internal fun BlacklistItem.toKookBlacklistItem(guildId: ID, operator: KookGuildBlacklistOperator): KookBlacklistItem = + KookBlacklistItemImpl(this, guildId, operator) \ No newline at end of file diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/internal/KookGuildBlacklistOperatorImpl.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/internal/KookGuildBlacklistOperatorImpl.kt new file mode 100644 index 00000000..2a0d5df5 --- /dev/null +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/blacklist/internal/KookGuildBlacklistOperatorImpl.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025. ForteScarlet. + * + * This file is part of simbot-component-kook. + * + * simbot-component-kook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * simbot-component-kook 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . + */ + +package love.forte.simbot.component.kook.blacklist.internal + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.map +import love.forte.simbot.ability.DeleteOption +import love.forte.simbot.ability.StandardDeleteOption +import love.forte.simbot.common.id.ID +import love.forte.simbot.common.id.literal +import love.forte.simbot.component.kook.blacklist.KookBlacklistItem +import love.forte.simbot.component.kook.blacklist.KookGuildBlacklistOperator +import love.forte.simbot.component.kook.bot.KookBot +import love.forte.simbot.component.kook.util.requestData +import love.forte.simbot.kook.api.ListData +import love.forte.simbot.kook.api.blacklist.CreateBlacklistApi +import love.forte.simbot.kook.api.blacklist.DeleteBlacklistApi +import love.forte.simbot.kook.api.blacklist.GetBlacklistListApi +import love.forte.simbot.kook.api.blacklist.createFlow + +/** + * + * @author ForteScarlet + */ +internal class KookGuildBlacklistOperatorImpl( + private val bot: KookBot, + override val guildId: ID +) : KookGuildBlacklistOperator { + override suspend fun list( + page: Int?, + size: Int? + ): ListData { + val api = GetBlacklistListApi.create(guildId = guildId.literal, page = page, pageSize = size) + val raw = bot.requestData(api) + return ListData( + raw.items.map { it.toKookBlacklistItem(guildId, this) }, + raw.meta, + raw.sort + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun flow(batchSize: Int?): Flow { + return GetBlacklistListApi.createFlow { page -> + val api = GetBlacklistListApi.create(guildId = guildId.literal, page = page, pageSize = batchSize) + bot.requestData(api) + }.flatMapConcat { it.items.asFlow() } + .map { it.toKookBlacklistItem(guildId, this) } + } + + override suspend fun add(targetId: ID, remark: String?, delMsgDays: Int?) { + val api = CreateBlacklistApi.create(guildId.literal, targetId.literal, remark, delMsgDays) + bot.requestData(api) + } + + override suspend fun delete(targetId: ID, vararg options: DeleteOption) { + val api = DeleteBlacklistApi.create(guildId.literal, targetId.literal) + + if (options.contains(StandardDeleteOption.IGNORE_ON_FAILURE)) { + runCatching { bot.requestData(api) } + // Log? + return + } + + bot.requestData(api) + } +} \ No newline at end of file diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/internal/KookGuildImpl.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/internal/KookGuildImpl.kt index 89a9e9cf..98e4171c 100644 --- a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/internal/KookGuildImpl.kt +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/internal/KookGuildImpl.kt @@ -29,6 +29,9 @@ import love.forte.simbot.common.id.ID import love.forte.simbot.common.id.StringID.Companion.ID import love.forte.simbot.common.id.literal import love.forte.simbot.component.kook.* +import love.forte.simbot.component.kook.blacklist.ExperimentalBlacklistApi +import love.forte.simbot.component.kook.blacklist.KookGuildBlacklistOperator +import love.forte.simbot.component.kook.blacklist.internal.KookGuildBlacklistOperatorImpl import love.forte.simbot.component.kook.bot.internal.KookBotImpl import love.forte.simbot.component.kook.role.KookGuildRole import love.forte.simbot.component.kook.role.KookGuildRoleCreator @@ -143,6 +146,10 @@ internal class KookGuildImpl( @ExperimentalSimbotAPI override fun roleCreator(): KookGuildRoleCreator = KookGuildRoleCreatorImpl(bot, this) + @ExperimentalBlacklistApi + override val blacklist: KookGuildBlacklistOperator + get() = KookGuildBlacklistOperatorImpl(bot, id) + override fun toString(): String { return "KookGuild(id=${source.id}, name=${source.name})" } diff --git a/simbot-component-kook-core/src/commonTest/kotlin/love/forte/simbot/component/kook/blacklist/KookGuildBlacklistOperatorTest.kt b/simbot-component-kook-core/src/commonTest/kotlin/love/forte/simbot/component/kook/blacklist/KookGuildBlacklistOperatorTest.kt new file mode 100644 index 00000000..cade44d0 --- /dev/null +++ b/simbot-component-kook-core/src/commonTest/kotlin/love/forte/simbot/component/kook/blacklist/KookGuildBlacklistOperatorTest.kt @@ -0,0 +1,564 @@ +/* + * Copyright (c) 2025. ForteScarlet. + * + * This file is part of simbot-component-kook. + * + * simbot-component-kook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * simbot-component-kook 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . + */ + +package love.forte.simbot.component.kook.blacklist + +import io.ktor.client.engine.* +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import love.forte.simbot.ability.OnCompletion +import love.forte.simbot.ability.StandardDeleteOption +import love.forte.simbot.common.id.ID +import love.forte.simbot.common.id.StringID.Companion.ID +import love.forte.simbot.common.id.literal +import love.forte.simbot.component.kook.blacklist.internal.KookGuildBlacklistOperatorImpl +import love.forte.simbot.component.kook.bot.KookBot +import love.forte.simbot.kook.stdlib.BotFactory +import love.forte.simbot.kook.stdlib.BotFactory.create +import love.forte.simbot.kook.stdlib.Ticket +import love.forte.simbot.logger.Logger +import love.forte.simbot.logger.LoggerFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * 针对 [KookGuildBlacklistOperator] 功能的集成测试。 + * + * 使用 ktor-client-mock 模拟底层 HTTP 请求,验证黑名单操作的完整流程。 + * + * @author ForteScarlet + */ +class KookGuildBlacklistOperatorTest { + + /** + * 创建一个用于测试的 mock KookBot + */ + private fun createMockBot(client: HttpClientEngine): KookBot { + val sourceBot = BotFactory.create(Ticket.botWsTicket("test_client_id", "test_token")) { + clientEngine = client + wsEngine = client + } + + return object : KookBot { + override val sourceBot = sourceBot + override val id: ID = "test_bot_id".ID + override val logger: Logger = LoggerFactory.getLogger("test") + + override fun isMe(id: ID): Boolean = id.literal == this.id.literal + + override val isActive: Boolean = true + override val isCompleted: Boolean = false + override val isStarted: Boolean = true + override val component get() = error("Not implemented for test") + override val coroutineContext get() = error("Not implemented for test") + override val guildRelation get() = error("Not implemented for test") + override val contactRelation get() = error("Not implemented for test") + override suspend fun join() = error("Not implemented for test") + override fun cancel(reason: Throwable?): Unit = error("Not implemented for test") + override suspend fun start() = error("Not implemented for test") + override fun onCompletion(handle: OnCompletion) { + TODO("Not yet implemented") + } + } + } + + @Test + fun testListBlacklist() = runTest { + // Mock HTTP 响应数据 + val mockClientEngine = MockEngine { request -> + // 验证请求参数 + assertTrue(request.url.toString().contains("blacklist/list")) + assertEquals("test_guild_123", request.url.parameters["guild_id"]) + + // 返回 KOOK API 格式的响应 + respond( + content = """{ + "code": 0, + "message": "操作成功", + "data": { + "items": [ + { + "user_id": "26954123", + "created_time": 1640340668000, + "remark": "测试黑名单原因", + "user": { + "id": "26954123", + "username": "testuser", + "identify_num": "2826", + "online": true, + "status": 1, + "avatar": "avatar_url", + "vip_avatar": "vip_avatar_url", + "banner": "", + "nickname": "Test User", + "roles": [], + "is_vip": false, + "bot": false + } + } + ], + "meta": { + "page": 1, + "page_total": 1, + "page_size": 50, + "total": 1 + }, + "sort": {} + } + }""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val bot = createMockBot(mockClientEngine) + val guildId = "test_guild_123".ID + val operator = KookGuildBlacklistOperatorImpl(bot, guildId) + + // 执行测试 + val listData = operator.list(page = null, size = null) + + // 验证结果 + assertNotNull(listData) + assertEquals(1, listData.items.size) + assertEquals(1, listData.meta.total) + + val item = listData.items[0] + assertEquals("26954123".ID, item.userId) + assertEquals(guildId, item.guildId) + assertEquals("测试黑名单原因", item.remark) + assertEquals(1640340668000L, item.createdTime.milliseconds) + + val user = item.userInfo + assertEquals("26954123", user.id) + assertEquals("testuser", user.username) + assertEquals("Test User", user.nickname) + } + + @Test + fun testListBlacklistWithPagination() = runTest { + // 测试带分页参数的黑名单列表查询 + val mockClientEngine = MockEngine { request -> + assertTrue(request.url.toString().contains("blacklist/list")) + assertEquals("test_guild_456", request.url.parameters["guild_id"]) + assertEquals("2", request.url.parameters["page"]) + assertEquals("25", request.url.parameters["page_size"]) + + respond( + content = """{ + "code": 0, + "message": "操作成功", + "data": { + "items": [], + "meta": { + "page": 2, + "page_total": 1, + "page_size": 25, + "total": 0 + }, + "sort": {} + } + }""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val bot = createMockBot(mockClientEngine) + val guildId = "test_guild_456".ID + val operator = KookGuildBlacklistOperatorImpl(bot, guildId) + + val listData = operator.list(page = 2, size = 25) + + assertNotNull(listData) + assertTrue(listData.items.isEmpty()) + assertEquals(2, listData.meta.page) + assertEquals(25, listData.meta.pageSize) + } + + @Test + fun testFlowBlacklist() = runTest { + var requestCount = 0 + + // Mock 多次请求以测试 flow 分页 + val mockClientEngine = MockEngine { request -> + assertTrue(request.url.toString().contains("blacklist/list")) + requestCount++ + + // 第一页返回数据,第二页返回空 + val pageContent = if (requestCount == 1) { + """{ + "items": [ + { + "user_id": "user_001", + "created_time": 1640340668000, + "remark": "第一页用户", + "user": { + "id": "user_001", + "username": "user1", + "identify_num": "0001", + "online": false, + "status": 0, + "avatar": "", + "vip_avatar": "", + "banner": "", + "nickname": "User 1", + "roles": [], + "is_vip": false, + "bot": false + } + } + ], + "meta": { + "page": 1, + "page_total": 2, + "page_size": 1, + "total": 2 + }, + "sort": {} + }""" + } else { + """{ + "items": [ + { + "user_id": "user_002", + "created_time": 1640340669000, + "remark": "第二页用户", + "user": { + "id": "user_002", + "username": "user2", + "identify_num": "0002", + "online": false, + "status": 0, + "avatar": "", + "vip_avatar": "", + "banner": "", + "nickname": "User 2", + "roles": [], + "is_vip": false, + "bot": false + } + } + ], + "meta": { + "page": 2, + "page_total": 2, + "page_size": 1, + "total": 2 + }, + "sort": {} + }""" + } + + respond( + content = """{ + "code": 0, + "message": "操作成功", + "data": $pageContent + }""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val bot = createMockBot(mockClientEngine) + val guildId = "test_guild_flow".ID + val operator = KookGuildBlacklistOperatorImpl(bot, guildId) + + // 执行测试 + val items = operator.flow(batchSize = 1).toList() + + // 验证结果 + assertEquals(2, items.size) + assertEquals("user_001".ID, items[0].userId) + assertEquals("user_002".ID, items[1].userId) + assertEquals("第一页用户", items[0].remark) + assertEquals("第二页用户", items[1].remark) + } + + @Test + fun testAddToBlacklist() = runTest { + // 测试添加用户到黑名单 + val mockClientEngine = MockEngine { request -> + assertTrue(request.url.toString().contains("blacklist/create")) + + respond( + content = """{ + "code": 0, + "message": "操作成功", + "data": {} + }""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val bot = createMockBot(mockClientEngine) + val guildId = "test_guild_add".ID + val operator = KookGuildBlacklistOperatorImpl(bot, guildId) + + // 测试基本添加 + operator.add(targetId = "target_user_123".ID) + + // 测试带完整参数的添加 + operator.add( + targetId = "target_user_456".ID, + remark = "违规行为", + delMsgDays = 3 + ) + } + + @Test + fun testDeleteFromBlacklist() = runTest { + // 测试从黑名单中移除用户 + val mockClientEngine = MockEngine { request -> + assertTrue(request.url.toString().contains("blacklist/delete")) + + respond( + content = """{ + "code": 0, + "message": "操作成功", + "data": {} + }""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val bot = createMockBot(mockClientEngine) + val guildId = "test_guild_delete".ID + val operator = KookGuildBlacklistOperatorImpl(bot, guildId) + + // 测试删除 + operator.delete(targetId = "target_user_789".ID) + } + + @Test + fun testDeleteWithIgnoreOnFailure() = runTest { + // 测试使用 IGNORE_ON_FAILURE 选项删除 + var requestCount = 0 + + val mockClientEngine = MockEngine { request -> + requestCount++ + assertTrue(request.url.toString().contains("blacklist/delete")) + + // 模拟失败响应 + respond( + content = """{ + "code": 40000, + "message": "请求失败", + "data": {} + }""", + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val bot = createMockBot(mockClientEngine) + val guildId = "test_guild_ignore".ID + val operator = KookGuildBlacklistOperatorImpl(bot, guildId) + + // 使用 IGNORE_ON_FAILURE 选项,即使失败也不应抛出异常 + operator.delete( + targetId = "non_existent_user".ID, + StandardDeleteOption.IGNORE_ON_FAILURE + ) + + assertEquals(1, requestCount) + } + + @Test + fun testBlacklistItemDelete() = runTest { + // 测试 KookBlacklistItem 的 delete 方法 + val mockClientEngine = MockEngine { request -> + when { + request.url.toString().contains("blacklist/list") -> { + respond( + content = """{ + "code": 0, + "message": "操作成功", + "data": { + "items": [ + { + "user_id": "item_user_123", + "created_time": 1640340668000, + "remark": "待删除的黑名单项", + "user": { + "id": "item_user_123", + "username": "itemuser", + "identify_num": "1234", + "online": false, + "status": 0, + "avatar": "", + "vip_avatar": "", + "banner": "", + "nickname": "Item User", + "roles": [], + "is_vip": false, + "bot": false + } + } + ], + "meta": { + "page": 1, + "page_total": 1, + "page_size": 50, + "total": 1 + }, + "sort": {} + } + }""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + request.url.toString().contains("blacklist/delete") -> { + respond( + content = """{ + "code": 0, + "message": "操作成功", + "data": {} + }""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + else -> error("Unexpected request: ${request.url}") + } + } + + val bot = createMockBot(mockClientEngine) + val guildId = "test_guild_item".ID + val operator = KookGuildBlacklistOperatorImpl(bot, guildId) + + // 先获取黑名单列表 + val listData = operator.list(page = null, size = null) + assertEquals(1, listData.items.size) + + val item = listData.items[0] + assertEquals("item_user_123".ID, item.userId) + + // 调用 item 的 delete 方法 + item.delete() + } + + @Test + fun testAllBlacklist() = runTest { + // 测试获取全量黑名单列表 + var requestCount = 0 + + val mockClientEngine = MockEngine { request -> + assertTrue(request.url.toString().contains("blacklist/list")) + requestCount++ + + // 第一页返回2条数据,第二页返回空 + val pageContent = if (requestCount == 1) { + """{ + "items": [ + { + "user_id": "all_user_001", + "created_time": 1640340668000, + "remark": "用户1", + "user": { + "id": "all_user_001", + "username": "user1", + "identify_num": "0001", + "online": false, + "status": 0, + "avatar": "", + "vip_avatar": "", + "banner": "", + "nickname": "User 1", + "roles": [], + "is_vip": false, + "bot": false + } + }, + { + "user_id": "all_user_002", + "created_time": 1640340669000, + "remark": "用户2", + "user": { + "id": "all_user_002", + "username": "user2", + "identify_num": "0002", + "online": false, + "status": 0, + "avatar": "", + "vip_avatar": "", + "banner": "", + "nickname": "User 2", + "roles": [], + "is_vip": false, + "bot": false + } + } + ], + "meta": { + "page": 1, + "page_total": 1, + "page_size": 50, + "total": 2 + }, + "sort": {} + }""" + } else { + """{ + "items": [], + "meta": { + "page": 2, + "page_total": 1, + "page_size": 50, + "total": 2 + }, + "sort": {} + }""" + } + + respond( + content = """{ + "code": 0, + "message": "操作成功", + "data": $pageContent + }""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val bot = createMockBot(mockClientEngine) + val guildId = "test_guild_all".ID + val operator = KookGuildBlacklistOperatorImpl(bot, guildId) + + // 执行测试 + val allItems = operator.all() + + // 验证结果 + assertEquals(2, allItems.size) + assertEquals("all_user_001".ID, allItems[0].userId) + assertEquals("all_user_002".ID, allItems[1].userId) + } +} diff --git a/simbot-component-kook-core/src/jvmMain/java/module-info.java b/simbot-component-kook-core/src/jvmMain/java/module-info.java index 559cbabb..4c26fdc8 100644 --- a/simbot-component-kook-core/src/jvmMain/java/module-info.java +++ b/simbot-component-kook-core/src/jvmMain/java/module-info.java @@ -36,6 +36,7 @@ exports love.forte.simbot.component.kook.event; exports love.forte.simbot.component.kook.message; exports love.forte.simbot.component.kook.role; + exports love.forte.simbot.component.kook.blacklist; exports love.forte.simbot.component.kook.util; // provider diff --git a/simbot-component-kook-stdlib/src/commonMain/kotlin/love/forte/simbot/kook/stdlib/internal/ActualEnumMap.kt b/simbot-component-kook-stdlib/src/commonMain/kotlin/love/forte/simbot/kook/stdlib/internal/ActualEnumMap.kt index 9d3e2479..20b2e33f 100644 --- a/simbot-component-kook-stdlib/src/commonMain/kotlin/love/forte/simbot/kook/stdlib/internal/ActualEnumMap.kt +++ b/simbot-component-kook-stdlib/src/commonMain/kotlin/love/forte/simbot/kook/stdlib/internal/ActualEnumMap.kt @@ -1,18 +1,21 @@ /* - * Copyright (c) 2023. ForteScarlet. + * Copyright (c) 2023-2025. ForteScarlet. * - * This file is part of simbot-component-kook. + * This file is part of simbot-component-kook. * - * simbot-component-kook is free software: you can redistribute it and/or modify it under the terms of - * the GNU Lesser General Public License as published by the Free Software Foundation, - * either version 3 of the License, or (at your option) any later version. + * simbot-component-kook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * simbot-component-kook 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 Lesser General Public License for more details. + * simbot-component-kook 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 Lesser General Public License for more details. * - * You should have received a copy of the GNU Lesser General Public License along with simbot-component-kook, - * If not, see . + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . */ package love.forte.simbot.kook.stdlib.internal @@ -40,12 +43,12 @@ internal class ActualEnumMap, V> private constructor(private val val } /** - * @throws IndexOutOfBoundsException if key is out of range + * @throws IndexOutOfBoundsException if [key] is out of range */ operator fun get(key: E): V = values[key.ordinal] /** - * @throws IndexOutOfBoundsException if key is out of range + * @throws IndexOutOfBoundsException if [key] is out of range */ operator fun set(key: E, value: V) { values[key.ordinal] = value