1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-24 20:43:33 +08:00

Reconstruct MessageSource, fix ,

This commit is contained in:
Him188 2020-04-04 22:08:53 +08:00
parent 9f7088d4a4
commit 2b49a7586f
25 changed files with 625 additions and 869 deletions
mirai-core-qqandroid/src
androidMain/kotlin/net/mamoe/mirai/qqandroid
commonMain/kotlin/net/mamoe/mirai/qqandroid
jvmMain/kotlin/net/mamoe/mirai/qqandroid
mirai-core/src
androidMain/kotlin/net/mamoe/mirai
commonMain/kotlin/net.mamoe.mirai
jvmMain/kotlin/net/mamoe/mirai

View File

@ -1,185 +0,0 @@
/*
* Copyright 2020 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/master/LICENSE
*/
package net.mamoe.mirai.qqandroid
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.consumeEachBufferRange
import io.ktor.utils.io.core.Input
import io.ktor.utils.io.core.readBytes
import kotlinx.coroutines.io.*
import kotlinx.io.core.*
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.qqandroid.utils.ByteArrayPool
import net.mamoe.mirai.qqandroid.utils.toReadPacket
import net.mamoe.mirai.utils.MiraiInternalAPI
import java.nio.ByteBuffer
@OptIn(MiraiInternalAPI::class)
@Suppress("DEPRECATION")
internal actual fun ByteReadChannel.toKotlinByteReadChannel(): kotlinx.coroutines.io.ByteReadChannel {
return object : kotlinx.coroutines.io.ByteReadChannel {
override val availableForRead: Int
get() = this@toKotlinByteReadChannel.availableForRead
override val isClosedForRead: Boolean
get() = this@toKotlinByteReadChannel.isClosedForRead
override val isClosedForWrite: Boolean
get() = this@toKotlinByteReadChannel.isClosedForWrite
@Suppress("DEPRECATION_ERROR", "OverridingDeprecatedMember")
override var readByteOrder: ByteOrder
get() = when (this@toKotlinByteReadChannel.readByteOrder) {
io.ktor.utils.io.core.ByteOrder.BIG_ENDIAN -> ByteOrder.BIG_ENDIAN
io.ktor.utils.io.core.ByteOrder.LITTLE_ENDIAN -> ByteOrder.LITTLE_ENDIAN
}
set(value) {
this@toKotlinByteReadChannel.readByteOrder = when (value) {
ByteOrder.BIG_ENDIAN -> io.ktor.utils.io.core.ByteOrder.BIG_ENDIAN
ByteOrder.LITTLE_ENDIAN -> io.ktor.utils.io.core.ByteOrder.LITTLE_ENDIAN
}
}
@Suppress("DEPRECATION_ERROR", "DEPRECATION", "OverridingDeprecatedMember")
override val totalBytesRead: Long
get() = this@toKotlinByteReadChannel.totalBytesRead
override fun cancel(cause: Throwable?): Boolean = this@toKotlinByteReadChannel.cancel(cause)
override suspend fun consumeEachBufferRange(visitor: ConsumeEachBufferVisitor) =
this@toKotlinByteReadChannel.consumeEachBufferRange(visitor)
override suspend fun discard(max: Long): Long = this@toKotlinByteReadChannel.discard(max)
@Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE")
@ExperimentalIoApi
override fun <R> lookAhead(visitor: LookAheadSession.() -> R): R {
return this@toKotlinByteReadChannel.lookAhead l@{
visitor(object : LookAheadSession {
override fun consumed(n: Int) {
return this@l.consumed(n)
}
override fun request(skip: Int, atLeast: Int): ByteBuffer? {
return this@l.request(skip, atLeast)
}
})
}
}
@Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE")
@ExperimentalIoApi
override suspend fun <R> lookAheadSuspend(visitor: suspend LookAheadSuspendSession.() -> R): R =
this@toKotlinByteReadChannel.lookAheadSuspend l@{
visitor(object : LookAheadSuspendSession {
override suspend fun awaitAtLeast(n: Int): Boolean {
return this@l.awaitAtLeast(n)
}
override fun consumed(n: Int) {
return this@l.consumed(n)
}
override fun request(skip: Int, atLeast: Int): ByteBuffer? {
return this@l.request(skip, atLeast)
}
})
}
override suspend fun read(min: Int, consumer: (ByteBuffer) -> Unit) =
this@toKotlinByteReadChannel.read(min, consumer)
override suspend fun readAvailable(dst: ByteBuffer): Int = this@toKotlinByteReadChannel.readAvailable(dst)
override suspend fun readAvailable(dst: ByteArray, offset: Int, length: Int): Int =
this@toKotlinByteReadChannel.readAvailable(dst, offset, length)
override suspend fun readAvailable(dst: IoBuffer): Int {
ByteArrayPool.useInstance {
val read = this@toKotlinByteReadChannel.readAvailable(it, 0, it.size)
dst.writeFully(it, 0, read)
return read
}
}
override suspend fun readBoolean(): Boolean = this@toKotlinByteReadChannel.readBoolean()
override suspend fun readByte(): Byte = this@toKotlinByteReadChannel.readByte()
override suspend fun readDouble(): Double = this@toKotlinByteReadChannel.readDouble()
override suspend fun readFloat(): Float = this@toKotlinByteReadChannel.readFloat()
override suspend fun readFully(dst: ByteBuffer): Int {
TODO("not implemented")
}
override suspend fun readFully(dst: ByteArray, offset: Int, length: Int) =
this@toKotlinByteReadChannel.readFully(dst, offset, length)
override suspend fun readFully(dst: IoBuffer, n: Int) {
ByteArrayPool.useInstance {
dst.writeFully(it, 0, this.readAvailable(it, 0, it.size))
}
}
override suspend fun readInt(): Int = this@toKotlinByteReadChannel.readInt()
override suspend fun readLong(): Long = this@toKotlinByteReadChannel.readLong()
override suspend fun readPacket(size: Int, headerSizeHint: Int): ByteReadPacket {
return this@toKotlinByteReadChannel.readPacket(size, headerSizeHint).readBytes().toReadPacket()
}
override suspend fun readRemaining(limit: Long, headerSizeHint: Int): ByteReadPacket {
return this@toKotlinByteReadChannel.readRemaining(limit, headerSizeHint).readBytes().toReadPacket()
}
@OptIn(ExperimentalIoApi::class)
@ExperimentalIoApi
override fun readSession(consumer: ReadSession.() -> Unit) {
@Suppress("DEPRECATION")
this@toKotlinByteReadChannel.readSession lambda@{
consumer(object : ReadSession {
override val availableForRead: Int
get() = this@lambda.availableForRead
override fun discard(n: Int): Int = this@lambda.discard(n)
override fun request(atLeast: Int): IoBuffer? {
val ioBuffer: io.ktor.utils.io.core.IoBuffer = this@lambda.request(atLeast) ?: return null
val buffer = IoBuffer.Pool.borrow()
val bytes = (ioBuffer as Input).readBytes()
buffer.writeFully(bytes)
return buffer
}
})
}
}
override suspend fun readShort(): Short = this@toKotlinByteReadChannel.readShort()
@Suppress("EXPERIMENTAL_OVERRIDE", "EXPERIMENTAL_API_USAGE")
@ExperimentalIoApi
override suspend fun readSuspendableSession(consumer: suspend SuspendableReadSession.() -> Unit) =
this@toKotlinByteReadChannel.readSuspendableSession l@{
consumer(object : SuspendableReadSession {
override val availableForRead: Int
get() = this@l.availableForRead
override suspend fun await(atLeast: Int): Boolean = this@l.await(atLeast)
override fun discard(n: Int): Int = this@l.discard(n)
override fun request(atLeast: Int): IoBuffer? {
@Suppress("DuplicatedCode") val ioBuffer: io.ktor.utils.io.core.IoBuffer =
this@l.request(atLeast) ?: return null
val buffer = IoBuffer.Pool.borrow()
val bytes = (ioBuffer as Input).readBytes()
buffer.writeFully(bytes)
return buffer
}
})
}
override suspend fun readUTF8Line(limit: Int): String? = this@toKotlinByteReadChannel.readUTF8Line(limit)
override suspend fun <A : Appendable> readUTF8LineTo(out: A, limit: Int): Boolean =
this@toKotlinByteReadChannel.readUTF8LineTo(out, limit)
}
}

View File

