Commit 2b49a758 authored by Him188's avatar Him188

Reconstruct MessageSource, fix #197, #133

parent 9f7088d4
/*
* 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)
}
}
\ No newline at end of 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,
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()
network.run {
PbMessageSvc.PbMsgWithDraw.createForGroupMessage(
bot.asQQAndroidBot().client,
group.id,
source.sequenceId,
source.messageRandom
).sendAndExpect()
source.id
).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
}
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" }
}
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")
}
}
@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" }
}
check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #${source.id}: $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(
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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"
}
if (it.sender is Member) {
transformOneMessage(it.createAt())
when (val source = it.source) {
is OnlineMessageSource.Incoming.FromGroup -> {
transformOneMessage(At(source.sender))
transformOneMessage(PlainText(" "))
}
}
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))
......
......@@ -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
......
......@@ -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
)
......
......@@ -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
)
}
}
......
......@@ -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,
......
......@@ -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)
......
......@@ -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")
}
/**
* 在一段时间后撤回这条消息.. [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): MessageReceipt<C> {
return runBlocking { return@runBlocking quoteReply(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()
}
}
\ No newline at end of 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]
......
......@@ -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] 操作.
......
......@@ -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)
)
}
\ No newline at end of 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)"
}
......@@ -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 =
......
......@@ -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
......
......@@ -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,66 +53,65 @@ expect open class MessageReceipt<out C : Contact> @OptIn(ExperimentalMessageSour
* 是否为发送给群的消息的回执
*/
val isToGroup: Boolean
}
/**
* 撤回这条消息. [recall] 或 [recallIn] 只能被调用一次.
*
* @see Bot.recall
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
suspend fun recall()
/**
* 撤回这条消息. [recall] 或 [recallIn] 只能被调用一次.
*
* @see Bot.recall
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
suspend inline fun MessageReceipt<*>.recall() {
return target.bot.recall(source)
}
/**
* 在一段时间后撤回这条消息.. [recall] 或 [recallIn] 只能被调用一次.
*
* @param millis 延迟时间, 单位为毫秒
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
fun recallIn(millis: Long): Job
/**
* 在一段时间后撤回这条消息. [recall] 或 [recallIn] 只能被调用一次.
*
* @param timeMillis 延迟时间, 单位为毫秒
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
inline fun MessageReceipt<*>.recallIn(
timeMillis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = source.recallIn(timeMillis, coroutineContext)
/**
* [确保 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 引用一条消息
*/
@JvmSynthetic
inline fun MessageReceipt<*>.quote(): QuoteReply = this.source.quote()
/**
* 引用这条消息并回复.
* @see MessageChain.quote 引用一条消息
*/
suspend fun quoteReply(message: MessageChain)
/**
* 引用这条消息并回复.
* @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>
}
/**
* 获取源消息 [MessageSource.id]
*
* @see MessageSource.id
* 引用这条消息并回复.
* @see MessageChain.quote 引用一条消息
*/
@get:JvmSynthetic
@ExperimentalMessageSource
inline val MessageReceipt<*>.sourceId: Long
get() = this.source.id
@JvmSynthetic
suspend inline fun <C : Contact> MessageReceipt<C>.quoteReply(message: String): MessageReceipt<C> {
return this.quoteReply(message.toMessage())
}
/**
* 获取源消息 [MessageSource.sequenceId]
* 获取源消息 [MessageSource.id]
*
* @see MessageSource.sequenceId
* @see MessageSource.id
*/
@get:JvmSynthetic
@ExperimentalMessageSource
inline val MessageReceipt<*>.sourceSequenceId: Int
get() = this.source.sequenceId
inline val MessageReceipt<*>.sourceId: Int
get() = this.source.id
/**
* 获取源消息 [MessageSource.time]
......@@ -119,15 +119,6 @@ inline val MessageReceipt<*>.sourceSequenceId: Int
* @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())
}
......@@ -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
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]
*/
@LazyProperty
val originalMessage: MessageChain
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)
}
// For MessageChain
/**
* 撤回这条消息
*/
suspend inline fun OnlineMessageSource.recall() = bot.recall(this)
/**
* 消息 id.
*
* 仅接收到的消息才可以获取这个 id.
*
* @see MessageSource.id
* 撤回这条消息
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageChain.id: Long
get() = this[MessageSource].id
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
*
* @see MessageSource.id
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageChain.sequenceId: Int
get() = this.getOrNull(MessageSource)?.sequenceId ?: error("Only MessageChain from server has sequenceId")
inline val MessageChain.id: Int
get() = this.source.id
/**
* 消息随机数. 由服务器或客户端指定后不能更改. 它是消息 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]
\ No newline at end of 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]"
}
\ No newline at end of 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)
......
......@@ -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")
}
/**
* 在一段时间后撤回这条消息.. [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): MessageReceipt<C> {
return runBlocking { return@runBlocking quoteReply(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()
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment