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