mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-29 02:00:21 +08:00
[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:
parent
c016b822e7
commit
f4fa2cabf4
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
/**
|
||||
* 将一条消息设置为群精华消息, 需要管理员或群主权限.
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
24
mirai-core-mock/src/contact/essence/MockEssences.kt
Normal file
24
mirai-core-mock/src/contact/essence/MockEssences.kt
Normal 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)
|
||||
}
|
@ -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 {
|
||||
|
59
mirai-core-mock/src/internal/contact/essence/MockEssences.kt
Normal file
59
mirai-core-mock/src/internal/contact/essence/MockEssences.kt
Normal 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()
|
||||
}
|
||||
}
|
@ -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)"
|
||||
}
|
||||
|
||||
|
174
mirai-core/src/commonMain/kotlin/contact/essence/EssencesImpl.kt
Normal file
174
mirai-core/src/commonMain/kotlin/contact/essence/EssencesImpl.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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,
|
||||
|
@ -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()),
|
||||
)
|
||||
)
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user