[core] feat: essence message setting (#2314)

* feat: remove essence message

* feat: Essences

* add: share and remove

* fix: impl

* fix: arguments

* feat: image url to image

* add: doc

* fix: doc

* Copyright: 2023

* remove: method removeEssenceMessage

* feat: lazy load source

* add: no parse

* add: sendAndExpect try

* fix: remove throw

* fix: parse IMAGE_MD5_REGEX
This commit is contained in:
cssxsh 2023-03-21 22:53:23 +08:00 committed by GitHub
parent c016b822e7
commit f4fa2cabf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 690 additions and 9 deletions

View File

@ -435,6 +435,7 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin
public abstract fun getBotAsMember ()Lnet/mamoe/mirai/contact/NormalMember;
public fun getBotMuteRemaining ()I
public fun getBotPermission ()Lnet/mamoe/mirai/contact/MemberPermission;
public abstract fun getEssences ()Lnet/mamoe/mirai/contact/essence/Essences;
public abstract fun getId ()J
public abstract fun getMembers ()Lnet/mamoe/mirai/contact/ContactList;
public abstract fun getName ()Ljava/lang/String;
@ -954,6 +955,32 @@ public final class net/mamoe/mirai/contact/announcement/OnlineAnnouncementKt {
public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot;
}
public final class net/mamoe/mirai/contact/essence/EssenceMessageRecord {
public final fun getFullSource ()Lnet/mamoe/mirai/message/data/MessageSource;
public final fun getFullSource (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getGroup ()Lnet/mamoe/mirai/contact/Group;
public final fun getOperator ()Lnet/mamoe/mirai/contact/NormalMember;
public final fun getOperatorId ()J
public final fun getOperatorNick ()Ljava/lang/String;
public final fun getOperatorTime ()I
public final fun getSender ()Lnet/mamoe/mirai/contact/NormalMember;
public final fun getSenderId ()J
public final fun getSenderNick ()Ljava/lang/String;
public final fun getSenderTime ()I
public final fun getSource ()Lnet/mamoe/mirai/message/data/MessageSource;
public final fun getSource (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun toString ()Ljava/lang/String;
}
public abstract interface class net/mamoe/mirai/contact/essence/Essences : net/mamoe/mirai/utils/Streamable {
public fun getPage (II)Ljava/util/List;
public abstract fun getPage (IILkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun remove (Lnet/mamoe/mirai/message/data/MessageSource;)V
public abstract fun remove (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun share (Lnet/mamoe/mirai/message/data/MessageSource;)Ljava/lang/String;
public abstract fun share (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFile : net/mamoe/mirai/contact/file/AbsoluteFileFolder {
public abstract fun getExpiryTime ()J
public abstract fun getMd5 ()[B

View File

@ -435,6 +435,7 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin
public abstract fun getBotAsMember ()Lnet/mamoe/mirai/contact/NormalMember;
public fun getBotMuteRemaining ()I
public fun getBotPermission ()Lnet/mamoe/mirai/contact/MemberPermission;
public abstract fun getEssences ()Lnet/mamoe/mirai/contact/essence/Essences;
public abstract fun getId ()J
public abstract fun getMembers ()Lnet/mamoe/mirai/contact/ContactList;
public abstract fun getName ()Ljava/lang/String;
@ -954,6 +955,32 @@ public final class net/mamoe/mirai/contact/announcement/OnlineAnnouncementKt {
public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot;
}
public final class net/mamoe/mirai/contact/essence/EssenceMessageRecord {
public final fun getFullSource ()Lnet/mamoe/mirai/message/data/MessageSource;
public final fun getFullSource (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getGroup ()Lnet/mamoe/mirai/contact/Group;
public final fun getOperator ()Lnet/mamoe/mirai/contact/NormalMember;
public final fun getOperatorId ()J
public final fun getOperatorNick ()Ljava/lang/String;
public final fun getOperatorTime ()I
public final fun getSender ()Lnet/mamoe/mirai/contact/NormalMember;
public final fun getSenderId ()J
public final fun getSenderNick ()Ljava/lang/String;
public final fun getSenderTime ()I
public final fun getSource ()Lnet/mamoe/mirai/message/data/MessageSource;
public final fun getSource (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun toString ()Ljava/lang/String;
}
public abstract interface class net/mamoe/mirai/contact/essence/Essences : net/mamoe/mirai/utils/Streamable {
public fun getPage (II)Ljava/util/List;
public abstract fun getPage (IILkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun remove (Lnet/mamoe/mirai/message/data/MessageSource;)V
public abstract fun remove (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun share (Lnet/mamoe/mirai/message/data/MessageSource;)Ljava/lang/String;
public abstract fun share (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFile : net/mamoe/mirai/contact/file/AbsoluteFileFolder {
public abstract fun getExpiryTime ()J
public abstract fun getMd5 ()[B

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -17,6 +17,7 @@ import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.active.GroupActive
import net.mamoe.mirai.contact.announcement.Announcements
import net.mamoe.mirai.contact.essence.Essences
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.contact.roaming.RoamingSupported
import net.mamoe.mirai.event.events.*
@ -229,6 +230,13 @@ public interface Group : Contact, CoroutineScope, FileSupported, AudioSupported,
*/
public suspend fun setEssenceMessage(source: MessageSource): Boolean
/**
* 群精华消息相关功能接口
*
* @since 2.15
*/
public val essences: Essences
public companion object {
/**
* 将一条消息设置为群精华消息, 需要管理员或群主权限.

View File

@ -0,0 +1,70 @@
/*
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.contact.essence
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.NormalMember
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.MiraiInternalApi
/**
* 精华消息记录
* @since 2.15
* @param group 记录的群聊
* @param sender 消息的发送者
* @param senderId 消息的发送者的ID
* @param senderNick 消息的发送者的Nick
* @param senderTime 消息的发送的时间 *
* @param operator 设置精华的操作者
* @param operatorId 设置精华的操作者的ID
* @param operatorNick 设置精华的操作者的Nick
* @param operatorTime 设置精华的时间
*/
public class EssenceMessageRecord @MiraiInternalApi constructor(
public val group: Group,
public val sender: NormalMember?,
public val senderId: Long,
public val senderNick: String,
public val senderTime: Int,
public val operator: NormalMember?,
public val operatorId: Long,
public val operatorNick: String,
public val operatorTime: Int,
private val loadMessageSource: suspend (parse: Boolean) -> MessageSource
) {
override fun toString(): String {
return "EssenceMessageRecord(group=${group}, sender=${senderNick}(${senderId}), senderTime=${senderTime}, operator=${operatorNick}(${operatorId}), operatorTime=${operatorTime})"
}
/**
* 获取消息源
*
* 其中的 [MessageSource.originalMessage] 将会尝试以加载为原消息格式
*
* **注意** 当精华消息中包含 图片 会尝试将其下载然后重新上传, 以保证可用性
*
* @see getSource
*/
@JvmBlockingBridge
public suspend fun getFullSource(): MessageSource {
return loadMessageSource(true)
}
/**
* 获取消息源
*
* @see getFullSource
*/
@JvmBlockingBridge
public suspend fun getSource(): MessageSource {
return loadMessageSource(false)
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.contact.essence
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.utils.NotStableForInheritance
import net.mamoe.mirai.utils.Streamable
/**
* 表示一个群精华消息管理.
*
* ## 获取 [Essences] 实例
*
* 只可以通过 [Group.essences] 获取一个群的精华消息管理, [Essences] 实例.
*
* ### 获取精华消息列表
*
* 通过 [asFlow] `asStream` 可以获取到*惰性*, 在从流中收集数据时才会请求服务器获取数据. 通常建议在 Kotlin 使用协程的 [asFlow], Java 使用 `asStream`.
*
* 若要获取全部精华消息列表, 可使用 [toList].
*
* ### 获取精华消息分享链接
*
* 通过 [share] 可以获得一个精华消息的分享链接
*
* ### 移除精华消息
*
* 通过 [remove] 可以从列表中移除指定精华消息 (WEB API)
*
* @since 2.15
*/
@NotStableForInheritance
public interface Essences : Streamable<EssenceMessageRecord> {
/**
* 按页获取精华消息记录
* @param start 起始索引 0 开始
* @param limit 页大小 返回的记录最大数量最大取 50
* @throws IllegalStateException [limit] 过大或其他参数错误时会触发异常
*/
@JvmBlockingBridge
public suspend fun getPage(start: Int, limit: Int): List<EssenceMessageRecord>
/**
* 分享精华消息
* @param source 要分享的消息源
* @throws IllegalStateException [source] 不为精华消息时将会触发异常
* @return 分享 URL
*/
@JvmBlockingBridge
public suspend fun share(source: MessageSource): String
/**
* 移除精华消息
* @throws IllegalStateException [source] 不为精华消息或权限不足时将会触发异常
* @param source 要移除的消息源
*/
@JvmBlockingBridge
public suspend fun remove(source: MessageSource)
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.mock.contact.essence
import net.mamoe.mirai.contact.NormalMember
import net.mamoe.mirai.contact.essence.Essences
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.mock.MockBotDSL
public interface MockEssences : Essences {
/**
* 直接以 [actor] 的身份设置一条精华消息
*/
@MockBotDSL
public fun mockSetEssences(source: MessageSource, actor: NormalMember)
}

View File

@ -31,7 +31,9 @@ import net.mamoe.mirai.mock.contact.MockGroup
import net.mamoe.mirai.mock.contact.MockGroupControlPane
import net.mamoe.mirai.mock.contact.MockNormalMember
import net.mamoe.mirai.mock.contact.active.MockGroupActive
import net.mamoe.mirai.mock.contact.essence.MockEssences
import net.mamoe.mirai.mock.internal.contact.active.MockGroupActiveImpl
import net.mamoe.mirai.mock.internal.contact.essence.MockEssencesImpl
import net.mamoe.mirai.mock.internal.contact.roaming.MockRoamingMessages
import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcToGroup
import net.mamoe.mirai.mock.internal.msgsrc.newMsgSrc
@ -337,9 +339,13 @@ internal class MockGroupImpl(
resource.mockUploadVoice(bot)
override suspend fun setEssenceMessage(source: MessageSource): Boolean {
checkBotPermission(MemberPermission.ADMINISTRATOR)
essences.mockSetEssences(source, this.botAsMember)
return true
}
override val essences: MockEssences = MockEssencesImpl(this)
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"))
@Suppress("OverridingDeprecatedMember", "DEPRECATION", "DEPRECATION_ERROR")
override val filesRoot: RemoteFile by lazy {

View File

@ -0,0 +1,59 @@
/*
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.mock.internal.contact.essence
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import net.mamoe.mirai.contact.NormalMember
import net.mamoe.mirai.contact.essence.EssenceMessageRecord
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.mock.contact.essence.MockEssences
import net.mamoe.mirai.mock.internal.contact.MockGroupImpl
import net.mamoe.mirai.utils.ConcurrentHashMap
import net.mamoe.mirai.utils.currentTimeSeconds
internal class MockEssencesImpl(
private val group: MockGroupImpl
) : MockEssences {
private val cache: MutableMap<MessageSource, EssenceMessageRecord> = ConcurrentHashMap()
override fun mockSetEssences(source: MessageSource, actor: NormalMember) {
val record = EssenceMessageRecord(
group = group,
sender = group[source.fromId],
senderId = source.fromId,
senderNick = group[source.fromId]?.nick.orEmpty(),
senderTime = source.time,
operator = actor,
operatorId = actor.id,
operatorNick = actor.nick,
operatorTime = currentTimeSeconds().toInt(),
loadMessageSource = { source }
)
cache[source] = record
}
override suspend fun getPage(start: Int, limit: Int): List<EssenceMessageRecord> {
return cache.values.toList().subList(start, start + limit)
}
override suspend fun share(source: MessageSource): String {
return "https://qun.qq.com/essence/share?_wv=3&_wwv=128&_wvx=2&sharekey=..."
}
override suspend fun remove(source: MessageSource) {
cache.remove(source)
}
override fun asFlow(): Flow<EssenceMessageRecord> {
return cache.values.asFlow()
}
}

View File

@ -19,6 +19,7 @@ import net.mamoe.mirai.LowLevelApi
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.active.GroupActive
import net.mamoe.mirai.contact.announcement.Announcements
import net.mamoe.mirai.contact.essence.Essences
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.contact.roaming.RoamingMessages
import net.mamoe.mirai.data.GroupHonorType
@ -29,6 +30,7 @@ import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.contact.active.GroupActiveImpl
import net.mamoe.mirai.internal.contact.announcement.AnnouncementsImpl
import net.mamoe.mirai.internal.contact.essence.EssencesImpl
import net.mamoe.mirai.internal.contact.file.RemoteFilesImpl
import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
import net.mamoe.mirai.internal.contact.roaming.RoamingMessagesImplGroup
@ -393,6 +395,27 @@ internal abstract class CommonGroupImpl constructor(
override val roamingMessages: RoamingMessages by lazy { RoamingMessagesImplGroup(this) }
// 鉴于在 [essences] 中 有相同的功能的 Web API 所以此方法移除
// override suspend fun removeEssenceMessage(source: MessageSource): Boolean {
// checkBotPermission(MemberPermission.ADMINISTRATOR)
// val result = bot.network.sendAndExpect(
// TroopEssenceMsgManager.RemoveEssence(
// bot.client,
// this@CommonGroupImpl.uin,
// source.internalIds.first(),
// source.ids.first()
// ), 5000, 2
// )
// return result.success
// }
override val essences: Essences by lazy {
EssencesImpl(
this as GroupImpl,
bot.network.logger.subLogger("Group $id"),
)
}
override fun toString(): String = "Group($id)"
}

View File

@ -0,0 +1,174 @@
/*
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.internal.contact.essence
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonPrimitive
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.contact.checkBotPermission
import net.mamoe.mirai.contact.essence.EssenceMessageRecord
import net.mamoe.mirai.contact.essence.Essences
import net.mamoe.mirai.internal.contact.GroupImpl
import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopEssenceMsgManager
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
internal class EssencesImpl(
internal val group: GroupImpl,
internal val logger: MiraiLogger,
) : Essences {
private suspend fun parse(content: JsonObject): Message {
return when (content.getValue("msg_type").jsonPrimitive.intOrNull) {
1 -> PlainText(content = content.getValue("text").jsonPrimitive.content)
2 -> Face(id = content.getValue("face_index").jsonPrimitive.int)
3 -> {
val url = content.getValue("image_url").jsonPrimitive.content
try {
// url -> bytes -> group.upload
val bytes = group.bot.downloadEssenceMessageImage(url)
bytes.toExternalResource().use { group.uploadImage(it) }
} catch (cause: Exception) {
logger.debug({ "essence message image $url download fail." }, cause)
val match = IMAGE_MD5_REGEX.find(url) ?: return emptyMessageChain()
val (md5, ext) = match.destructured
val imageId = buildString {
append(md5)
insert(8,"-")
insert(13,"-")
insert(18,"-")
insert(23,"-")
insert(0, "{")
append("}.")
append(ext.replace("jpeg", "jpg"))
}
Image(imageId)
}
}
else -> {
// XXX: unknown message type
logger.warning { "unknown digest message type for $content" }
emptyMessageChain()
}
}
}
private fun plain(content: JsonObject): String {
return when (content.getValue("msg_type").jsonPrimitive.intOrNull) {
1 -> content.getValue("text").jsonPrimitive.content
2 -> Face(id = content.getValue("face_index").jsonPrimitive.int).content
3 -> "[图片]"
else -> ""
}
}
private suspend fun source(digests: DigestMessage, parse: Boolean): MessageSource {
return group.bot.buildMessageSource(MessageSourceKind.GROUP) {
ids = intArrayOf(digests.msgSeq)
internalIds = intArrayOf(digests.msgRandom)
time = digests.senderTime
fromId = digests.senderUin
targetId = group.id
if (parse) {
messages(digests.msgContent.map { content -> parse(content) })
} else {
messages(digests.msgContent.joinToString { content -> plain(content) }.toPlainText())
}
}
}
private fun record(digests: DigestMessage): EssenceMessageRecord {
return EssenceMessageRecord(
group = group,
sender = group[digests.senderUin],
senderId = digests.senderUin,
senderNick = digests.senderNick,
senderTime = digests.senderTime,
operator = group[digests.addDigestUin],
operatorId = digests.addDigestUin,
operatorNick = digests.addDigestNick,
operatorTime = digests.addDigestTime,
loadMessageSource = { source(digests = digests, parse = false) }
)
}
override suspend fun getPage(start: Int, limit: Int): List<EssenceMessageRecord> {
val page = group.bot.getDigestList(
groupCode = group.id,
pageStart = start,
pageLimit = limit
)
return page.messages.map(this::record)
}
override suspend fun share(source: MessageSource): String {
val share = group.bot.shareDigest(
groupCode = group.id,
msgSeq = source.ids.first(),
msgRandom = source.internalIds.first(),
targetGroupCode = 0
)
return "https://qun.qq.com/essence/share?_wv=3&_wwv=128&_wvx=2&sharekey=${share.shareKey}"
}
override suspend fun remove(source: MessageSource) {
group.checkBotPermission(MemberPermission.ADMINISTRATOR)
val result = group.bot.network.sendAndExpect(
TroopEssenceMsgManager.RemoveEssence(
group.bot.client,
group.uin,
source.internalIds.first(),
source.ids.first()
), 5000, 2
)
if (result.success.not()) {
try {
group.bot.cancelDigest(
groupCode = group.id,
msgSeq = source.ids.first(),
msgRandom = source.internalIds.first()
)
} catch (cause: IllegalStateException) {
cause.addSuppressed(IllegalStateException(result.msg))
throw cause
}
}
}
override fun asFlow(): Flow<EssenceMessageRecord> {
return flow {
var offset = 0
while (currentCoroutineContext().isActive) {
val page = group.bot.getDigestList(
groupCode = group.id,
pageStart = offset,
pageLimit = 30
)
for (message in page.messages) {
emit(record(message))
}
if (page.isEnd) break
if (page.messages.isEmpty()) break
offset += 30
}
}
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.internal.contact.essence
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.contact.active.defaultJson
import net.mamoe.mirai.internal.network.components.HttpClientProvider
import net.mamoe.mirai.internal.network.psKey
import net.mamoe.mirai.internal.network.sKey
import net.mamoe.mirai.utils.CheckableResponseA
import net.mamoe.mirai.utils.JsonStruct
import net.mamoe.mirai.utils.loadAs
@Serializable
internal data class DigestData(
@SerialName("data") val `data`: JsonElement = JsonNull,
@SerialName("wording") val reason: String = "",
@SerialName("retmsg") override val errorMessage: String,
@SerialName("retcode") override val errorCode: Int
) : CheckableResponseA(), JsonStruct
@Serializable
internal data class DigestList(
@SerialName("group_role")
val role: Int = 0,
@SerialName("is_end")
val isEnd: Boolean = false,
@SerialName("msg_list")
val messages: List<DigestMessage> = emptyList(),
@SerialName("show_tips")
val showTips: Boolean = false
)
@Serializable
internal data class DigestMessage(
@SerialName("add_digest_nick")
val addDigestNick: String = "",
@SerialName("add_digest_time")
val addDigestTime: Int = 0,
@SerialName("add_digest_uin")
val addDigestUin: Long = 0,
@SerialName("group_code")
val groupCode: String = "",
@SerialName("msg_content")
val msgContent: List<JsonObject> = emptyList(),
@SerialName("msg_random")
val msgRandom: Int = 0,
@SerialName("msg_seq")
val msgSeq: Int = 0,
@SerialName("sender_nick")
val senderNick: String = "",
@SerialName("sender_time")
val senderTime: Int = 0,
@SerialName("sender_uin")
val senderUin: Long = 0
)
internal val IMAGE_MD5_REGEX: Regex = """([0-9a-fA-F]{32})\.([0-9a-zA-Z]+)""".toRegex()
@Serializable
internal data class DigestShare(
@SerialName("share_key")
val shareKey: String = ""
)
private fun <T> DigestData.loadData(serializer: KSerializer<T>): T {
return try {
defaultJson.decodeFromJsonElement(serializer, this.data)
} catch (cause: Exception) {
throw IllegalStateException("parse digest data error, status: $errorCode - $errorMessage", cause)
}
}
internal suspend fun QQAndroidBot.getDigestList(
groupCode: Long, pageStart: Int, pageLimit: Int
): DigestList {
return components[HttpClientProvider].getHttpClient().get {
url("https://qun.qq.com/cgi-bin/group_digest/digest_list")
parameter("group_code", groupCode)
parameter("page_start", pageStart)
parameter("page_limit", pageLimit)
parameter("bkn", client.wLoginSigInfo.bkn)
headers {
// ktor bug
append(
"cookie",
"uin=o${id}; skey=${sKey}; p_uin=o${id}; p_skey=${psKey(host)};"
)
}
}.bodyAsText().loadAs(DigestData.serializer()).loadData(DigestList.serializer())
}
internal suspend fun QQAndroidBot.cancelDigest(
groupCode: Long, msgSeq: Int, msgRandom: Int
) {
val data = components[HttpClientProvider].getHttpClient().get {
url("https://qun.qq.com/cgi-bin/group_digest/cancel_digest")
parameter("group_code", groupCode)
parameter("msg_seq", msgSeq)
parameter("msg_random", msgRandom)
parameter("bkn", client.wLoginSigInfo.bkn)
headers {
// ktor bug
append(
"cookie",
"uin=o${id}; skey=${sKey}; p_uin=o${id}; p_skey=${psKey(host)};"
)
}
}.bodyAsText().loadAs(DigestData.serializer())
when (data.errorCode) {
0, 11007, 11001 -> Unit
else -> throw IllegalStateException("cancel digest error, status: ${data.errorCode} - ${data.errorMessage}, reason: ${data.reason}")
}
}
internal suspend fun QQAndroidBot.shareDigest(
groupCode: Long, msgSeq: Int, msgRandom: Int, targetGroupCode: Long
): DigestShare {
return components[HttpClientProvider].getHttpClient().get {
url("https://qun.qq.com/cgi-bin/group_digest/share_digest")
parameter("group_code", groupCode)
parameter("msg_seq", msgSeq)
parameter("msg_random", msgRandom)
parameter("target_group_code", targetGroupCode)
parameter("bkn", client.wLoginSigInfo.bkn)
headers {
// ktor bug
append(
"cookie",
"uin=o${id}; skey=${sKey}; p_uin=o${id}; p_skey=${psKey(host)};"
)
}
}.bodyAsText().loadAs(DigestData.serializer()).loadData(DigestShare.serializer())
}
internal suspend fun QQAndroidBot.downloadEssenceMessageImage(urlString: String): ByteArray {
return components[HttpClientProvider].getHttpClient().get {
url(urlString)
}.body()
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -165,6 +165,7 @@ internal object KnownPacketFactories {
TroopManagement.Kick,
TroopManagement.SwitchAnonymousChat,
TroopEssenceMsgManager.SetEssence,
TroopEssenceMsgManager.RemoveEssence,
NudgePacket,
Heartbeat.Alive,
PbMessageSvc.PbMsgWithDraw,

View File

@ -28,15 +28,16 @@ import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
* */
internal class TroopEssenceMsgManager {
internal object SetEssence : OutgoingPacketFactory<SetEssence.Response>("OidbSvc.0xeac_1") {
internal data class Response(val success: Boolean, val msg: String?) : Packet
internal data class Response(val success: Boolean, val msg: String?) : Packet
internal object SetEssence : OutgoingPacketFactory<Response>("OidbSvc.0xeac_1") {
operator fun invoke(
client: QQAndroidClient,
troopUin: Long,
msg_random: Int,
msg_seq: Int
msgRandom: Int,
msgSeq: Int
) = buildOutgoingUniPacket(client) {
writeProtoBuf(
OidbSso.OIDBSSOPkg.serializer(), OidbSso.OIDBSSOPkg(
@ -45,8 +46,40 @@ internal class TroopEssenceMsgManager {
serviceType = 1,
bodybuffer = Oidb0xeac.ReqBody(
groupCode = troopUin,
msgSeq = msg_seq.and(-1),
msgRandom = msg_random
msgSeq = msgSeq.and(-1),
msgRandom = msgRandom
).toByteArray(Oidb0xeac.ReqBody.serializer()),
)
)
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
readProtoBuf(OidbSso.OIDBSSOPkg.serializer()).let { pkg ->
pkg.bodybuffer.loadAs(Oidb0xeac.RspBody.serializer()).let { data ->
return Response(data.errorCode == 0, data.wording)
}
}
}
}
internal object RemoveEssence : OutgoingPacketFactory<Response>("OidbSvc.0xeac_2") {
operator fun invoke(
client: QQAndroidClient,
troopUin: Long,
msgRandom: Int,
msgSeq: Int
) = buildOutgoingUniPacket(client) {
writeProtoBuf(
OidbSso.OIDBSSOPkg.serializer(), OidbSso.OIDBSSOPkg(
command = 3756,
result = 0,
serviceType = 1,
bodybuffer = Oidb0xeac.ReqBody(
groupCode = troopUin,
msgSeq = msgSeq.and(-1),
msgRandom = msgRandom
).toByteArray(Oidb0xeac.ReqBody.serializer()),
)
)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.