@ -24,6 +24,7 @@ import kotlinx.serialization.UnstableDefault
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.int
import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotImpl
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.*
@ -35,8 +36,7 @@ import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.contact.MemberInfoImpl
import net.mamoe.mirai.qqandroid.contact.QQImpl
import net.mamoe.mirai.qqandroid.contact.checkIsGroupImpl
import net.mamoe.mirai.qqandroid.message.OnlineFriendImageImpl
import net.mamoe.mirai.qqandroid.message.OnlineGroupImageImpl
import net.mamoe.mirai.qqandroid.message.*
import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
@ -50,11 +50,22 @@ import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString
import net.mamoe.mirai.qqandroid.utils.toReadPacket
import net.mamoe.mirai.utils.*
import kotlin.collections.asSequence
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.jvm.JvmSynthetic
import kotlin.math.absoluteValue
import kotlin.random.Random
@OptIn(ExperimentalContracts::class)
internal fun Bot.asQQAndroidBot(): QQAndroidBot {
contract {
returns() implies (this@asQQAndroidBot is QQAndroidBot)
}
return this as QQAndroidBot
}
@OptIn(MiraiInternalAPI::class)
internal class QQAndroidBot constructor(
context: Context,
@ -170,69 +181,58 @@ internal abstract class QQAndroidBotBase constructor(
TODO("not implemented")
}
@Suppress("RemoveExplicitTypeArguments") // false positive
@ExperimentalMessageSource
override suspend fun recall(source: MessageSource) {
if (source.senderId != id && source.groupId != 0L) {
getGroup(source.groupId).checkBotPermissionOperator()
}
// println(source._miraiContentToString())
source.ensureSequenceIdAvailable()
network.run {
val response: PbMessageSvc.PbMsgWithDraw.Response =
if (source.groupId == 0L) {
PbMessageSvc.PbMsgWithDraw.Friend(
bot.client,
source.senderId,
source.sequenceId,
source.messageRandom,
source.time
).sendAndExpect()
} else {
MessageRecallEvent.GroupRecall(
bot,
source.senderId,
source.id,
source.time.toInt(),
null,
getGroup(source.groupId)
).broadcast()
PbMessageSvc.PbMsgWithDraw.Group(
bot.client,
source.groupId,
source.sequenceId,
source.messageRandom
).sendAndExpect()
check(source is MessageSourceImpl)
val response: PbMessageSvc.PbMsgWithDraw.Response = when (source) {
is MessageSourceToGroupImpl,
is MessageSourceFromGroupImpl
-> {
val group = when (source) {
is MessageSourceToGroupImpl -> source.target
is MessageSourceFromGroupImpl -> source.group
else -> error("stub")
}
group.checkBotPermissionOperator()
MessageRecallEvent.GroupRecall(
this,
source.senderId,
source.id,
source.time,
null,
group
).broadcast()
check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #${source.id}: $response" }
network.run {
PbMessageSvc.PbMsgWithDraw.createForGroupMessage(
bot.asQQAndroidBot().client,
group.id,
source.sequenceId,
source.id
).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
}
}
is MessageSourceFromFriendImpl,
is MessageSourceToFriendImpl
-> network.run {
PbMessageSvc.PbMsgWithDraw.createForFriendMessage(
bot.client,
source.senderId,
source.sequenceId,
source.id,
source.time
).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
}
else -> error("stub")
}
check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #${source.id}: $response" }
}
@OptIn(LowLevelAPI::class)
override suspend fun _lowLevelRecallFriendMessage(friendId: Long, messageId: Long, time: Long) {
network.run {
val response: PbMessageSvc.PbMsgWithDraw.Response =
PbMessageSvc.PbMsgWithDraw.Friend(client, friendId, (messageId shr 32).toInt(), messageId.toInt(), time)
.sendAndExpect()
check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #${messageId}: $response" }
}
}
@LowLevelAPI
override suspend fun _lowLevelRecallGroupMessage(groupId: Long, messageId: Long) {
network.run {
val response: PbMessageSvc.PbMsgWithDraw.Response =
PbMessageSvc.PbMsgWithDraw.Group(client, groupId, (messageId shr 32).toInt(), messageId.toInt())
.sendAndExpect()
check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #${messageId}: $response" }
}
}
@LowLevelAPI
@MiraiExperimentalAPI
override suspend fun _lowLevelGetAnnouncements(groupId: Long, page: Int, amount: Int): GroupAnnouncementList {
@ -382,6 +382,7 @@ internal abstract class QQAndroidBotBase constructor(
val group = getGroup(groupCode)
val time = currentTimeSeconds
message.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
network.run {
val data = message.calculateValidationDataForGroup(

View File

@ -25,7 +25,9 @@ import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendGroup
import net.mamoe.mirai.qqandroid.message.MessageSourceToGroupImpl
import net.mamoe.mirai.qqandroid.message.ensureSequenceIdAvailable
import net.mamoe.mirai.qqandroid.message.firstIsInstanceOrNull
import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
@ -303,18 +305,17 @@ internal class GroupImpl(
|| imageCount >= 4
|| (event.message.any<QuoteReply>()
&& (imageCount != 0 || length > 100))
) {
return bot.lowLevelSendLongGroupMessage(this.id, event.message)
}
) return bot.lowLevelSendLongGroupMessage(this.id, event.message)
msg = event.message
} else msg = message.asMessageChain()
msg.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
lateinit var source: MessageSourceFromSendGroup
lateinit var source: MessageSourceToGroupImpl
bot.network.run {
val response: MessageSvc.PbSendMsg.Response = MessageSvc.PbSendMsg.ToGroup(
val response: MessageSvc.PbSendMsg.Response = MessageSvc.PbSendMsg.createToGroup(
bot.client,
id,
this@GroupImpl,
msg
) {
source = it

View File

@ -22,10 +22,13 @@ import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.OfflineFriendImage
import net.mamoe.mirai.message.data.QuoteReply
import net.mamoe.mirai.message.data.asMessageChain
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.message.MessageSourceToFriendImpl
import net.mamoe.mirai.qqandroid.message.ensureSequenceIdAvailable
import net.mamoe.mirai.qqandroid.message.firstIsInstanceOrNull
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.StTroopMemberInfo
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
@ -63,12 +66,13 @@ internal class MemberImpl constructor(
if (event.isCancelled) {
throw EventCancelledException("cancelled by FriendMessageSendEvent")
}
lateinit var source: MessageSource
lateinit var source: MessageSourceToFriendImpl
event.message.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
bot.network.run {
check(
MessageSvc.PbSendMsg.ToFriend(
MessageSvc.PbSendMsg.createToFriend(
bot.client,
id,
this@MemberImpl,
event.message
) {
source = it

View File

@ -29,10 +29,13 @@ import net.mamoe.mirai.event.events.ImageUploadEvent
import net.mamoe.mirai.event.events.MessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.OfflineFriendImage
import net.mamoe.mirai.message.data.QuoteReply
import net.mamoe.mirai.message.data.asMessageChain
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.message.MessageSourceToFriendImpl
import net.mamoe.mirai.qqandroid.message.ensureSequenceIdAvailable
import net.mamoe.mirai.qqandroid.message.firstIsInstanceOrNull
import net.mamoe.mirai.qqandroid.network.highway.postImage
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x352
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
@ -80,12 +83,13 @@ internal class QQImpl(
if (event.isCancelled) {
throw EventCancelledException("cancelled by FriendMessageSendEvent")
}
lateinit var source: MessageSource
event.message.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
lateinit var source: MessageSourceToFriendImpl
bot.network.run {
check(
MessageSvc.PbSendMsg.ToFriend(
MessageSvc.PbSendMsg.createToFriend(
bot.client,
id,
this@QQImpl,
event.message
) {
source = it

View File

@ -11,104 +11,88 @@
package net.mamoe.mirai.qqandroid.message
import kotlinx.atomicfu.AtomicBoolean
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.event.subscribingGetAsync
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.messageRandom
import net.mamoe.mirai.message.data.sequenceId
import net.mamoe.mirai.message.data.OfflineMessageSource
import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgComm
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.OnlinePush
import net.mamoe.mirai.qqandroid.utils._miraiContentToString
import net.mamoe.mirai.qqandroid.utils.coerceAtMostOrFail
import net.mamoe.mirai.qqandroid.utils.io.serialization.loadAs
import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
import net.mamoe.mirai.utils.MiraiExperimentalAPI
internal class MessageSourceFromServer(
val delegate: ImMsgBody.SourceMsg
) : MessageSource {
override val time: Long get() = delegate.time.toLong() and 0xFFFFFFFF
override val originalMessage: MessageChain by lazy {
delegate.toMessageChain()
}
internal interface MessageSourceImpl {
val sequenceId: Int
override val id: Long
get() = (delegate.origSeqs?.firstOrNull()
?: error("cannot find sequenceId from ImMsgBody.SourceMsg")).toLong().shl(32) or
delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!.and(0xFFFFFFFF)
override val toUin: Long get() = delegate.toUin // always 0
override suspend fun ensureSequenceIdAvailable() {
// nothing to do
}
// override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.senderUin
override val groupId: Long get() = Group.calculateGroupCodeByGroupUin(delegate.toUin)
override fun toString(): String = ""
var isRecalledOrPlanned: Boolean
}
internal class MessageSourceFromMsg(
val delegate: MsgComm.Msg
) : MessageSource {
override val time: Long get() = delegate.msgHead.msgTime.toLong() and 0xFFFFFFFF
override val id: Long =
delegate.msgHead.msgSeq.toLong().shl(32) or
delegate.msgBody.richText.attr!!.random.toLong().and(0xFFFFFFFF)
override suspend fun ensureSequenceIdAvailable() {
// nothing to do
internal suspend inline fun MessageSource.ensureSequenceIdAvailable() {
if (this is MessageSourceToGroupImpl) {
this.ensureSequenceIdAvailable()
}
}
override val toUin: Long get() = delegate.msgHead.toUin
override val senderId: Long get() = delegate.msgHead.fromUin
override val groupId: Long get() = delegate.msgHead.groupInfo?.groupCode ?: 0
override val originalMessage: MessageChain by lazy {
delegate.toMessageChain()
}
fun toJceData(): ImMsgBody.SourceMsg {
return if (groupId == 0L) {
toJceDataImplForFriend()
} else toJceDataImplForGroup()
}
internal class MessageSourceFromFriendImpl(
override val bot: Bot,
val msg: MsgComm.Msg
) : OnlineMessageSource.Incoming.FromFriend(), MessageSourceImpl {
override val sequenceId: Int get() = msg.msgHead.msgSeq
private val isRecalled: AtomicBoolean = atomic(false)
override var isRecalledOrPlanned: Boolean
get() = isRecalled.value
set(value) {
isRecalled.value = value
}
override val id: Int get() = msg.msgBody.richText.attr!!.random
override val time: Int get() = msg.msgHead.msgTime
override val originalMessage: MessageChain by lazy { msg.toMessageChain(bot, isGroup = false, addSource = false) }
override val target: Bot get() = bot
override val sender: QQ get() = bot.getFriend(msg.msgHead.fromUin)
private val elems by lazy {
delegate.msgBody.richText.elems.toMutableList().also {
msg.msgBody.richText.elems.toMutableList().also {
if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
}
}
private fun toJceDataImplForFriend(): ImMsgBody.SourceMsg {
internal fun toJceDataImplForFriend(): ImMsgBody.SourceMsg {
return ImMsgBody.SourceMsg(
origSeqs = listOf(delegate.msgHead.msgSeq),
senderUin = delegate.msgHead.fromUin,
toUin = delegate.msgHead.toUin,
origSeqs = listOf(msg.msgHead.msgSeq),
senderUin = msg.msgHead.fromUin,
toUin = msg.msgHead.toUin,
flag = 1,
elems = delegate.msgBody.richText.elems,
elems = msg.msgBody.richText.elems,
type = 0,
time = delegate.msgHead.msgTime,
time = msg.msgHead.msgTime,
pbReserve = SourceMsg.ResvAttr(
origUids = messageRandom.toLong() and 0xffFFffFF
origUids = id.toULong().toLong()
).toByteArray(SourceMsg.ResvAttr.serializer()),
srcMsg = MsgComm.Msg(
msgHead = MsgComm.MsgHead(
fromUin = delegate.msgHead.fromUin, // qq
toUin = delegate.msgHead.toUin, // group
msgType = delegate.msgHead.msgType, // 82?
c2cCmd = delegate.msgHead.c2cCmd,
msgSeq = delegate.msgHead.msgSeq,
msgTime = delegate.msgHead.msgTime,
msgUid = messageRandom.toLong() and 0xffFFffFF, // ok
// groupInfo = MsgComm.GroupInfo(groupCode = delegate.msgHead.groupInfo.groupCode),
fromUin = msg.msgHead.fromUin, // qq
toUin = msg.msgHead.toUin, // group
msgType = msg.msgHead.msgType, // 82?
c2cCmd = msg.msgHead.c2cCmd,
msgSeq = msg.msgHead.msgSeq,
msgTime = msg.msgHead.msgTime,
msgUid = id.toULong().toLong(), // ok
// groupInfo = MsgComm.GroupInfo(groupCode = msg.msgHead.groupInfo.groupCode),
isSrcMsg = true
),
msgBody = ImMsgBody.MsgBody(
@ -119,59 +103,116 @@ internal class MessageSourceFromMsg(
).toByteArray(MsgComm.Msg.serializer())
)
}
}
private fun toJceDataImplForGroup(): ImMsgBody.SourceMsg {
internal class MessageSourceFromGroupImpl(
override val bot: Bot,
private val msg: MsgComm.Msg
) : OnlineMessageSource.Incoming.FromGroup(), MessageSourceImpl {
private val isRecalled: AtomicBoolean = atomic(false)
override var isRecalledOrPlanned: Boolean
get() = isRecalled.value
set(value) {
isRecalled.value = value
}
override val sequenceId: Int get() = msg.msgHead.msgSeq
override val id: Int get() = msg.msgBody.richText.attr!!.random
override val time: Int get() = msg.msgHead.msgTime
override val originalMessage: MessageChain by lazy { msg.toMessageChain(bot, isGroup = true, addSource = false) }
override val target: Bot get() = bot
override val sender: Member
get() = bot.getGroup(
msg.msgHead.groupInfo?.groupCode
?: error("cannot find groupCode for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
).getOrNull(msg.msgHead.fromUin)
?: error("cannot find member for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
fun toJceDataImplForGroup(): ImMsgBody.SourceMsg {
return ImMsgBody.SourceMsg(
origSeqs = listOf(delegate.msgHead.msgSeq),
senderUin = delegate.msgHead.fromUin,
origSeqs = listOf(msg.msgHead.msgSeq),
senderUin = msg.msgHead.fromUin,
toUin = 0,
flag = 1,
elems = delegate.msgBody.richText.elems,
elems = msg.msgBody.richText.elems,
type = 0,
time = delegate.msgHead.msgTime,
time = msg.msgHead.msgTime,
pbReserve = EMPTY_BYTE_ARRAY,
srcMsg = EMPTY_BYTE_ARRAY
)
}
override fun toString(): String = ""
}
internal abstract class MessageSourceFromSend : MessageSource {
internal class OfflineMessageSourceImpl( // from others' quotation
val delegate: ImMsgBody.SourceMsg, override val bot: Bot
) : OfflineMessageSource(), MessageSourceImpl {
private val isRecalled: AtomicBoolean = atomic(false)
override var isRecalledOrPlanned: Boolean
get() = isRecalled.value
set(value) {
isRecalled.value = value
}
override val sequenceId: Int
get() = delegate.origSeqs?.first() ?: error("cannot find sequenceId")
override val time: Int get() = delegate.time
override val originalMessage: MessageChain by lazy { delegate.toMessageChain(bot) }
/*
override val id: Long
get() = (delegate.origSeqs?.firstOrNull()
?: error("cannot find sequenceId from ImMsgBody.SourceMsg")).toLong().shl(32) or
delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!.and(0xFFFFFFFF)
*/
abstract override val originalMessage: MessageChain
override val id: Int
get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!
.coerceAtMostOrFail(Int.MAX_VALUE.toLong()).toInt()
fun toJceData(): ImMsgBody.SourceMsg {
return if (groupId == 0L) {
toJceDataImplForFriend()
} else toJceDataImplForGroup()
}
// override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.senderUin
override val targetId: Long get() = Group.calculateGroupCodeByGroupUin(delegate.toUin)
}
internal class MessageSourceToFriendImpl(
override val sequenceId: Int,
override val id: Int,
override val time: Int,
override val originalMessage: MessageChain,
override val sender: Bot,
override val target: QQ
) : OnlineMessageSource.Outgoing.ToFriend(), MessageSourceImpl {
override val bot: Bot
get() = sender
private val isRecalled: AtomicBoolean = atomic(false)
override var isRecalledOrPlanned: Boolean
get() = isRecalled.value
set(value) {
isRecalled.value = value
}
private val elems by lazy {
originalMessage.toRichTextElems(groupId != 0L, true)
originalMessage.toRichTextElems(forGroup = false, withGeneralFlags = true)
}
private fun toJceDataImplForFriend(): ImMsgBody.SourceMsg {
val messageUid: Long = 262144L.shl(32) or messageRandom.toLong().and(0xffFFffFF)
fun toJceDataImplForFriend(): ImMsgBody.SourceMsg {
val messageUid: Long = sequenceId.toLong().shl(32) or id.toLong().and(0xffFFffFF)
return ImMsgBody.SourceMsg(
origSeqs = listOf(sequenceId),
senderUin = senderId,
toUin = toUin,
toUin = targetId,
flag = 1,
elems = elems,
type = 0,
time = time.toInt(),
time = time,
pbReserve = SourceMsg.ResvAttr(
origUids = messageUid
).toByteArray(SourceMsg.ResvAttr.serializer()),
srcMsg = MsgComm.Msg(
msgHead = MsgComm.MsgHead(
fromUin = senderId, // qq
toUin = toUin, // group
toUin = targetId, // group
msgType = 9, // 82?
c2cCmd = 11,
msgSeq = sequenceId,
msgTime = time.toInt(),
msgTime = time,
msgUid = messageUid, // ok
// groupInfo = MsgComm.GroupInfo(groupCode = delegate.msgHead.groupInfo.groupCode),
isSrcMsg = true
@ -187,28 +228,68 @@ internal abstract class MessageSourceFromSend : MessageSource {
)
}
private fun toJceDataImplForGroup(): ImMsgBody.SourceMsg {
}
internal class MessageSourceToGroupImpl(
override val id: Int,
override val time: Int,
override val originalMessage: MessageChain,
override val sender: Bot,
override val target: Group
) : OnlineMessageSource.Outgoing.ToGroup(), MessageSourceImpl {
override val bot: Bot
get() = sender
private val isRecalled: AtomicBoolean = atomic(false)
override var isRecalledOrPlanned: Boolean
get() = isRecalled.value
set(value) {
isRecalled.value = value
}
private val elems by lazy {
originalMessage.toRichTextElems(forGroup = false, withGeneralFlags = true)
}
private lateinit var sequenceIdDeferred: Deferred<Int>
override val sequenceId: Int get() = sequenceIdDeferred.getCompleted()
@OptIn(MiraiExperimentalAPI::class)
internal fun startWaitingSequenceId(coroutineScope: CoroutineScope) {
sequenceIdDeferred =
coroutineScope.subscribingGetAsync<OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt, Int>(
timeoutMillis = 3000
) {
if (it.messageRandom == this@MessageSourceToGroupImpl.id) {
it.sequenceId
} else null
}
}
suspend fun ensureSequenceIdAvailable() {
sequenceIdDeferred.join()
}
fun toJceDataImplForGroup(): ImMsgBody.SourceMsg {
return ImMsgBody.SourceMsg(
origSeqs = listOf(sequenceId),
senderUin = senderId,
toUin = toUin,
toUin = Group.calculateGroupUinByGroupCode(targetId),
flag = 1,
elems = elems,
type = 0,
time = time.toInt(),
time = time,
pbReserve = SourceMsg.ResvAttr(
origUids = messageRandom.toLong() and 0xffFFffFF
origUids = id.toLong() and 0xffFFffFF // id is actually messageRandom
).toByteArray(SourceMsg.ResvAttr.serializer()),
srcMsg = MsgComm.Msg(
msgHead = MsgComm.MsgHead(
fromUin = senderId, // qq
toUin = toUin, // group
toUin = Group.calculateGroupUinByGroupCode(targetId), // group
msgType = 82, // 82?
c2cCmd = 1,
msgSeq = sequenceId,
msgTime = time.toInt(),
msgUid = messageRandom.toLong() and 0xffFFffFF, // ok
groupInfo = MsgComm.GroupInfo(groupCode = groupId),
msgTime = time,
msgUid = id.toLong() and 0xffFFffFF, // ok
groupInfo = MsgComm.GroupInfo(groupCode = targetId),
isSrcMsg = true
),
msgBody = ImMsgBody.MsgBody(
@ -221,65 +302,4 @@ internal abstract class MessageSourceFromSend : MessageSource {
).toByteArray(MsgComm.Msg.serializer())
)
}
}
internal class MessageSourceFromSendFriend(
val messageRandom: Int,
override val time: Long,
override val senderId: Long,
override val toUin: Long,
override val groupId: Long,
val sequenceId: Int,
override val originalMessage: MessageChain
) : MessageSourceFromSend() {
@OptIn(ExperimentalCoroutinesApi::class)
override val id: Long
get() = sequenceId.toLong().shl(32) or
messageRandom.toLong().and(0xFFFFFFFF)
override suspend fun ensureSequenceIdAvailable() {
// nothing to do
}
override fun toString(): String {
return ""
}
}
internal class MessageSourceFromSendGroup(
val messageRandom: Int,
override val time: Long,
override val senderId: Long,
override val toUin: Long,
override val groupId: Long,
override val originalMessage: MessageChain
) : MessageSourceFromSend() {
private lateinit var sequenceIdDeferred: Deferred<Int>
@OptIn(ExperimentalCoroutinesApi::class)
override val id: Long
get() = sequenceIdDeferred.getCompleted().toLong().shl(32) or
messageRandom.toLong().and(0xFFFFFFFF)
@OptIn(MiraiExperimentalAPI::class)
internal fun startWaitingSequenceId(coroutineScope: CoroutineScope) {
sequenceIdDeferred =
coroutineScope.subscribingGetAsync<OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt, Int>(
timeoutMillis = 3000
) {
if (it.messageRandom == this@MessageSourceFromSendGroup.messageRandom) {
it.sequenceId
} else null
}
}
override suspend fun ensureSequenceIdAvailable() {
sequenceIdDeferred.join()
}
override fun toString(): String {
return ""
}
}

View File

@ -14,8 +14,8 @@ package net.mamoe.mirai.qqandroid.message
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUInt
import kotlinx.io.core.toByteArray
import net.mamoe.mirai.Bot
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.HummerCommelem
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
@ -42,9 +42,11 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B
if (this.any<QuoteReply>()) {
when (val source = this[QuoteReply].source) {
is MessageSourceFromServer -> elements.add(ImMsgBody.Elem(srcMsg = source.delegate))
is MessageSourceFromMsg -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceData()))
is MessageSourceFromSend -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceData()))
is OfflineMessageSourceImpl -> elements.add(ImMsgBody.Elem(srcMsg = source.delegate))
is MessageSourceToFriendImpl -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceDataImplForFriend()))
is MessageSourceToGroupImpl -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceDataImplForGroup()))
is MessageSourceFromFriendImpl -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceDataImplForFriend()))
is MessageSourceFromGroupImpl -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceDataImplForGroup()))
else -> error("unsupported MessageSource implementation: ${source::class.simpleName}")
}
}
@ -122,19 +124,17 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B
.also { transformOneMessage(UNSUPPORTED_FLASH_MESSAGE_PLAIN) }
is AtAll -> elements.add(atAllData)
is Face -> elements.add(ImMsgBody.Elem(face = it.toJceData()))
is QuoteReplyToSend -> {
is QuoteReply -> {
if (forGroup) {
check(it is QuoteReplyToSend.ToGroup) {
"sending a quote to group using QuoteReplyToSend.ToFriend is prohibited"
when (val source = it.source) {
is OnlineMessageSource.Incoming.FromGroup -> {
transformOneMessage(At(source.sender))
transformOneMessage(PlainText(" "))
}
}
if (it.sender is Member) {
transformOneMessage(it.createAt())
}
transformOneMessage(PlainText(" "))
}
}
is QuoteReply, // already transformed above
is MessageSource, // mirai only
is MessageSource, // mirai metadata only
is RichMessage // already transformed above
-> {
@ -173,28 +173,36 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B
private val PB_RESERVE_FOR_RICH_MESSAGE =
"08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes()
@Suppress("SpellCheckingInspection")
private val PB_RESERVE_FOR_DOUTU = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes()
private val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes()
@OptIn(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
internal fun MsgComm.Msg.toMessageChain(): MessageChain {
internal fun MsgComm.Msg.toMessageChain(bot: Bot, isGroup: Boolean, addSource: Boolean): MessageChain {
val elements = this.msgBody.richText.elems
return buildMessageChain(elements.size + 1) {
+MessageSourceFromMsg(delegate = this@toMessageChain)
elements.joinToMessageChain(this)
if (addSource) {
if (isGroup) {
+MessageSourceFromGroupImpl(bot, this@toMessageChain)
} else {
+MessageSourceFromFriendImpl(bot, this@toMessageChain)
}
}
elements.joinToMessageChain(bot, this)
}.cleanupRubbishMessageElements()
}
// These two functions have difference method signature, don't combine.
@OptIn(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
internal fun ImMsgBody.SourceMsg.toMessageChain(): MessageChain {
internal fun ImMsgBody.SourceMsg.toMessageChain(bot: Bot): MessageChain {
val elements = this.elems!!
return buildMessageChain(elements.size + 1) {
+MessageSourceFromServer(delegate = this@toMessageChain)
elements.joinToMessageChain(this)
+OfflineMessageSourceImpl(delegate = this@toMessageChain, bot = bot)
elements.joinToMessageChain(bot, this)
}.cleanupRubbishMessageElements()
}
@ -228,7 +236,7 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
}
internal inline fun <reified R> Iterable<*>.firstIsInstance(): R {
this.forEach {
for (it in this) {
if (it is R) {
return it
}
@ -236,12 +244,21 @@ internal inline fun <reified R> Iterable<*>.firstIsInstance(): R {
throw NoSuchElementException("Collection contains no element matching the predicate.")
}
internal inline fun <reified R> Iterable<*>.firstIsInstanceOrNull(): R? {
for (it in this) {
if (it is R) {
return it
}
}
return null
}
@OptIn(MiraiInternalAPI::class, LowLevelAPI::class)
internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilder) {
internal fun List<ImMsgBody.Elem>.joinToMessageChain(bot: Bot, message: MessageChainBuilder) {
// (this._miraiContentToString())
this.forEach {
when {
it.srcMsg != null -> message.add(QuoteReply(MessageSourceFromServer(it.srcMsg)))
it.srcMsg != null -> message.add(QuoteReply(OfflineMessageSourceImpl(it.srcMsg, bot)))
it.notOnlineImage != null -> message.add(OnlineFriendImageImpl(it.notOnlineImage))
it.customFace != null -> message.add(OnlineGroupImageImpl(it.customFace))
it.face != null -> message.add(Face(it.face.index))

View File

@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat
import kotlinx.io.core.ByteReadPacket
@ -40,7 +42,7 @@ internal class PbMessageSvc {
}
// 12 1A 08 01 10 00 18 E7 C1 AD B8 02 22 0A 08 BF BA 03 10 BF 81 CB B7 03 2A 02 08 00
fun Group(
fun createForGroupMessage(
client: QQAndroidClient,
groupCode: Long,
messageSequenceId: Int, // 56639
@ -71,12 +73,12 @@ internal class PbMessageSvc {
)
}
fun Friend(
fun createForFriendMessage(
client: QQAndroidClient,
toUin: Long,
messageSequenceId: Int, // 56639
messageRandom: Int, // 921878719
time: Long
time: Int
): OutgoingPacket = buildOutgoingUniPacket(client) {
val messageUid: Long = 262144L.shl(32) or messageRandom.toLong().and(0xffFFffFF)
writeProtoBuf(
@ -91,7 +93,7 @@ internal class PbMessageSvc {
toUin = toUin,
msgSeq = messageSequenceId,
msgUid = messageUid,
msgTime = time and 0xffffffff,
msgTime = time.toULong().toLong(),
routingHead = MsgSvc.RoutingHead(
c2c = MsgSvc.C2C(
toUin = toUin

View File

@ -8,6 +8,7 @@
*/
@file: OptIn(LowLevelAPI::class)
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive
@ -19,6 +20,7 @@ import kotlinx.io.core.discardExact
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.BotJoinGroupEvent
import net.mamoe.mirai.event.events.BotOfflineEvent
@ -29,8 +31,8 @@ import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.contact.GroupImpl
import net.mamoe.mirai.qqandroid.contact.checkIsQQImpl
import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendFriend
import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendGroup
import net.mamoe.mirai.qqandroid.message.MessageSourceToFriendImpl
import net.mamoe.mirai.qqandroid.message.MessageSourceToGroupImpl
import net.mamoe.mirai.qqandroid.message.toMessageChain
import net.mamoe.mirai.qqandroid.message.toRichTextElems
import net.mamoe.mirai.qqandroid.network.MultiPacketByIterable
@ -82,6 +84,7 @@ internal class MessageSvc {
*/
@OptIn(MiraiInternalAPI::class)
internal object PbGetMsg : OutgoingPacketFactory<PbGetMsg.Response>("MessageSvc.PbGetMsg") {
@Suppress("SpellCheckingInspection")
operator fun invoke(
client: QQAndroidClient,
syncFlag: MsgSvc.SyncFlag = MsgSvc.SyncFlag.START,
@ -226,7 +229,7 @@ internal class MessageSvc {
if (friend.lastMessageSequence.compareAndSet(instant, msg.msgHead.msgSeq)) {
return@mapNotNull FriendMessage(
friend,
msg.toMessageChain()
msg.toMessageChain(bot, isGroup = false, addSource = true)
)
}
} else return@mapNotNull null
@ -289,34 +292,33 @@ internal class MessageSvc {
}
}
inline fun ToFriend(
inline fun createToFriend(
client: QQAndroidClient,
toUin: Long,
qq: QQ,
message: MessageChain,
crossinline sourceCallback: (MessageSourceFromSendFriend) -> Unit
crossinline sourceCallback: (MessageSourceToFriendImpl) -> Unit
): OutgoingPacket {
val source = MessageSourceFromSendFriend(
messageRandom = Random.nextInt().absoluteValue,
senderId = client.uin,
toUin = toUin,
time = currentTimeSeconds,
groupId = 0,
val source = MessageSourceToFriendImpl(
id = Random.nextInt().absoluteValue,
sender = client.bot,
target = qq,
time = currentTimeSeconds.toInt(),
sequenceId = client.atomicNextMessageSequenceId(),
originalMessage = message
)
sourceCallback(source)
return ToFriend(client, toUin, message, source)
return createToFriend(client, qq.id, message, source)
}
/**
* 发送好友消息
*/
@Suppress("FunctionName")
private fun ToFriend(
private fun createToFriend(
client: QQAndroidClient,
toUin: Long,
message: MessageChain,
source: MessageSourceFromSendFriend
source: MessageSourceToFriendImpl
): OutgoingPacket = buildOutgoingUniPacket(client) {
///writeFully("0A 08 0A 06 08 89 FC A6 8C 0B 12 06 08 01 10 00 18 00 1A 1F 0A 1D 12 08 0A 06 0A 04 F0 9F 92 A9 12 11 AA 02 0E 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 9B 7A 28 F4 CA 9B B8 03 32 34 08 92 C2 C4 F1 05 10 92 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 89 84 F9 A2 06 48 DE 8C EA E5 0E 58 D9 BD BB A0 09 60 1D 68 92 C2 C4 F1 05 70 00 40 01".hexToBytes())
@ -331,43 +333,42 @@ internal class MessageSvc {
)
),
msgSeq = source.sequenceId,
msgRand = source.messageRandom,
syncCookie = SyncCookie(time = source.time).toByteArray(SyncCookie.serializer())
msgRand = source.id,
syncCookie = SyncCookie(time = source.time.toULong().toLong()).toByteArray(SyncCookie.serializer())
// msgVia = 1
)
)
}
inline fun ToGroup(
inline fun createToGroup(
client: QQAndroidClient,
groupCode: Long,
group: Group,
message: MessageChain,
sourceCallback: (MessageSourceFromSendGroup) -> Unit
sourceCallback: (MessageSourceToGroupImpl) -> Unit
): OutgoingPacket {
val source = MessageSourceFromSendGroup(
messageRandom = Random.nextInt().absoluteValue,
senderId = client.uin,
toUin = Group.calculateGroupUinByGroupCode(groupCode),
time = currentTimeSeconds,
groupId = groupCode,
val source = MessageSourceToGroupImpl(
id = Random.nextInt().absoluteValue,
sender = client.bot,
target = group,
time = currentTimeSeconds.toInt(),
originalMessage = message//,
// sourceMessage = message
)
sourceCallback(source)
return ToGroup(client, groupCode, message, source)
return createToGroup(client, group.id, message, source)
}
/**
* 发送群消息
*/
@Suppress("FunctionName")
private fun ToGroup(
private fun createToGroup(
client: QQAndroidClient,
groupCode: Long,
message: MessageChain,
source: MessageSourceFromSendGroup
source: MessageSourceToGroupImpl
): OutgoingPacket = buildOutgoingUniPacket(client) {
///writeFully("0A 08 0A 06 08 89 FC A6 8C 0B 12 06 08 01 10 00 18 00 1A 1F 0A 1D 12 08 0A 06 0A 04 F0 9F 92 A9 12 11 AA 02 0E 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 9B 7A 28 F4 CA 9B B8 03 32 34 08 92 C2 C4 F1 05 10 92 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 89 84 F9 A2 06 48 DE 8C EA E5 0E 58 D9 BD BB A0 09 60 1D 68 92 C2 C4 F1 05 70 00 40 01".hexToBytes())
@ -384,7 +385,7 @@ internal class MessageSvc {
)
),
msgSeq = client.atomicNextMessageSequenceId(),
msgRand = source.messageRandom,
msgRand = source.id,
syncCookie = EMPTY_BYTE_ARRAY,
msgVia = 1
)

View File

@ -84,7 +84,7 @@ internal class OnlinePush {
return GroupMessage(
senderName = pbPushMsg.msg.msgHead.groupInfo.groupCard,
sender = group[pbPushMsg.msg.msgHead.fromUin],
message = pbPushMsg.msg.toMessageChain(),
message = pbPushMsg.msg.toMessageChain(bot, isGroup = true, addSource = true),
permission = when {
flags and 16 != 0 -> MemberPermission.ADMINISTRATOR
flags and 8 != 0 -> MemberPermission.OWNER
@ -369,13 +369,12 @@ internal class OnlinePush {
if (meta.authorUin == bot.id) {
null
} else MessageRecallEvent.GroupRecall(
bot,
meta.authorUin,
meta.seq.toLong().shl(32) or
meta.msgRandom.toLong().and(0xffffffff),
meta.time,
memebr,
group
bot = bot,
authorId = meta.authorUin,
messageId = meta.msgRandom,
messageTime = meta.time,
operator = memebr,
group = group
)
}
}

View File

@ -16,6 +16,7 @@ import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
private val indent: String = " ".repeat(4)
/**
@ -126,10 +127,12 @@ internal fun Any?._miraiContentToString(prefix: String = ""): String = when (thi
internal expect fun KProperty1<*, *>.getValueAgainstPermission(receiver: Any): Any?
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
private val KProperty1<*, *>.isConst: Boolean get() = false // on JVM, it will be resolved to member function
private val KProperty1<*, *>.isConst: Boolean
get() = false // on JVM, it will be resolved to member function
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
private val KClass<*>.isData: Boolean get() = false // on JVM, it will be resolved to member function
private val KClass<*>.isData: Boolean
get() = false // on JVM, it will be resolved to member function
private fun Any.contentToStringReflectively(
prefix: String,

View File

@ -7,7 +7,6 @@ import kotlinx.coroutines.io.ByteReadChannel
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.ExperimentalMessageSource
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
@ -156,7 +155,6 @@ actual abstract class Bot actual constructor() : CoroutineScope, LowLevelBotAPIA
* @see _lowLevelRecallFriendMessage 低级 API
* @see _lowLevelRecallGroupMessage 低级 API
*/
@ExperimentalMessageSource
@JvmSynthetic
actual abstract suspend fun recall(source: MessageSource)

View File

@ -7,10 +7,14 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.Bot
import net.mamoe.mirai.JavaFriendlyAPI
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.recallIn
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.message.data.ExperimentalMessageSource
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.QuoteReply
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
@ -31,7 +35,7 @@ import net.mamoe.mirai.utils.unsafeWeakRef
@OptIn(MiraiInternalAPI::class)
actual open class MessageReceipt<out C : Contact> @OptIn(ExperimentalMessageSource::class)
actual constructor(
actual val source: MessageSource,
actual val source: OnlineMessageSource.Outgoing,
target: C,
private val botAsMember: Member?
) {
@ -51,96 +55,33 @@ actual constructor(
private val _isRecalled = atomic(false)
/**
* 撤回这条消息. [recall] [recallIn] 只能被调用一次.
*
* @see Bot.recall
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
@OptIn(ExperimentalMessageSource::class)
actual suspend fun recall() {
@Suppress("BooleanLiteralArgument")
if (_isRecalled.compareAndSet(false, true)) {
when (val contact = target) {
is Group -> {
contact.bot.recall(source)
}
is QQ -> {
TODO()
}
else -> error("Unknown contact type")
}
} else error("message is already or planned to be recalled")
@JavaFriendlyAPI
@JvmName("quoteReply")
fun __quoteReplyBlockingForJava__(message: Message): MessageReceipt<C> {
return runBlocking { return@runBlocking quoteReply(message) }
}
/**
* 在一段时间后撤回这条消息.. [recall] [recallIn] 只能被调用一次.
*
* @param millis 延迟时间, 单位为毫秒
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
actual fun recallIn(millis: Long): Job {
@Suppress("BooleanLiteralArgument")
if (_isRecalled.compareAndSet(false, true)) {
return when (val contact = target) {
is QQ,
is Group
-> contact.bot.recallIn(source, millis)
else -> error("Unknown contact type")
}
} else error("message is already or planned to be recalled")
}
/**
* [确保 sequenceId可用][MessageSource.ensureSequenceIdAvailable] 然后引用这条消息.
* @see MessageChain.quote 引用一条消息
*/
@OptIn(ExperimentalMessageSource::class)
actual open suspend fun quote(): QuoteReplyToSend {
this.source.ensureSequenceIdAvailable()
@OptIn(LowLevelAPI::class)
return _unsafeQuote()
}
/**
* 引用这条消息, 但不会 [确保 sequenceId可用][MessageSource.ensureSequenceIdAvailable].
* sequenceId 可用前就发送这条消息则会导致一个异常.
* 当且仅当用于存储而不用于发送时使用这个方法.
*
* @see MessageChain.quote 引用一条消息
*/
@OptIn(ExperimentalMessageSource::class)
@LowLevelAPI
@Suppress("FunctionName")
actual fun _unsafeQuote(): QuoteReplyToSend {
return this.source.quote(botAsMember as? QQ)
}
/**
* 引用这条消息并回复.
* @see MessageChain.quote 引用一条消息
*/
@JvmSynthetic
actual suspend fun quoteReply(message: MessageChain) {
target.sendMessage(this.quote() + message)
}
@JavaFriendlyAPI
@JvmName("quoteReply")
fun __quoteReplyBlockingForJava__(message: Message) {
runBlocking { quoteReply(message) }
fun __quoteReplyBlockingForJava__(message: String): MessageReceipt<C> {
return runBlocking { quoteReply(message) }
}
@JavaFriendlyAPI
@JvmName("recall")
fun __recallBlockingForJava__() {
runBlocking { recall() }
return runBlocking { return@runBlocking recall() }
}
@JavaFriendlyAPI
@JvmName("recall")
fun __recallInBlockingForJava__(timeMillis: Long): Job {
return recallIn(timeMillis = timeMillis)
}
@JavaFriendlyAPI
@JvmName("quote")
fun __quoteBlockingForJava__() {
runBlocking { quote() }
fun __quoteBlockingForJava__(): QuoteReply {
return this.quote()
}
}

View File

@ -19,10 +19,7 @@ import kotlinx.coroutines.launch
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.ExperimentalMessageSource
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.*
@ -170,10 +167,8 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
* @throws PermissionDeniedException [Bot] 无权限操作时
*
* @see Bot.recall (扩展函数) 接受参数 [MessageChain]
* @see _lowLevelRecallFriendMessage 低级 API
* @see _lowLevelRecallGroupMessage 低级 API
* @see MessageSource.recall
*/
@ExperimentalMessageSource
@JvmSynthetic
abstract suspend fun recall(source: MessageSource)
@ -222,20 +217,19 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
/**
* 撤回这条消息.
* 根据 [message] 内的 [MessageSource] 进行相关判断.
*
* [Bot] 撤回自己的消息不需要权限.
* [Bot] 撤回群员的消息需要管理员权限.
* [Bot] 撤回自己的消息不需要权限, 但需要在发出后 2 分钟内撤回.
* [Bot] 撤回群员的消息需要管理员权限, 可在任意时间撤回.
*
* @throws PermissionDeniedException [Bot] 无权限操作时
* @see Bot.recall
*/
@JvmSynthetic
suspend inline fun Bot.recall(message: MessageChain) = this.recall(message[MessageSource])
suspend inline fun Bot.recall(message: MessageChain) =
this.recall(message.source)
/**
* 在一段时间后撤回这条消息.
* 将根据 [MessageSource.groupId] 判断消息是群消息还是好友消息.
* 在一段时间后撤回这个消息源所指代的消息.
*
* @param millis 延迟的时间, 单位为毫秒
* @param coroutineContext 额外的 [CoroutineContext]

View File

@ -109,8 +109,7 @@ sealed class MessageRecallEvent : BotEvent {
* 消息 id.
* @see MessageSource.id
*/
@ExperimentalMessageSource
abstract val messageId: Long
abstract val messageId: Int
/**
* 原发送时间
@ -122,8 +121,7 @@ sealed class MessageRecallEvent : BotEvent {
*/
data class FriendRecall(
override val bot: Bot,
@ExperimentalMessageSource
override val messageId: Long,
override val messageId: Int,
override val messageTime: Int,
/**
* 撤回操作人, 可能为 [Bot.uin] 或好友的 [QQ.id]
@ -137,8 +135,7 @@ sealed class MessageRecallEvent : BotEvent {
data class GroupRecall(
override val bot: Bot,
override val authorId: Long,
@ExperimentalMessageSource
override val messageId: Long,
override val messageId: Int,
override val messageTime: Int,
/**
* 操作人. null 时则为 [Bot] 操作.

View File

@ -13,7 +13,6 @@ import kotlinx.coroutines.Job
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.*
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.SinceMirai
import net.mamoe.mirai.utils.WeakRef
@ -69,21 +68,6 @@ interface LowLevelBotAPIAccessor {
@LowLevelAPI
suspend fun _lowLevelQueryGroupMemberList(groupUin: Long, groupCode: Long, ownerId: Long): Sequence<MemberInfo>
/**
* 撤回一条由机器人发送给好友的消息
* @param messageId [MessageSource.id]
*/
@MiraiExperimentalAPI("还未实现")
@LowLevelAPI
suspend fun _lowLevelRecallFriendMessage(friendId: Long, messageId: Long, time: Long)
/**
* 撤回一条群里的消息. 可以是机器人发送也可以是其他群员发送.
* @param messageId [MessageSource.id]
*/
@LowLevelAPI
suspend fun _lowLevelRecallGroupMessage(groupId: Long, messageId: Long)
/**
* 获取群公告列表
* @param page 页码
@ -131,20 +115,3 @@ interface LowLevelBotAPIAccessor {
@MiraiExperimentalAPI
suspend fun _lowLevelGetGroupActiveData(groupId: Long): GroupActiveData
}
/**
* 撤回一条群里的消息. 可以是机器人发送也可以是其他群员发送.
*/
@Suppress("FunctionName")
@MiraiExperimentalAPI
@LowLevelAPI
suspend fun LowLevelBotAPIAccessor._lowLevelRecallGroupMessage(
groupId: Long,
messageSequenceId: Int,
messageRandom: Int
) {
this._lowLevelRecallGroupMessage(
groupId,
messageSequenceId.toLong().shl(32) or messageRandom.toLong().and(0xFFFFFFFFL)
)
}

View File

@ -13,6 +13,8 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.source
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
@ -23,6 +25,7 @@ class FriendMessage(
override val sender: QQ by sender.unsafeWeakRef()
override val bot: Bot get() = sender.bot
override val subject: QQ get() = sender
override val source: OnlineMessageSource.Incoming.FromFriend get() = message.source as OnlineMessageSource.Incoming.FromFriend
override fun toString(): String = "FriendMessage(sender=${sender.id}, message=$message)"
}

View File

@ -15,6 +15,8 @@ import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.source
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
@ -34,6 +36,8 @@ class GroupMessage(
override val subject: Group get() = group
override val source: OnlineMessageSource.Incoming.FromGroup get() = message.source as OnlineMessageSource.Incoming.FromGroup
inline fun Long.member(): Member = group[this]
override fun toString(): String =

View File

@ -177,8 +177,7 @@ abstract class MessagePacketBase<out TSender : QQ, out TSubject : Contact> : Pac
@JvmName("reply2")
suspend inline fun MessageChain.quoteReply(): MessageReceipt<TSubject> = quoteReply(this)
@ExperimentalMessageSource
inline fun MessageChain.quote(): QuoteReplyToSend = this.quote(sender)
open val source: OnlineMessageSource.Incoming get() = message.source as OnlineMessageSource.Incoming
// endregion

View File

@ -7,14 +7,17 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("NOTHING_TO_INLINE")
package net.mamoe.mirai.message
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.recallIn
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmSynthetic
/**
@ -29,19 +32,17 @@ import kotlin.jvm.JvmSynthetic
* @see QQ.sendMessage 发送群消息, 返回回执此对象
*
* @see MessageReceipt.sourceId id
* @see MessageReceipt.sourceSequenceId 源序列号
* @see MessageReceipt.sourceTime 源时间
*/
expect open class MessageReceipt<out C : Contact> @OptIn(ExperimentalMessageSource::class) constructor(
source: MessageSource,
source: OnlineMessageSource.Outgoing,
target: C,
botAsMember: Member?
) {
/**
* 指代发送出去的消息
*/
@ExperimentalMessageSource
val source: MessageSource
val source: OnlineMessageSource.Outgoing
/**
* 发送目标, [Group] [QQ]
@ -52,82 +53,72 @@ expect open class MessageReceipt<out C : Contact> @OptIn(ExperimentalMessageSour
* 是否为发送给群的消息的回执
*/
val isToGroup: Boolean
/**
* 撤回这条消息. [recall] [recallIn] 只能被调用一次.
*
* @see Bot.recall
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
suspend fun recall()
/**
* 在一段时间后撤回这条消息.. [recall] [recallIn] 只能被调用一次.
*
* @param millis 延迟时间, 单位为毫秒
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
fun recallIn(millis: Long): Job
/**
* [确保 sequenceId可用][MessageSource.ensureSequenceIdAvailable] 然后引用这条消息.
* @see MessageChain.quote 引用一条消息
*/
open suspend fun quote(): QuoteReplyToSend
/**
* 引用这条消息, 但不会 [确保 sequenceId可用][MessageSource.ensureSequenceIdAvailable].
* sequenceId 可用前就发送这条消息则会导致一个异常.
* 当且仅当用于存储而不用于发送时使用这个方法.
*
* @see MessageChain.quote 引用一条消息
*/
@LowLevelAPI
@Suppress("FunctionName")
fun _unsafeQuote(): QuoteReplyToSend
/**
* 引用这条消息并回复.
* @see MessageChain.quote 引用一条消息
*/
suspend fun quoteReply(message: MessageChain)
}
/**
* 撤回这条消息. [recall] [recallIn] 只能被调用一次.
*
* @see Bot.recall
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
suspend inline fun MessageReceipt<*>.recall() {
return target.bot.recall(source)
}
/**
* 在一段时间后撤回这条消息. [recall] [recallIn] 只能被调用一次.
*
* @param timeMillis 延迟时间, 单位为毫秒
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
inline fun MessageReceipt<*>.recallIn(
timeMillis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = source.recallIn(timeMillis, coroutineContext)
/**
* 引用这条消息.
* @see MessageChain.quote 引用一条消息
*/
@JvmSynthetic
inline fun MessageReceipt<*>.quote(): QuoteReply = this.source.quote()
/**
* 引用这条消息并回复.
* @see MessageChain.quote 引用一条消息
*/
@JvmSynthetic
suspend inline fun <C : Contact> MessageReceipt<C>.quoteReply(message: Message): MessageReceipt<C> {
@Suppress("UNCHECKED_CAST")
return target.sendMessage(this.quote() + message) as MessageReceipt<C>
}
/**
* 引用这条消息并回复.
* @see MessageChain.quote 引用一条消息
*/
@JvmSynthetic
suspend inline fun <C : Contact> MessageReceipt<C>.quoteReply(message: String): MessageReceipt<C> {
return this.quoteReply(message.toMessage())
}
/**
* 获取源消息 [MessageSource.id]
*
* @see MessageSource.id
*/
@get:JvmSynthetic
@ExperimentalMessageSource
inline val MessageReceipt<*>.sourceId: Long
inline val MessageReceipt<*>.sourceId: Int
get() = this.source.id
/**
* 获取源消息 [MessageSource.sequenceId]
*
* @see MessageSource.sequenceId
*/
@get:JvmSynthetic
@ExperimentalMessageSource
inline val MessageReceipt<*>.sourceSequenceId: Int
get() = this.source.sequenceId
/**
* 获取源消息 [MessageSource.time]
*
* @see MessageSource.time
*/
@get:JvmSynthetic
@ExperimentalMessageSource
inline val MessageReceipt<*>.sourceTime: Long
inline val MessageReceipt<*>.sourceTime: Int
get() = this.source.time
suspend inline fun MessageReceipt<*>.quoteReply(message: Message) {
return this.quoteReply(message.asMessageChain())
}
suspend inline fun MessageReceipt<*>.quoteReply(message: String) {
return this.quoteReply(message.toMessage().asMessageChain())
}

View File

@ -9,11 +9,21 @@
@file:JvmMultifileClass
@file:JvmName("MessageUtils")
@file:Suppress("NOTHING_TO_INLINE")
package net.mamoe.mirai.message.data
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.ContactMessage
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.recallIn
import net.mamoe.mirai.utils.LazyProperty
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.SinceMirai
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
@ -33,108 +43,189 @@ annotation class ExperimentalMessageSource
*
* @see Bot.recall 撤回一条消息
* @see MessageSource.quote 引用这条消息, 创建 [MessageChain]
*
* @see OnlineMessageSource 在线消息的 [MessageSource]
* @see OfflineMessageSource 离线消息的 [MessageSource]
*/
@ExperimentalMessageSource
interface MessageSource : Message, MessageMetadata {
@SinceMirai("0.33.0")
sealed class MessageSource : Message, MessageMetadata {
companion object Key : Message.Key<MessageSource>
/**
* Mirai 中使用的 id.
* 32 位为 [sequenceId],
* 32 位为 [messageRandom]
*
* @see ensureSequenceIdAvailable 确保 sequenceId 可用
* 所属 [Bot]
*/
val id: Long
abstract val bot: Bot
/**
* 等待 [sequenceId] 获取, 确保其可用.
*
* 这个方法 3 秒超时, 抛出 [IllegalStateException], 则表明原消息发送失败.
* 消息 id.
*/
suspend fun ensureSequenceIdAvailable()
abstract val id: Int // random
/**
* 发送时间, 单位为秒. 撤回好友消息时可能需要
*/
val time: Long
abstract val time: Int
/**
* 发送人. 以为机器人自己 (`BotAsQQ`, `BotAsMember`)
* 发送人. 能为机器人自己, 好友的 id, 或群 id
*/
val senderId: Long
abstract val senderId: Long
/**
* 消息发送对象, 可以为一个群的 `uin` ( `id`)或一个好友, 或机器人自己
* 发送目标. 可能为机器人自己, 好友的 id, 或群 id
*/
val toUin: Long
abstract val targetId: Long // groupCode / friendUin
/**
* 当群消息时为群 id, [Group.id], 好友消息时为 0
*/
val groupId: Long
/**
* 原消息内容
* 原消息内容.
*/
@LazyProperty
val originalMessage: MessageChain
abstract val originalMessage: MessageChain
final override fun toString(): String = ""
}
// ONLINE
/**
* 在线消息的 [MessageSource].
* 拥有对象化的 [sender], [target], 也可以直接 [recall] [quote]
*
* ### 来源
* **必定是一个发出去的消息或接收到的消息的 [MessageChain] 中的一个元数据 [MessageMetadata].**
*
* #### 机器人主动发送消息
* 当机器人 [主动发出消息][Member.sendMessage], 将会得到一个 [消息回执][MessageReceipt].
* 此回执的 [消息源][MessageReceipt.source] 即为一个 [外向消息源][OnlineMessageSource.Outgoing], 代表着刚刚发出的那条消息的来源.
*
* #### 机器人接受消息
* 当机器人接收一条消息 [ContactMessage], 这条消息包含一个 [内向消息源][OnlineMessageSource.Incoming], 代表着接收到的这条消息的来源.
*/
@SinceMirai("0.33.0")
sealed class OnlineMessageSource : MessageSource() {
/**
* 消息发送人. 可能为 [机器人][Bot] [好友][QQ] [群员][Member].
* 即类型必定为 [Bot], [QQ] [Member]
*/
abstract val sender: Any
/**
* 固定返回空字符串 ("")
* 消息发送目标. 可能为 [机器人][Bot] [好友][QQ] [][Group].
* 即类型必定为 [Bot], [QQ] [Group]
*/
override fun toString(): String
abstract val target: Any
/**
* 消息主体. 群消息时为 [Group]. 好友消息时为 [QQ].
* 不论是机器人接收的消息还是发送的消息, 此属性都指向机器人能进行回复的目标.
*/
abstract val subject: Contact // Group or QQ
/**
* [机器人主动发送消息][Contact.sendMessage] 产生的 [MessageSource]
*/
sealed class Outgoing : OnlineMessageSource() {
abstract override val sender: Bot
abstract override val target: Contact
final override val senderId: Long get() = sender.id
final override val targetId: Long get() = target.id
abstract class ToFriend : Outgoing() {
abstract override val target: QQ
final override val subject: QQ get() = target
// final override fun toString(): String = "OnlineMessageSource.ToFriend(target=${target.id})"
}
abstract class ToGroup : Outgoing() {
abstract override val target: Group
final override val subject: Group get() = target
// final override fun toString(): String = "OnlineMessageSource.ToGroup(group=${target.id})"
}
}
/**
* 接收到的一条消息的 [MessageSource]
*/
sealed class Incoming : OnlineMessageSource() {
abstract override val sender: QQ // out QQ
abstract override val target: Bot
final override val senderId: Long get() = sender.id
final override val targetId: Long get() = target.id
abstract class FromFriend : Incoming() {
abstract override val sender: QQ
final override val subject: QQ get() = sender
// final override fun toString(): String = "OnlineMessageSource.FromFriend(from=${sender.id})"
}
abstract class FromGroup : Incoming() {
abstract override val sender: Member
final override val subject: Group get() = sender.group
val group: Group get() = sender.group
// final override fun toString(): String = "OnlineMessageSource.FromGroup(group=${group.id}, sender=${sender.id})"
}
}
}
/**
* 序列号. 若是机器人发出去的消息, 请先 [确保 sequenceId 可用][MessageSource.ensureSequenceIdAvailable]
* @see MessageSource.id
* 引用这条消息
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageSource.sequenceId: Int
get() = (this.id shr 32).toInt()
fun OnlineMessageSource.quote(): QuoteReply {
@OptIn(MiraiInternalAPI::class)
return QuoteReply(this)
}
/**
* 消息随机数. 由服务器或客户端指定后不能更改. 它是消息 id 的一部分.
* @see MessageSource.id
* 引用这条消息
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageSource.messageRandom: Int
get() = this.id.toInt()
fun MessageChain.quote(): QuoteReply {
@OptIn(MiraiInternalAPI::class)
return QuoteReply(this.source)
}
/**
* 撤回这条消息
*/
suspend inline fun OnlineMessageSource.recall() = bot.recall(this)
/**
* 撤回这条消息
*/
inline fun MessageSource.recallIn(
timeMillis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = bot.recallIn(this, timeMillis, coroutineContext)
// OFFLINE
/**
* 由一条消息中的 [QuoteReply] 得到的 [MessageSource].
* 此消息源可能来自一条与机器人无关的消息. 因此无法提供对象化的 `sender` `target` 获取.
*/
@SinceMirai("0.33.0")
abstract class OfflineMessageSource : MessageSource() {
// final override fun toString(): String = "OfflineMessageSource(sender=$senderId, target=$targetId)"
} // TODO: 2020/4/4 可能要分群和好友
// For MessageChain
/**
* 消息 id.
*
* 仅接收到的消息才可以获取这个 id.
* 仅从服务器接收的消息才可以获取 id
*
* @see MessageSource.id
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageChain.id: Long
get() = this[MessageSource].id
inline val MessageChain.id: Int
get() = this.source.id
/**
* 消息序列号, 可能来自服务器也可以发送时赋值, 不唯一.
*
* 仅接收到的消息才可以获取这个序列号.
*
* @see MessageSource.id
* 获取这条消息源
* 仅从服务器接收的消息才可以获取消息源
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageChain.sequenceId: Int
get() = this.getOrNull(MessageSource)?.sequenceId ?: error("Only MessageChain from server has sequenceId")
/**
* 消息随机数. 由服务器或客户端指定后不能更改. 它是消息 id 的一部分.
* @see MessageSource.id
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageChain.messageRandom: Int
get() = this.getOrNull(MessageSource)?.messageRandom ?: error("Only MessageChain from server has sequenceId")
inline val MessageChain.source: MessageSource
get() = this[MessageSource]

View File

@ -12,66 +12,24 @@
package net.mamoe.mirai.message.data
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.SinceMirai
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
/**
* 从服务器接收的或客户端构造用来发送的群内的或好友的引用回复.
* 引用回复.
*
* 可以引用一条群消息并发送给一个好友, 或是引用好友消息发送给群.
* 可以引用自己发出的消息. 详见 [MessageReceipt.quote]
*
* 总是使用 [quote] 来构造这个实例.
* @see MessageSource 获取更多信息
*/
open class QuoteReply
@OptIn(ExperimentalMessageSource::class)
@MiraiInternalAPI constructor(val source: MessageSource) : Message, MessageMetadata {
@SinceMirai("0.33.0")
class QuoteReply(val source: MessageSource) : Message, MessageMetadata {
// TODO: 2020/4/4 Metadata or Content?
companion object Key : Message.Key<QuoteReply>
final override fun toString(): String = "[mirai:quote]"
}
/**
* 用于发送的引用回复.
* 总是使用 [quote] 来构造实例.
*/
@OptIn(MiraiInternalAPI::class, ExperimentalMessageSource::class)
sealed class QuoteReplyToSend
@MiraiInternalAPI constructor(source: MessageSource) : QuoteReply(source) {
class ToGroup(source: MessageSource, val sender: QQ) : QuoteReplyToSend(source) {
fun createAt(): At = At(sender as Member)
}
class ToFriend(source: MessageSource) : QuoteReplyToSend(source)
}
/**
* 引用这条消息.
* @see sender 消息发送人.
*/
@ExperimentalMessageSource
@OptIn(MiraiInternalAPI::class)
fun MessageChain.quote(sender: QQ?): QuoteReplyToSend {
this.firstOrNull<MessageSource>()?.let {
return it.quote(sender)
}
error("cannot find MessageSource")
}
/**
* 引用这条消息.
* @see from 消息来源. 若是好友发送
*/
@ExperimentalMessageSource
@OptIn(MiraiInternalAPI::class)
fun MessageSource.quote(from: QQ?): QuoteReplyToSend {
return if (this.groupId != 0L) {
check(from is Member) { "sender must be Member to quote a GroupMessage" }
QuoteReplyToSend.ToGroup(this, from)
} else QuoteReplyToSend.ToFriend(this)
override fun toString(): String = "[mirai:quote]"
}

View File

@ -7,10 +7,7 @@ import kotlinx.coroutines.io.ByteReadChannel
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.ExperimentalMessageSource
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.*
@ -166,7 +163,6 @@ actual abstract class Bot actual constructor() : CoroutineScope, LowLevelBotAPIA
* @see _lowLevelRecallFriendMessage 低级 API
* @see _lowLevelRecallGroupMessage 低级 API
*/
@ExperimentalMessageSource
@JvmSynthetic
actual abstract suspend fun recall(source: MessageSource)

View File

@ -7,10 +7,14 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.Bot
import net.mamoe.mirai.JavaFriendlyAPI
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.recallIn
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.message.data.ExperimentalMessageSource
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.QuoteReply
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
@ -31,7 +35,7 @@ import net.mamoe.mirai.utils.unsafeWeakRef
@OptIn(MiraiInternalAPI::class)
actual open class MessageReceipt<out C : Contact> @OptIn(ExperimentalMessageSource::class)
actual constructor(
actual val source: MessageSource,
actual val source: OnlineMessageSource.Outgoing,
target: C,
private val botAsMember: Member?
) {
@ -51,87 +55,33 @@ actual constructor(
private val _isRecalled = atomic(false)
/**
* 撤回这条消息. [recall] [recallIn] 只能被调用一次.
*
* @see Bot.recall
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
@OptIn(ExperimentalMessageSource::class)
actual suspend fun recall() {
@Suppress("BooleanLiteralArgument")
if (_isRecalled.compareAndSet(false, true)) {
target.bot.recall(source)
} else error("message is already or planned to be recalled")
@JavaFriendlyAPI
@JvmName("quoteReply")
fun __quoteReplyBlockingForJava__(message: Message): MessageReceipt<C> {
return runBlocking { return@runBlocking quoteReply(message) }
}
/**
* 在一段时间后撤回这条消息.. [recall] [recallIn] 只能被调用一次.
*
* @param millis 延迟时间, 单位为毫秒
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
actual fun recallIn(millis: Long): Job {
@Suppress("BooleanLiteralArgument")
if (_isRecalled.compareAndSet(false, true)) {
return when (val contact = target) {
is QQ,
is Group -> contact.bot.recallIn(source, millis)
else -> error("Unknown contact type")
}
} else error("message is already or planned to be recalled")
}
/**
* [确保 sequenceId可用][MessageSource.ensureSequenceIdAvailable] 然后引用这条消息.
* @see MessageChain.quote 引用一条消息
*/
@OptIn(ExperimentalMessageSource::class)
actual open suspend fun quote(): QuoteReplyToSend {
this.source.ensureSequenceIdAvailable()
@OptIn(LowLevelAPI::class)
return _unsafeQuote()
}
/**
* 引用这条消息, 但不会 [确保 sequenceId可用][MessageSource.ensureSequenceIdAvailable].
* sequenceId 可用前就发送这条消息则会导致一个异常.
* 当且仅当用于存储而不用于发送时使用这个方法.
*
* @see MessageChain.quote 引用一条消息
*/
@OptIn(ExperimentalMessageSource::class)
@LowLevelAPI
@Suppress("FunctionName")
actual fun _unsafeQuote(): QuoteReplyToSend {
return this.source.quote(botAsMember as? QQ)
}
/**
* 引用这条消息并回复.
* @see MessageChain.quote 引用一条消息
*/
@JvmSynthetic
actual suspend fun quoteReply(message: MessageChain) {
target.sendMessage(this.quote() + message)
}
@JavaFriendlyAPI
@JvmName("quoteReply")
fun __quoteReplyBlockingForJava__(message: Message) {
runBlocking { quoteReply(message) }
fun __quoteReplyBlockingForJava__(message: String): MessageReceipt<C> {
return runBlocking { quoteReply(message) }
}
@JavaFriendlyAPI
@JvmName("recall")
fun __recallBlockingForJava__() {
runBlocking { recall() }
return runBlocking { recall() }
}
@JavaFriendlyAPI
@JvmName("recall")
fun __recallInBlockingForJava__(timeMillis: Long): Job {
return recallIn(timeMillis = timeMillis)
}
@JavaFriendlyAPI
@JvmName("quote")
fun __quoteBlockingForJava__() {
runBlocking { quote() }
fun __quoteBlockingForJava__(): QuoteReply {
return this.quote()
}
}