Skip to content

Commit 38a3e40

Browse files
committed
feat(core): 实现 KookVoiceChannel, KookVoiceMember 并支持语音频道的相关操作
- Added support for managing voice channel members (e.g., move and kickout functionalities). - Implemented the KookVoiceMember interface and its core logic. - Integrated voice channel handling in the guild structure. - Updated related APIs for consistency.
1 parent 632dace commit 38a3e40

File tree

10 files changed

+232
-24
lines changed

10 files changed

+232
-24
lines changed

simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/api/channel/ChannelKickoutApi.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ public class ChannelKickoutApi private constructor(
3939
channelId: String,
4040
userId: String
4141
) : KookPostApi<Unit>() {
42-
// TODO 在 KookVoiceChannel 中添加支持的上层API
43-
4442
public companion object Factory {
4543
private val PATH = ApiPath.create("channel", "kickout")
4644

simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/api/channel/ChannelMoveUserApi.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ package love.forte.simbot.kook.api.channel
2323
import kotlinx.serialization.DeserializationStrategy
2424
import kotlinx.serialization.SerialName
2525
import kotlinx.serialization.Serializable
26-
import kotlinx.serialization.builtins.ListSerializer
27-
import kotlinx.serialization.json.JsonElement
26+
import kotlinx.serialization.builtins.serializer
2827
import love.forte.simbot.kook.api.KookPostApi
2928
import kotlin.jvm.JvmStatic
3029

@@ -40,7 +39,7 @@ import kotlin.jvm.JvmStatic
4039
public class ChannelMoveUserApi private constructor(
4140
targetId: String,
4241
userIds: List<String>
43-
) : KookPostApi<List<JsonElement>>() {
42+
) : KookPostApi<Unit>() {
4443

4544
public companion object Factory {
4645
private val PATH = ApiPath.create("channel", "move-user")
@@ -66,8 +65,8 @@ public class ChannelMoveUserApi private constructor(
6665
ChannelMoveUserApi(targetId, userIds.asList())
6766
}
6867

69-
override val resultDeserializationStrategy: DeserializationStrategy<List<JsonElement>>
70-
get() = ListSerializer(JsonElement.serializer())
68+
override val resultDeserializationStrategy: DeserializationStrategy<Unit>
69+
get() = Unit.serializer()
7170

7271
override val apiPath: ApiPath
7372
get() = PATH

simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/KookGuild.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,26 @@ public interface KookGuild : Guild, CoroutineScope, KookRoleOperator {
117117
* 获取此频道服务器内的所有聊天频道集合。
118118
*/
119119
override val chatChannels: Collectable<KookChatChannel>
120+
121+
/**
122+
* 尝试根据指定ID获取匹配的 [KookVoiceChannel]。未找到时得到null。
123+
*
124+
* @since 4.2.0
125+
* @see channel
126+
*/
127+
@ST(
128+
blockingBaseName = "getVoiceChannel",
129+
blockingSuffix = "",
130+
asyncBaseName = "getVoiceChannel",
131+
reserveBaseName = "getVoiceChannel"
132+
)
133+
public suspend fun voiceChannel(id: ID): KookVoiceChannel?
134+
135+
/**
136+
* 获取此频道服务器内的所有语音频道集合。
137+
*/
138+
public val voiceChannels: Collectable<KookVoiceChannel>
139+
120140
// categories
121141

122142
/**

simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/KookVoiceChannel.kt

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,56 @@
2020

2121
package love.forte.simbot.component.kook
2222

23+
import love.forte.simbot.common.collectable.Collectable
24+
import love.forte.simbot.common.id.ID
25+
import love.forte.simbot.suspendrunner.ST
26+
2327
/**
2428
* 一个 KOOK 中的语音子频道。
2529
*
30+
* 语音子频道也是 [KookChatChannel] 的一种,因为它也可以发送消息。
31+
*
2632
* @since 4.2.0
2733
* @author ForteScarlet
2834
*/
2935
public interface KookVoiceChannel : KookChatChannel {
30-
// TODO 相关整合
36+
/**
37+
* 获取当前语音频道中在线的用户列表。
38+
*/
39+
public val members: Collectable<KookVoiceMember>
40+
41+
/**
42+
* 语音频道之间移动用户
43+
*
44+
* 只能在语音频道之间移动,用户也必须在其他语音频道在线才能够移动到目标频道。
45+
*
46+
* @throws love.forte.simbot.kook.api.ApiResponseException 如果API的相应结果不是正确结果
47+
* @throws love.forte.simbot.kook.api.ApiResultException 如果API的相应结果不是正确结果
48+
*/
49+
@ST
50+
public suspend fun moveMember(targetChannel: ID, vararg targetMembers: ID) {
51+
moveMember(targetChannel, targetMembers.asList())
52+
}
53+
54+
/**
55+
* 语音频道之间移动用户
56+
*
57+
* 只能在语音频道之间移动,用户也必须在其他语音频道在线才能够移动到目标频道。
58+
*
59+
* @throws love.forte.simbot.kook.api.ApiResponseException 如果API的相应结果不是正确结果
60+
* @throws love.forte.simbot.kook.api.ApiResultException 如果API的相应结果不是正确结果
61+
*/
62+
@ST
63+
public suspend fun moveMember(targetChannel: ID, targetMembers: Iterable<ID>)
64+
65+
/**
66+
* 踢出语音频道中的用户。
67+
*
68+
* @see KookVoiceMember.kickout
69+
*
70+
* @throws love.forte.simbot.kook.api.ApiResponseException 如果API的相应结果不是正确结果
71+
* @throws love.forte.simbot.kook.api.ApiResultException 如果API的相应结果不是正确结果
72+
*/
73+
@ST
74+
public suspend fun kickoutMember(targetMember: ID)
3175
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2025. ForteScarlet.
3+
*
4+
* This file is part of simbot-component-kook.
5+
*
6+
* simbot-component-kook is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Lesser General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* simbot-component-kook is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with simbot-component-kook,
18+
* If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package love.forte.simbot.component.kook
22+
23+
import love.forte.simbot.common.id.ID
24+
25+
/**
26+
* Kook 语音频道内的成员。
27+
* @since 4.2.0
28+
* @author ForteScarlet
29+
*/
30+
public interface KookVoiceMember : KookMember {
31+
/**
32+
* 语音频道之间移动用户
33+
*
34+
* 只能在语音频道之间移动,用户也必须在其他语音频道在线才能够移动到目标频道。
35+
*
36+
* @throws love.forte.simbot.kook.api.ApiResponseException 如果API的相应结果不是正确结果
37+
* @throws love.forte.simbot.kook.api.ApiResultException 如果API的相应结果不是正确结果
38+
*/
39+
public suspend fun move(targetChannel: ID)
40+
41+
/**
42+
* 将成员踢出语音频道。
43+
*
44+
* @throws love.forte.simbot.kook.api.ApiResponseException 如果API的相应结果不是正确结果
45+
* @throws love.forte.simbot.kook.api.ApiResultException 如果API的相应结果不是正确结果
46+
*/
47+
public suspend fun kickout()
48+
}

simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/bot/internal/KookBotImpl.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import love.forte.simbot.common.id.literal
3636
import love.forte.simbot.component.kook.KookChannel
3737
import love.forte.simbot.component.kook.KookChatChannel
3838
import love.forte.simbot.component.kook.KookComponent
39+
import love.forte.simbot.component.kook.KookVoiceChannel
3940
import love.forte.simbot.component.kook.bot.KookBot
4041
import love.forte.simbot.component.kook.bot.KookBotConfiguration
4142
import love.forte.simbot.component.kook.bot.KookContactRelation
@@ -152,12 +153,15 @@ internal class KookBotImpl(
152153

153154
private val internalCache = InternalCache()
154155

155-
internal fun internalGuild(guildId: String) = internalCache.guilds[guildId]
156-
internal fun internalChatChannel(channelId: String) = internalCache.channels[channelId]
157-
internal fun internalCategory(categoryId: String) = internalCache.categories[categoryId]
156+
internal fun internalGuild(guildId: String): KookGuildImpl? = internalCache.guilds[guildId]
157+
internal fun internalChatChannel(channelId: String): KookChatChannel? = internalCache.channels[channelId]
158+
internal fun internalVoiceChannel(channelId: String): KookVoiceChannel? = internalChatChannel(channelId) as? KookVoiceChannel?
159+
internal fun internalCategory(categoryId: String): KookCategoryChannelImpl? = internalCache.categories[categoryId]
158160
internal fun internalChatChannels(guildId: String): Sequence<KookChatChannel> =
159161
internalCache.channels.values.asSequence().filter { it.source.guildId == guildId }
160162

163+
internal fun internalVoiceChannels(guildId: String): Sequence<KookVoiceChannel> =
164+
internalChatChannels(guildId).filterIsInstance<KookVoiceChannel>()
161165

162166
internal fun internalMembers(guildId: String): Sequence<KookMemberImpl> {
163167
return internalCache.members.entries.asSequence().filter { it.key.guildId == guildId }.map { it.value }

simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/internal/KookGuildImpl.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ internal class KookGuildImpl(
8181
override suspend fun chatChannel(id: ID): KookChatChannel? =
8282
bot.internalChatChannel(id.literal)
8383

84+
override suspend fun voiceChannel(id: ID): KookVoiceChannel? =
85+
bot.internalVoiceChannel(id.literal)
86+
87+
override val voiceChannels: Collectable<KookVoiceChannel>
88+
get() = bot.internalVoiceChannels(source.id).asCollectable()
89+
8490
@ExperimentalSimbotAPI
8591
override val categories: Collectable<KookCategoryChannel>
8692
get() = bot.internalCategories(source.id).asCollectable()

simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/internal/KookVoiceChannelImpl.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,22 @@
2020

2121
package love.forte.simbot.component.kook.internal
2222

23+
import kotlinx.coroutines.flow.asFlow
24+
import kotlinx.coroutines.flow.emitAll
25+
import kotlinx.coroutines.flow.mapNotNull
26+
import love.forte.simbot.common.collectable.Collectable
27+
import love.forte.simbot.common.collectable.flowCollectable
28+
import love.forte.simbot.common.id.ID
29+
import love.forte.simbot.common.id.literal
2330
import love.forte.simbot.component.kook.KookVoiceChannel
31+
import love.forte.simbot.component.kook.KookVoiceMember
2432
import love.forte.simbot.component.kook.bot.internal.KookBotImpl
33+
import love.forte.simbot.component.kook.util.requestData
34+
import love.forte.simbot.kook.api.channel.ChannelKickoutApi
35+
import love.forte.simbot.kook.api.channel.ChannelMoveUserApi
36+
import love.forte.simbot.kook.api.channel.GetChannelUserListApi
2537
import love.forte.simbot.kook.objects.Channel
38+
import love.forte.simbot.logger.LoggerFactory
2639
import kotlin.coroutines.CoroutineContext
2740

2841
/**
@@ -35,9 +48,43 @@ internal class KookVoiceChannelImpl(
3548
source: Channel,
3649
) : AbstractKookChatCapableChannelImpl(bot, source),
3750
KookVoiceChannel {
51+
companion object {
52+
private val logger = LoggerFactory.getLogger("love.forte.simbot.component.kook.KookVoiceChannel")
53+
}
54+
3855
override val coroutineContext: CoroutineContext
3956
get() = bot.subContext
4057

58+
override suspend fun kickoutMember(targetMember: ID) {
59+
val api = ChannelKickoutApi.create(source.id, targetMember.literal)
60+
bot.requestData(api)
61+
}
62+
63+
override val members: Collectable<KookVoiceMember>
64+
get() = flowCollectable {
65+
val api = GetChannelUserListApi.create(source.id)
66+
val userList = bot.requestData(api)
67+
val bot = this@KookVoiceChannelImpl.bot
68+
69+
emitAll(userList.asFlow().mapNotNull {
70+
val delegateMember = bot.internalMember(guildId = source.guildId, userId = it.id)
71+
logger.trace("Processing voice channel user-list(id={})'s delegate member: {}", it.id, delegateMember)
72+
if (delegateMember == null) {
73+
logger.warn("Delegate member for voice channel's user(id={}) is null.", it)
74+
return@mapNotNull null
75+
}
76+
it.toVoiceMember(bot, source.id, delegateMember)
77+
})
78+
}
79+
80+
override suspend fun moveMember(
81+
targetChannel: ID,
82+
targetMembers: Iterable<ID>
83+
) {
84+
val api = ChannelMoveUserApi.create(targetChannel.literal, targetMembers.map { it.literal })
85+
bot.requestData(api)
86+
}
87+
4188
override fun toString(): String {
4289
return "KookVoiceChannel(id=${source.id}, name=${source.name}, guildId=${source.guildId})"
4390
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2025. ForteScarlet.
3+
*
4+
* This file is part of simbot-component-kook.
5+
*
6+
* simbot-component-kook is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Lesser General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* simbot-component-kook is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with simbot-component-kook,
18+
* If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package love.forte.simbot.component.kook.internal
22+
23+
import love.forte.simbot.common.id.ID
24+
import love.forte.simbot.common.id.literal
25+
import love.forte.simbot.component.kook.KookMember
26+
import love.forte.simbot.component.kook.KookVoiceMember
27+
import love.forte.simbot.component.kook.bot.internal.KookBotImpl
28+
import love.forte.simbot.component.kook.util.requestDataBy
29+
import love.forte.simbot.kook.api.channel.ChannelKickoutApi
30+
import love.forte.simbot.kook.api.channel.ChannelMoveUserApi
31+
import love.forte.simbot.kook.api.channel.VoiceChannelUser
32+
33+
/**
34+
*
35+
* @author ForteScarlet
36+
*/
37+
internal class KookVoiceMemberImpl(
38+
private val bot: KookBotImpl,
39+
private val delegate: KookMember,
40+
private val voiceChannelId: String,
41+
private val voiceChannelUser: VoiceChannelUser
42+
) : KookVoiceMember, KookMember by delegate {
43+
44+
override suspend fun move(targetChannel: ID) {
45+
ChannelMoveUserApi.create(targetChannel.literal, voiceChannelUser.id).requestDataBy(bot)
46+
}
47+
48+
override suspend fun kickout() {
49+
ChannelKickoutApi.create(voiceChannelId, voiceChannelUser.id).requestDataBy(bot)
50+
}
51+
}
52+
53+
54+
internal fun VoiceChannelUser.toVoiceMember(bot: KookBotImpl, voiceChannelId: String, delegate: KookMember): KookVoiceMemberImpl =
55+
KookVoiceMemberImpl(bot, delegate, voiceChannelId, this)

simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/internal/Utils.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ package love.forte.simbot.component.kook.internal
2323
import love.forte.simbot.ability.DeleteOption
2424
import love.forte.simbot.ability.StandardDeleteOption
2525
import love.forte.simbot.component.kook.KookCategory
26-
import love.forte.simbot.component.kook.KookChannel
2726
import love.forte.simbot.component.kook.KookChatChannel
2827
import love.forte.simbot.component.kook.bot.KookBot
2928
import love.forte.simbot.component.kook.bot.internal.KookBotImpl
@@ -54,18 +53,6 @@ internal fun Channel.category(bot: KookBotImpl): KookCategory? {
5453
?.let { bot.internalCategory(it) }?.category
5554
}
5655

57-
/**
58-
* 将Channel对象转换为对应的KookChannel实现
59-
* @return 根据频道类型返回分类频道或聊天频道
60-
*/
61-
internal fun Channel.toChannel(bot: KookBotImpl): KookChannel {
62-
if (isCategory) {
63-
return toCategoryChannel(bot, sourceCategory = toCategory(bot))
64-
}
65-
66-
return toChatChannel(bot, typeValueOrNull)
67-
}
68-
6956

7057
/**
7158
* 根据频道类型将 Channel 转换为对应的聊天频道实现

0 commit comments

Comments
 (0)