Commit f7040c18 authored by Him188's avatar Him188 Committed by GitHub

Merge pull request #170 from mamoe/long-message

Support long message in general `sendMessage`
parents 6b332a73 2f063987
......@@ -32,9 +32,9 @@ internal actual class ECDHKeyPairImpl(
}
@Suppress("FunctionName")
actual fun ECDH() = ECDH(ECDH.generateKeyPair())
internal actual fun ECDH() = ECDH(ECDH.generateKeyPair())
actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
actual companion object {
@Suppress("ObjectPropertyName")
private var _isECDHAvailable: Boolean = false // because `runCatching` has no contract.
......
......@@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.qqandroid
import io.ktor.client.HttpClient
......@@ -29,12 +31,12 @@ import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.*
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.MessageRecallEvent
import net.mamoe.mirai.message.MessageReceipt
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.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendFriend
import net.mamoe.mirai.qqandroid.message.OnlineFriendImageImpl
import net.mamoe.mirai.qqandroid.message.OnlineGroupImageImpl
import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
......@@ -162,6 +164,7 @@ internal abstract class QQAndroidBotBase constructor(
TODO("not implemented")
}
@ExperimentalMessageSource
override suspend fun recall(source: MessageSource) {
if (source.senderId != uin && source.groupId != 0L) {
getGroup(source.groupId).checkBotPermissionOperator()
......@@ -368,34 +371,21 @@ internal abstract class QQAndroidBotBase constructor(
@LowLevelAPI
@MiraiExperimentalAPI
override suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message) {
override suspend fun _lowLevelSendLongGroupMessage(groupCode: Long, message: Message): MessageReceipt<Group> {
val chain = message.asMessageChain()
check(chain.toString().length <= 4500 && chain.count { it is Image } <= 50) { "message is too large" }
check(chain.toString().length <= 4500 && chain.count { it is Image } <= 50) { "message is too large. Allow up to 4500 chars or 50 images" }
val group = getGroup(groupCode)
val source = MessageSourceFromSendFriend(
messageRandom = Random.nextInt().absoluteValue,
senderId = client.uin,
toUin = Group.calculateGroupUinByGroupCode(groupCode),
time = currentTimeSeconds,
groupId = groupCode,
originalMessage = chain,
sequenceId = client.atomicNextMessageSequenceId()
// sourceMessage = message
)
// TODO: 2020/3/26 util 方法来添加单例元素
val toSend = buildMessageChain(chain.size) {
source.originalMessage.forEach {
if (it !is MessageSource) {
add(it)
}
}
add(source)
}
val time = currentTimeSeconds
network.run {
val data = toSend.calculateValidationDataForGroup(group)
val data = chain.calculateValidationDataForGroup(
sequenceId = client.atomicNextMessageSequenceId(),
time = time.toInt(),
random = Random.nextInt().absoluteValue.toUInt(),
groupCode,
group.botAsMember.nameCardOrNick
)
val response =
MultiMsg.ApplyUp.createForGroupLongMessage(
......@@ -441,17 +431,15 @@ internal abstract class QQAndroidBotBase constructor(
}
}
group.sendMessage(
return group.sendMessage(
RichMessage.longMessage(
brief = toSend.joinToString(limit = 30) {
when (it) {
is PlainText -> it.stringValue
is At -> it.toString()
else -> ""
}
brief = chain.toString().let { // already cached
if (it.length > 27) {
it.take(27) + "..."
} else it
},
resId = resId,
timeSeconds = source.time
timeSeconds = time
)
)
}
......
......@@ -23,9 +23,7 @@ import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.OfflineGroupImage
import net.mamoe.mirai.message.data.asMessageChain
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendGroup
import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
......@@ -273,13 +271,25 @@ internal class GroupImpl(
return members.delegate.filteringGetOrNull { it.id == id }
}
@OptIn(MiraiExperimentalAPI::class)
@JvmSynthetic
override suspend fun sendMessage(message: Message): MessageReceipt<Group> {
check(!isBotMuted) { "bot is muted. Remaining seconds=$botMuteRemaining" }
val event = GroupMessageSendEvent(this, message.asMessageChain()).broadcast()
if (event.isCancelled) {
throw EventCancelledException("cancelled by FriendMessageSendEvent")
throw EventCancelledException("cancelled by GroupMessageSendEvent")
}
if (message !is LongMessage) {
val length = event.message.toString().length
if (length > 4000
|| event.message.count { it is Image } > 3
|| (event.message.any<QuoteReply>() && (event.message.any<Image>() || length > 100))
) {
return bot._lowLevelSendLongGroupMessage(this.id, message)
}
}
lateinit var source: MessageSourceFromSendGroup
bot.network.run {
val response: MessageSvc.PbSendMsg.Response = MessageSvc.PbSendMsg.ToGroup(
......
......@@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.qqandroid.contact
import kotlinx.coroutines.launch
......
......@@ -8,9 +8,12 @@
*/
@file:OptIn(MiraiInternalAPI::class, LowLevelAPI::class)
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.qqandroid.contact
import kotlinx.atomicfu.AtomicInt
import kotlinx.atomicfu.atomic
import kotlinx.io.core.Closeable
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.Contact
......@@ -36,6 +39,8 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.toUHexString
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.jvm.JvmSynthetic
......@@ -46,12 +51,23 @@ internal inline class FriendInfoImpl(
override val uin: Long get() = jceFriendInfo.friendUin
}
@OptIn(ExperimentalContracts::class)
internal fun QQ.checkIsQQImpl(): QQImpl {
contract {
returns() implies (this@checkIsQQImpl is QQImpl)
}
check(this is QQImpl) { "A QQ instance is not instance of QQImpl. Don't interlace two protocol implementations together!" }
return this
}
internal class QQImpl(
bot: QQAndroidBot,
override val coroutineContext: CoroutineContext,
override val id: Long,
private val friendInfo: FriendInfo
) : QQ() {
var lastMessageSequence: AtomicInt = atomic(-1)
override val bot: QQAndroidBot by bot.unsafeWeakRef()
override val nick: String
get() = friendInfo.nick
......
......@@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE")
package net.mamoe.mirai.qqandroid.message
import kotlinx.coroutines.CoroutineScope
......@@ -146,7 +148,7 @@ internal abstract class MessageSourceFromSend : MessageSource {
}
private val elems by lazy {
originalMessage.toRichTextElems(groupId != 0L)
originalMessage.toRichTextElems(groupId != 0L, true)
}
private fun toJceDataImplForFriend(): ImMsgBody.SourceMsg {
......
......@@ -7,6 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file: OptIn(MiraiExperimentalAPI::class, MiraiInternalAPI::class, LowLevelAPI::class, ExperimentalUnsignedTypes::class)
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.qqandroid.message
......@@ -56,44 +57,6 @@ internal fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage {
)
}
/*
CustomFace#24412994 {
guid=<Empty ByteArray>
filePath={01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png
shortcut=
buffer=<Empty ByteArray>
flag=00 00 00 00
oldData=15 36 20 39 32 6B 41 31 00 38 37 32 66 30 36 36 30 33 61 65 31 30 33 62 37 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 30 31 45 39 34 35 31 42 2D 37 30 45 44 2D 45 41 45 33 2D 42 33 37 43 2D 31 30 31 46 31 45 45 42 46 35 42 35 7D 2E 70 6E 67 41
fileId=0x872F0660(-2026961312)
serverIp=0x3AE103B7(987825079)
serverPort=0x00000050(80)
fileType=0x00000000(0)
signature=<Empty ByteArray>
useful=0x00000001(1)
md5=01 E9 45 1B 70 ED EA E3 B3 7C 10 1F 1E EB F5 B5
thumbUrl=/gchatpic_new/1040400290/1041235568-2268005984-01E9451B70EDEAE3B37C101F1EEBF5B5/198?term=2
bigUrl=
origUrl=/gchatpic_new/1040400290/1041235568-2268005984-01E9451B70EDEAE3B37C101F1EEBF5B5/0?term=2
bizType=0x00000000(0)
repeatIndex=0x00000000(0)
repeatImage=0x00000000(0)
imageType=0x00000000(0)
index=0x00000000(0)
width=0x0000015F(351)
height=0x000000EB(235)
source=0x00000000(0)
size=0x0000057C(1404)
origin=0x00000000(0)
thumbWidth=0x000000C6(198)
thumbHeight=0x00000084(132)
showLen=0x00000000(0)
downloadLen=0x00000000(0)
_400Url=/gchatpic_new/1040400290/1041235568-2268005984-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2
_400Width=0x0000015F(351)
_400Height=0x000000EB(235)
pbReserve=<Empty ByteArray>
}
*/
internal val FACE_BUF = "00 01 00 04 52 CC F5 D0".hexToBytes()
internal fun Face.toJceData(): ImMsgBody.Face {
......@@ -133,76 +96,6 @@ internal fun OfflineGroupImage.toJceData(): ImMsgBody.CustomFace {
private val oldData: ByteArray =
"15 36 20 39 32 6B 41 31 00 38 37 32 66 30 36 36 30 33 61 65 31 30 33 62 37 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 30 31 45 39 34 35 31 42 2D 37 30 45 44 2D 45 41 45 33 2D 42 33 37 43 2D 31 30 31 46 31 45 45 42 46 35 42 35 7D 2E 70 6E 67 41".hexToBytes()
/*
customFace=CustomFace#2050019814 {
guid=<Empty ByteArray>
filePath=5F6C522DEAC4F36C0ED8EF362660EFD6.png
shortcut=
buffer=<Empty ByteArray>
flag=<Empty ByteArray>
oldData=<Empty ByteArray>
fileId=0xB40AF10E(-1274351346)
serverIp=0xB703E13A(-1224482502)
serverPort=0x00000050(80)
fileType=0x00000042(66)
signature=6B 44 61 76 72 79 68 79 57 67 70 52 41 45 78 49
useful=0x00000001(1)
md5=5F 6C 52 2D EA C4 F3 6C 0E D8 EF 36 26 60 EF D6
thumbUrl=
bigUrl=
origUrl=
bizType=0x00000005(5)
repeatIndex=0x00000000(0)
repeatImage=0x00000000(0)
imageType=0x000003E9(1001)
index=0x00000000(0)
width=0x0000005F(95)
height=0x00000054(84)
source=0x00000067(103)
size=0x000006E2(1762)
origin=0x00000000(0)
thumbWidth=0x00000000(0)
thumbHeight=0x00000000(0)
showLen=0x00000000(0)
downloadLen=0x00000000(0)
_400Url=
_400Width=0x00000000(0)
_400Height=0x00000000(0)
pbReserve=08 01 10 00 32 00 4A 0E 5B E5 8A A8 E7 94 BB E8 A1 A8 E6 83 85 5D 50 00 78 05
}
notOnlineImage=NotOnlineImage#2050019814 {
filePath=41AEF2D4B5BD24CF3791EFC5FEB67D60.jpg
fileLen=0x00000350(848)
downloadPath=/f2b7e5c0-acb3-4e83-aa5c-c8383840cc91
oldVerSendFile=<Empty ByteArray>
imgType=0x000003E8(1000)
previewsImage=<Empty ByteArray>
picMd5=41 AE F2 D4 B5 BD 24 CF 37 91 EF C5 FE B6 7D 60
picHeight=0x00000032(50)
picWidth=0x00000033(51)
resId=/f2b7e5c0-acb3-4e83-aa5c-c8383840cc91
flag=<Empty ByteArray>
thumbUrl=
original=0x00000000(0)
bigUrl=
origUrl=
bizType=0x00000005(5)
result=0x00000000(0)
index=0x00000000(0)
opFaceBuf=<Empty ByteArray>
oldPicMd5=false
thumbWidth=0x00000000(0)
thumbHeight=0x00000000(0)
fileId=0x00000000(0)
showLen=0x00000000(0)
downloadLen=0x00000000(0)
_400Url=
_400Width=0x00000000(0)
_400Height=0x00000000(0)
pbReserve=08 01 10 00 32 00 42 0E 5B E5 8A A8 E7 94 BB E8 A1 A8 E6 83 85 5D 50 00 78 05
}
*/
private val atAllData = ImMsgBody.Elem(
text = ImMsgBody.Text(
......@@ -222,7 +115,7 @@ private val atAllData = ImMsgBody.Elem(
private val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持查看[转发多条消息],请期待后续版本。")
@OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class)
internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgBody.Elem> {
internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: Boolean): MutableList<ImMsgBody.Elem> {
val elements = mutableListOf<ImMsgBody.Elem>()
if (this.any<QuoteReply>()) {
......@@ -310,6 +203,7 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
}
this.forEach(::transformOneMessage)
if (withGeneralFlags) {
when {
longTextResId != null -> {
elements.add(
......@@ -324,14 +218,19 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
}
this.any<RichMessage>() -> {
// 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 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
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "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())))
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_RICH_MESSAGE)))
}
else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_ELSE)))
}
else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes())))
}
return elements
}
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()
private val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes()
internal class OnlineGroupImageImpl(
internal val delegate: ImMsgBody.CustomFace
) : OnlineGroupImage() {
......@@ -416,17 +315,12 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
var last: SingleMessage? = null
return buildMessageChain(initialSize = this.count()) {
this@cleanupRubbishMessageElements.forEach { element ->
if (last == null) {
last = element
return@forEach
} else {
if (last is LongMessage && element is PlainText) {
if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) {
last = element
return@forEach
}
}
}
add(element)
last = element
......@@ -443,19 +337,6 @@ internal inline fun <reified R> Iterable<*>.firstIsInstance(): R {
throw NoSuchElementException("Collection contains no element matching the predicate.")
}
/*
if (this.any<QuoteReply>()) {
var removed = false
this.filter {
if (it is At && !removed) {
false
} else {
removed = true
true
}
}.asMessageChain()
} else this*/
internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilder) {
this.forEach {
when {
......@@ -467,9 +348,6 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilde
if (it.text.attr6Buf.isEmpty()) {
message.add(it.text.str.toMessage())
} else {
// 00 01 00 00 00 05 01 00 00 00 00 00 00 all
// 00 01 00 00 00 0A 00 3E 03 3F A2 00 00 one/nick
// 00 01 00 00 00 07 00 44 71 47 90 00 00 one/groupCard
val id: Long
it.text.attr6Buf.read {
discardExact(7)
......
......@@ -27,14 +27,17 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.login.Heartbeat
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.qqandroid.network.readUShortLVByteArray
import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
import net.mamoe.mirai.qqandroid.utils.cryptor.adjustToPublicKey
import net.mamoe.mirai.qqandroid.utils.io.readPacketExact
import net.mamoe.mirai.qqandroid.utils.io.readString
import net.mamoe.mirai.qqandroid.utils.io.useBytes
import net.mamoe.mirai.qqandroid.utils.io.withUse
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
import net.mamoe.mirai.qqandroid.utils.cryptor.adjustToPublicKey
import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.io.ByteArrayPool
import net.mamoe.mirai.utils.io.toInt
import net.mamoe.mirai.utils.io.toReadPacket
import net.mamoe.mirai.utils.io.toUHexString
import kotlin.jvm.JvmName
......@@ -171,9 +174,6 @@ internal object KnownPacketFactories {
?: IncomingFactories.firstOrNull { it.receivingCommandName == commandName }
}
/**
* full packet without length
*/
// do not inline. Exceptions thrown will not be reported correctly
@OptIn(MiraiInternalAPI::class)
@Suppress("UNCHECKED_CAST")
......@@ -183,22 +183,12 @@ internal object KnownPacketFactories {
val flag1 = readInt()
PacketLogger.verbose { "开始处理一个包" }
PacketLogger.verbose { "flag1(0A/0B) = ${flag1.toUByte().toUHexString()}" }
val flag2 = readByte().toInt()
PacketLogger.verbose {
"包类型(flag2) = $flag2. (可能是 ${when (flag2) {
2 -> "OicqRequest"
1 -> "Uni/ProtoBuf"
0 -> "Heartbeat"
else -> "未知"
}})"
}
val flag3 = readByte().toInt()
check(flag3 == 0) {
"Illegal flag3. Expected 0, whereas got $flag3. flag1=$flag1, flag2=$flag2. Remaining=${this.readBytes()
.toUHexString()}"
"Illegal flag3. Expected 0, whereas got $flag3. flag1=$flag1, flag2=$flag2. " +
"Remaining=${this.readBytes().toUHexString()}"
}
readString(readInt() - 4)// uinAccount
......@@ -209,14 +199,11 @@ internal object KnownPacketFactories {
kotlin.runCatching {
when (flag2) {
2 -> TEA.decrypt(data, DECRYPTER_16_ZERO, size)
.also { PacketLogger.verbose { "成功使用 16 zero 解密" } }
1 -> TEA.decrypt(data, bot.client.wLoginSigInfo.d2Key, size)
.also { PacketLogger.verbose { "成功使用 d2Key 解密" } }
0 -> data
else -> error("")
}
}.getOrElse {
PacketLogger.verbose { "失败, 尝试其他各种key" }
bot.client.tryDecryptOrNull(data, size) { it }
}?.toReadPacket()?.let { decryptedData ->
when (flag1) {
......@@ -236,7 +223,7 @@ internal object KnownPacketFactories {
} else {
handleIncomingPacket(it, bot, flag2, consumer)
}
} ?: inline {
} ?: kotlin.run {
PacketLogger.error { "任何key都无法解密: ${data.take(size).toUHexString()}" }
return
}
......@@ -295,8 +282,6 @@ internal object KnownPacketFactories {
}
}
private inline fun <R> inline(block: () -> R): R = block()
class IncomingPacket<T : Packet?>(
val packetFactory: PacketFactory<T>?,
val sequenceId: Int,
......@@ -358,9 +343,7 @@ internal object KnownPacketFactories {
}
}
}
8 -> {
input
}
8 -> input
else -> error("unknown dataCompressed flag: $dataCompressed")
}
......
......@@ -11,6 +11,7 @@
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive
import kotlinx.atomicfu.loop
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.*
import kotlinx.io.core.ByteReadPacket
......@@ -19,16 +20,15 @@ import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.qqandroid.network.MultiPacketByIterable
import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.event.events.BotJoinGroupEvent
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.MemberJoinEvent
import net.mamoe.mirai.getFriendOrNull
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.qqandroid.contact.GroupImpl
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.io.serialization.decodeUniPacket
import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
......@@ -37,6 +37,8 @@ import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendFriend
import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendGroup
import net.mamoe.mirai.qqandroid.message.toMessageChain
import net.mamoe.mirai.qqandroid.message.toRichTextElems
import net.mamoe.mirai.qqandroid.network.MultiPacketByIterable
import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPushForceOffline
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPushNotify
......@@ -195,9 +197,8 @@ internal class MessageSvc {
bot.groups.delegate.addLast(newGroup)
return@mapNotNull BotJoinGroupEvent(newGroup)
} else {
if (group == null) {
return@mapNotNull null
}
group ?: return@mapNotNull null
if (group.members.contains(msg.msgHead.authUin)) {
return@mapNotNull null
}
......@@ -207,22 +208,31 @@ internal class MessageSvc {
override val specialTitle: String get() = ""
override val muteTimestamp: Int get() = 0
override val uin: Long get() = msg.msgHead.authUin
override val nick: String
get() = msg.msgHead.authNick.takeIf { it.isNotEmpty() }
override val nick: String = msg.msgHead.authNick.takeIf { it.isNotEmpty() }
?: msg.msgHead.fromNick
}).also { group.members.delegate.addLast(it) })
}
}
166 -> {
val friend = bot.getFriendOrNull(msg.msgHead.fromUin) ?: return@mapNotNull null
return@mapNotNull when {
msg.msgHead.fromUin == bot.uin -> null
!bot.firstLoginSucceed -> null
else -> FriendMessage(
friend.checkIsQQImpl()
if (msg.msgHead.fromUin == bot.uin || !bot.firstLoginSucceed) {
return@mapNotNull null
}
friend.lastMessageSequence.loop { instant ->
if (msg.msgHead.msgSeq > instant) {
println("bigger")
if (friend.lastMessageSequence.compareAndSet(instant, msg.msgHead.msgSeq)) {
println("set ok")
return@mapNotNull FriendMessage(
friend,
msg.toMessageChain()
)
}
} else return@mapNotNull null
}
}
else -> return@mapNotNull null
}
......@@ -319,7 +329,7 @@ internal class MessageSvc {
contentHead = MsgComm.ContentHead(pkgNum = 1),
msgBody = ImMsgBody.MsgBody(
richText = ImMsgBody.RichText(
elems = message.toRichTextElems(false)
elems = message.toRichTextElems(false, true)
)
),
msgSeq = source.sequenceId,
......@@ -372,7 +382,7 @@ internal class MessageSvc {
contentHead = MsgComm.ContentHead(pkgNum = 1),
msgBody = ImMsgBody.MsgBody(
richText = ImMsgBody.RichText(
elems = message.toRichTextElems(true)
elems = message.toRichTextElems(true, true)
)
),
msgSeq = client.atomicNextMessageSequenceId(),
......
......@@ -21,7 +21,7 @@ expect interface ECDHPublicKey {
internal expect class ECDHKeyPairImpl : ECDHKeyPair
interface ECDHKeyPair {
internal interface ECDHKeyPair {
val privateKey: ECDHPrivateKey
val publicKey: ECDHPublicKey
......@@ -43,7 +43,7 @@ interface ECDHKeyPair {
/**
* 椭圆曲线密码, ECDH 加密
*/
expect class ECDH(keyPair: ECDHKeyPair) {
internal expect class ECDH(keyPair: ECDHKeyPair) {
val keyPair: ECDHKeyPair
/**
......@@ -74,9 +74,9 @@ expect class ECDH(keyPair: ECDHKeyPair) {
}
@Suppress("FunctionName")
expect fun ECDH(): ECDH
internal expect fun ECDH(): ECDH
val initialPublicKey
internal val initialPublicKey
get() = ECDH.constructPublicKey("3046301006072A8648CE3D020106052B8104001F03320004928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8".chunkedHexToBytes())
private val commonHeadFor02 = "302E301006072A8648CE3D020106052B8104001F031A00".chunkedHexToBytes()
private val commonHeadForNot02 = "3046301006072A8648CE3D020106052B8104001F033200".chunkedHexToBytes()
......@@ -86,7 +86,7 @@ private val byteArray_04 = byteArrayOf(0x04)
private val head1 = "302E301006072A8648CE3D020106052B8104001F031A00".chunkedHexToBytes()
private val head2 = "3046301006072A8648CE3D020106052B8104001F03320004".chunkedHexToBytes()
fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
internal fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
val head = if (this.size < 30) head1 else head2
return ECDH.constructPublicKey(head + this)
......
......@@ -23,7 +23,7 @@ import kotlin.random.Random
/**
* 解密错误
*/
class DecryptionFailedException : Exception {
internal class DecryptionFailedException : Exception {
constructor() : super()
constructor(message: String?) : super(message)
}
......@@ -34,7 +34,7 @@ class DecryptionFailedException : Exception {
* **注意**: 此为 Mirai 内部 API. 它可能会在任何时刻被改变.
*/
@MiraiInternalAPI
object TEA {
internal object TEA {
// TODO: 2020/2/28 使用 stream 式输入以避免缓存
/**
......
......@@ -20,7 +20,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
fun BytePacketBuilder.writeShortLVByteArrayLimitedLength(array: ByteArray, maxLength: Int) {
internal fun BytePacketBuilder.writeShortLVByteArrayLimitedLength(array: ByteArray, maxLength: Int) {
if (array.size <= maxLength) {
writeShort(array.size.toShort())
writeFully(array)
......@@ -32,13 +32,13 @@ fun BytePacketBuilder.writeShortLVByteArrayLimitedLength(array: ByteArray, maxLe
}
}
inline fun BytePacketBuilder.writeShortLVByteArray(byteArray: ByteArray): Int {
internal inline fun BytePacketBuilder.writeShortLVByteArray(byteArray: ByteArray): Int {
this.writeShort(byteArray.size.toShort())
this.writeFully(byteArray)
return byteArray.size
}
inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
internal inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
BytePacketBuilder().apply(builder).build().use {
if (tag != null) writeUByte(tag)
val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFFFFFL)
......@@ -47,7 +47,7 @@ inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset:
return length.toInt()
}
inline fun BytePacketBuilder.writeShortLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
internal inline fun BytePacketBuilder.writeShortLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
BytePacketBuilder().apply(builder).build().use {
if (tag != null) writeUByte(tag)
val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFFFFFL)
......@@ -56,18 +56,16 @@ inline fun BytePacketBuilder.writeShortLVPacket(tag: UByte? = null, lengthOffset
return length.toInt()
}
inline fun BytePacketBuilder.writeShortLVString(str: String) = writeShortLVByteArray(str.toByteArray())
internal inline fun BytePacketBuilder.writeShortLVString(str: String) = writeShortLVByteArray(str.toByteArray())
fun BytePacketBuilder.writeHex(uHex: String) {
internal fun BytePacketBuilder.writeHex(uHex: String) {
uHex.split(" ").forEach {
if (it.isNotBlank()) {
writeUByte(it.toUByte(16))
}
}
}
/**
* 会使用 [ByteArrayPool] 缓存
*/
@OptIn(MiraiInternalAPI::class)
inline fun BytePacketBuilder.encryptAndWrite(key: ByteArray, encoder: BytePacketBuilder.() -> Unit) =
internal inline fun BytePacketBuilder.encryptAndWrite(key: ByteArray, encoder: BytePacketBuilder.() -> Unit) =
TEA.encrypt(BytePacketBuilder().apply(encoder).build(), key) { decrypted -> writeFully(decrypted) }
\ No newline at end of file
......@@ -32,10 +32,10 @@ internal actual class ECDHKeyPairImpl(
}
@Suppress("FunctionName")
actual fun ECDH() =
internal actual fun ECDH() =
ECDH(ECDH.generateKeyPair())
actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
actual companion object {
actual val isECDHAvailable: Boolean
......
......@@ -7,6 +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
......@@ -152,6 +153,7 @@ actual abstract class Bot actual constructor() : CoroutineScope, LowLevelBotAPIA
* @see _lowLevelRecallFriendMessage 低级 API
* @see _lowLevelRecallGroupMessage 低级 API
*/
@ExperimentalMessageSource
@JvmSynthetic
actual abstract suspend fun recall(source: MessageSource)
......
......@@ -5,6 +5,7 @@ import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.contact.recall
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
......@@ -61,6 +62,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
*
* @see Bot.recall (扩展函数) 接受参数 [MessageChain]
*/
@ExperimentalMessageSource
@JvmName("recall")
fun __recallBlockingForJava__(source: MessageSource) {
runBlocking { recall(source) }
......@@ -88,6 +90,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
* @param millis 延迟的时间, 单位为毫秒
* @see recall
*/
@ExperimentalMessageSource
@JvmName("recallIn")
fun __recallIn_MemberForJava__(source: MessageSource, millis: Long) {
runBlocking { recallIn(source, millis) }
......@@ -148,6 +151,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
/**
* 异步调用 [__recallBlockingForJava__]
*/
@ExperimentalMessageSource
@JvmName("recallAsync")
fun __recallAsyncForJava__(source: MessageSource): Future<Unit> {
return future { recall(source) }
......
......@@ -54,6 +54,8 @@ actual abstract class Contact : CoroutineScope, ContactJavaFriendlyAPI() {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -48,6 +48,8 @@ actual abstract class ContactJavaFriendlyAPI {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -125,6 +125,8 @@ actual abstract class Group : Contact(), CoroutineScope {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -110,6 +110,8 @@ actual abstract class Member : MemberJavaFriendlyAPI() {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -78,6 +78,8 @@ actual abstract class QQ : Contact(), CoroutineScope {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -29,7 +29,8 @@ import net.mamoe.mirai.utils.unsafeWeakRef
*/
@Suppress("FunctionName")
@OptIn(MiraiInternalAPI::class)
actual open class MessageReceipt<C : Contact> actual constructor(
actual open class MessageReceipt<C : Contact> @OptIn(ExperimentalMessageSource::class)
actual constructor(
actual val source: MessageSource,
target: C,
private val botAsMember: Member?
......@@ -56,6 +57,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
* @see Bot.recall
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
@OptIn(ExperimentalMessageSource::class)
actual suspend fun recall() {
@Suppress("BooleanLiteralArgument")
if (_isRecalled.compareAndSet(false, true)) {
......@@ -82,7 +84,8 @@ actual open class MessageReceipt<C : Contact> actual constructor(
if (_isRecalled.compareAndSet(false, true)) {
return when (val contact = target) {
is QQ,
is Group -> contact.bot.recallIn(source, millis)
is Group,
-> contact.bot.recallIn(source, millis)
else -> error("Unknown contact type")
}
} else error("message is already or planned to be recalled")
......@@ -92,6 +95,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
* [确保 sequenceId可用][MessageSource.ensureSequenceIdAvailable] 然后引用这条消息.
* @see MessageChain.quote 引用一条消息
*/
@OptIn(ExperimentalMessageSource::class)
actual open suspend fun quote(): QuoteReplyToSend {
this.source.ensureSequenceIdAvailable()
@OptIn(LowLevelAPI::class)
......@@ -105,6 +109,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
*
* @see MessageChain.quote 引用一条消息
*/
@OptIn(ExperimentalMessageSource::class)
@LowLevelAPI
@Suppress("FunctionName")
actual fun _unsafeQuote(): QuoteReplyToSend {
......
......@@ -19,6 +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
......@@ -33,6 +34,7 @@ import kotlin.jvm.JvmSynthetic
/**
* 登录, 返回 [this]
*/
@JvmSynthetic
suspend inline fun <B : Bot> B.alsoLogin(): B = also { login() }
// 任何人都能看到这个方法
......@@ -167,6 +169,7 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
* @see _lowLevelRecallFriendMessage 低级 API
* @see _lowLevelRecallGroupMessage 低级 API
*/
@ExperimentalMessageSource
@JvmSynthetic
abstract suspend fun recall(source: MessageSource)
......@@ -223,6 +226,7 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
* @throws PermissionDeniedException 当 [Bot] 无权限操作时
* @see Bot.recall
*/
@JvmSynthetic
suspend inline fun Bot.recall(message: MessageChain) = this.recall(message[MessageSource])
/**
......@@ -233,6 +237,7 @@ suspend inline fun Bot.recall(message: MessageChain) = this.recall(message[Messa
* @param coroutineContext 额外的 [CoroutineContext]
* @see recall
*/
@JvmSynthetic
inline fun Bot.recallIn(
source: MessageSource,
millis: Long,
......@@ -249,6 +254,7 @@ inline fun Bot.recallIn(
* @param coroutineContext 额外的 [CoroutineContext]
* @see recall
*/
@JvmSynthetic
inline fun Bot.recallIn(
message: MessageChain,
millis: Long,
......@@ -265,15 +271,20 @@ inline fun Bot.recallIn(
*
* @param cause 原因. 为 null 时视为正常关闭, 非 null 时视为异常关闭
*/
@JvmSynthetic
suspend inline fun Bot.closeAndJoin(cause: Throwable? = null) {
close(cause)
coroutineContext[Job]?.join()
}
@JvmSynthetic
inline fun Bot.containsFriend(id: Long): Boolean = this.friends.contains(id)
@JvmSynthetic
inline fun Bot.containsGroup(id: Long): Boolean = this.groups.contains(id)
@JvmSynthetic
inline fun Bot.getFriendOrNull(id: Long): QQ? = this.friends.getOrNull(id)
@JvmSynthetic
inline fun Bot.getGroupOrNull(id: Long): Group? = this.groups.getOrNull(id)
\ No newline at end of file
......@@ -57,6 +57,8 @@ expect abstract class Contact() : CoroutineScope, ContactJavaFriendlyAPI {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -129,6 +129,8 @@ expect abstract class Group() : Contact, CoroutineScope {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -132,6 +132,8 @@ expect abstract class Member() : MemberJavaFriendlyAPI {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see MessageSendEvent.FriendMessageSendEvent 发送好友信息事件, cancellable
* @see MessageSendEvent.GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -87,6 +87,8 @@ expect abstract class QQ() : Contact, CoroutineScope {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -13,6 +13,7 @@ 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.MessageReceipt
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.MiraiExperimentalAPI
......@@ -137,6 +138,7 @@ interface LowLevelBotAPIAccessor {
/**
* 获取群活跃信息
*/
@SinceMirai("0.29.0")
@LowLevelAPI
@MiraiExperimentalAPI
suspend fun _lowLevelGetGroupActiveData(groupId: Long): GroupActiveData
......@@ -147,7 +149,7 @@ interface LowLevelBotAPIAccessor {
@SinceMirai("0.31.0")
@LowLevelAPI
@MiraiExperimentalAPI
suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message)
suspend fun _lowLevelSendLongGroupMessage(groupCode: Long, message: Message): MessageReceipt<Group>
}
/**
......
......@@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE", "unused")
package net.mamoe.mirai.message
......@@ -153,6 +153,7 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact> : Packet, Bot
/**
* 引用这个消息
*/
@ExperimentalMessageSource
inline fun MessageChain.quote(): QuoteReplyToSend = this.quote(sender)
operator fun <M : Message> get(at: Message.Key<M>): M {
......
......@@ -15,12 +15,16 @@ import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.recallIn
import kotlin.jvm.JvmSynthetic
/**
* 发送消息后得到的回执. 可用于撤回.
*
* 此对象持有 [Contact] 的弱引用, [Bot] 离线后将会释放引用, 届时 [target] 将无法访问.
*
* @param source 指代发送出去的消息
* @param target 消息发送对象
*
* @see Group.sendMessage 发送群消息, 返回回执(此对象)
* @see QQ.sendMessage 发送群消息, 返回回执(此对象)
*
......@@ -28,11 +32,15 @@ import net.mamoe.mirai.recallIn
* @see MessageReceipt.sourceSequenceId 源序列号
* @see MessageReceipt.sourceTime 源时间
*/
expect open class MessageReceipt<C : Contact>(
expect open class MessageReceipt<C : Contact> @OptIn(ExperimentalMessageSource::class) constructor(
source: MessageSource,
target: C,
botAsMember: Member?
) {
/**
* 指代发送出去的消息
*/
@ExperimentalMessageSource
val source: MessageSource
/**
......@@ -90,6 +98,8 @@ expect open class MessageReceipt<C : Contact>(
*
* @see MessageSource.id
*/
@get:JvmSynthetic
@ExperimentalMessageSource
inline val MessageReceipt<*>.sourceId: Long get() = this.source.id
/**
......@@ -97,6 +107,8 @@ inline val MessageReceipt<*>.sourceId: Long get() = this.source.id
*
* @see MessageSource.sequenceId
*/
@get:JvmSynthetic
@ExperimentalMessageSource
inline val MessageReceipt<*>.sourceSequenceId: Int get() = this.source.sequenceId
/**
......@@ -104,6 +116,8 @@ inline val MessageReceipt<*>.sourceSequenceId: Int get() = this.source.sequenceI
*
* @see MessageSource.time
*/
@get:JvmSynthetic
@ExperimentalMessageSource
inline val MessageReceipt<*>.sourceTime: Long get() = this.source.time
suspend inline fun MessageReceipt<out Contact>.quoteReply(message: Message) {
......
......@@ -37,7 +37,7 @@ interface Image : Message, MessageContent {
* 图片的 id. 只需要有这个 id 即可发送图片.
*
* 示例:
* 好友图片的 id: `/f8f1ab55-bf8e-4236-b55e-955848d7069f`
* 好友图片的 id: `/f8f1ab55-bf8e-4236-b55e-955848d7069f` 或 `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206`
* 群图片的 id: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png`
*/
val imageId: String
......@@ -53,7 +53,7 @@ interface Image : Message, MessageContent {
@JsName("newImage")
@JvmName("newImage")
fun Image(imageId: String): Image = when (imageId.length) {
37 -> OfflineFriendImage(imageId) // /f8f1ab55-bf8e-4236-b55e-955848d7069f
54, 37 -> OfflineFriendImage(imageId) // /f8f1ab55-bf8e-4236-b55e-955848d7069f
42 -> OfflineGroupImage(imageId) // {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png
else -> throw IllegalArgumentException("Bad imageId, expecting length=37 or 42, got ${imageId.length}")
}
......@@ -211,7 +211,7 @@ abstract class OnlineGroupImage : GroupImage(), OnlineImage
/**
* 好友图片
*
* [imageId] 形如 `/f8f1ab55-bf8e-4236-b55e-955848d7069f` (37 长度)
* [imageId] 形如 `/f8f1ab55-bf8e-4236-b55e-955848d7069f` (37 长度) 或 `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206` (54 长度)
*/ // NotOnlineImage
@OptIn(MiraiInternalAPI::class)
sealed class FriendImage : AbstractImage() {
......
......@@ -127,6 +127,7 @@ inline fun <reified M : Message> MessageChain.any(): Boolean = this.any { it is
/**
* 获取第一个 [M] 类型的 [Message] 实例
*/
@OptIn(ExperimentalMessageSource::class)
@JvmSynthetic
@Suppress("UNCHECKED_CAST")
fun <M : Message> MessageChain.firstOrNull(key: Message.Key<M>): M? = when (key) {
......@@ -415,7 +416,10 @@ internal class MessageChainImplByIterable constructor(
) : Message, Iterable<SingleMessage>, MessageChain {
override val size: Int by lazy { delegate.count() }
override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
override fun toString(): String = this.delegate.joinToString("") { it.toString() }
var toStringTemp: String? = null
override fun toString(): String =
toStringTemp ?: this.delegate.joinToString("") { it.toString() }.also { toStringTemp = it }
override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) }
}
......@@ -428,7 +432,10 @@ internal class MessageChainImplByCollection constructor(
) : Message, Iterable<SingleMessage>, MessageChain {
override val size: Int get() = delegate.size
override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
override fun toString(): String = this.delegate.joinToString("") { it.toString() }
var toStringTemp: String? = null
override fun toString(): String =
toStringTemp ?: this.delegate.joinToString("") { it.toString() }.also { toStringTemp = it }
override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) }
}
......@@ -446,7 +453,10 @@ internal class MessageChainImplBySequence constructor(
*/
private val collected: List<SingleMessage> by lazy { delegate.toList() }
override fun iterator(): Iterator<SingleMessage> = collected.iterator()
override fun toString(): String = this.collected.joinToString("") { it.toString() }
var toStringTemp: String? = null
override fun toString(): String =
toStringTemp ?: this.collected.joinToString("") { it.toString() }.also { toStringTemp = it }
override operator fun contains(sub: String): Boolean = collected.any { it.contains(sub) }
}
......
......@@ -19,6 +19,12 @@ import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
/**
* MessageSource 正计划于 0.32 或 0.33 或之后进行 API 不兼容的重写.
*/
@RequiresOptIn(message = "MessageSource 正计划于 0.32 或 0.33 或之后进行 API 不兼容的重写", level = RequiresOptIn.Level.WARNING)
annotation class ExperimentalMessageSource
/**
* 消息源, 它存在于 [MessageChain] 中, 用于表示这个消息的来源.
*
......@@ -29,6 +35,7 @@ import kotlin.jvm.JvmSynthetic
* @see Bot.recall 撤回一条消息
* @see MessageSource.quote 引用这条消息, 创建 [MessageChain]
*/
@ExperimentalMessageSource
interface MessageSource : Message, MessageMetadata {
companion object Key : Message.Key<MessageSource>
......@@ -82,6 +89,7 @@ interface MessageSource : Message, MessageMetadata {
* 序列号. 若是机器人发出去的消息, 请先 [确保 sequenceId 可用][MessageSource.ensureSequenceIdAvailable]
* @see MessageSource.id
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageSource.sequenceId: Int
get() = (this.id shr 32).toInt()
......@@ -90,6 +98,7 @@ inline val MessageSource.sequenceId: Int
* 消息随机数. 由服务器或客户端指定后不能更改. 它是消息 id 的一部分.
* @see MessageSource.id
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageSource.messageRandom: Int
get() = this.id.toInt()
......@@ -98,24 +107,33 @@ inline val MessageSource.messageRandom: Int
/**
* 消息 id.
*
* 仅接收到的消息才可以获取这个 id.
*
* @see MessageSource.id
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageChain.id: Long
get() = this[MessageSource].id
/**
* 消息序列号, 可能来自服务器也可以发送时赋值, 不唯一.
*
* 仅接收到的消息才可以获取这个序列号.
*
* @see MessageSource.id
*/
@ExperimentalMessageSource
@get:JvmSynthetic
inline val MessageChain.sequenceId: Int
get() = this[MessageSource].sequenceId
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[MessageSource].messageRandom
get() = this.getOrNull(MessageSource)?.messageRandom ?: error("Only MessageChain from server has sequenceId")
......@@ -29,6 +29,7 @@ import kotlin.jvm.JvmName
* 总是使用 [quote] 来构造这个实例.
*/
open class QuoteReply
@OptIn(ExperimentalMessageSource::class)
@MiraiInternalAPI constructor(val source: MessageSource) : Message, MessageMetadata {
companion object Key : Message.Key<QuoteReply>
......@@ -39,7 +40,7 @@ open class QuoteReply
* 用于发送的引用回复.
* 总是使用 [quote] 来构造实例.
*/
@OptIn(MiraiInternalAPI::class)
@OptIn(MiraiInternalAPI::class, ExperimentalMessageSource::class)
sealed class QuoteReplyToSend
@MiraiInternalAPI constructor(source: MessageSource) : QuoteReply(source) {
class ToGroup(source: MessageSource, val sender: QQ) : QuoteReplyToSend(source) {
......@@ -53,6 +54,7 @@ sealed class QuoteReplyToSend
* 引用这条消息.
* @see sender 消息发送人.
*/
@ExperimentalMessageSource
@OptIn(MiraiInternalAPI::class)
fun MessageChain.quote(sender: QQ?): QuoteReplyToSend {
this.firstOrNull<MessageSource>()?.let {
......@@ -65,6 +67,7 @@ fun MessageChain.quote(sender: QQ?): QuoteReplyToSend {
* 引用这条消息.
* @see from 消息来源. 若是好友发送
*/
@ExperimentalMessageSource
@OptIn(MiraiInternalAPI::class)
fun MessageSource.quote(from: QQ?): QuoteReplyToSend {
return if (this.groupId != 0L) {
......
......@@ -7,6 +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
......@@ -162,6 +163,7 @@ actual abstract class Bot actual constructor() : CoroutineScope, LowLevelBotAPIA
* @see _lowLevelRecallFriendMessage 低级 API
* @see _lowLevelRecallGroupMessage 低级 API
*/
@ExperimentalMessageSource
@JvmSynthetic
actual abstract suspend fun recall(source: MessageSource)
......
......@@ -5,6 +5,7 @@ import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.contact.recall
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
......@@ -61,6 +62,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
*
* @see Bot.recall (扩展函数) 接受参数 [MessageChain]
*/
@ExperimentalMessageSource
@JvmName("recall")
fun __recallBlockingForJava__(source: MessageSource) {
runBlocking { recall(source) }
......@@ -88,6 +90,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
* @param millis 延迟的时间, 单位为毫秒
* @see recall
*/
@ExperimentalMessageSource
@JvmName("recallIn")
fun __recallIn_MemberForJava__(source: MessageSource, millis: Long) {
runBlocking { recallIn(source, millis) }
......@@ -148,6 +151,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
/**
* 异步调用 [__recallBlockingForJava__]
*/
@ExperimentalMessageSource
@JvmName("recallAsync")
fun __recallAsyncForJava__(source: MessageSource): Future<Unit> {
return future { recall(source) }
......
......@@ -53,6 +53,8 @@ actual abstract class Contact : CoroutineScope, ContactJavaFriendlyAPI() {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -48,6 +48,8 @@ actual abstract class ContactJavaFriendlyAPI {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -127,6 +127,8 @@ actual abstract class Group : Contact(), CoroutineScope {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -118,6 +118,8 @@ actual abstract class Member : MemberJavaFriendlyAPI() {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -78,6 +78,8 @@ actual abstract class QQ : Contact(), CoroutineScope {
/**
* 向这个对象发送消息.
*
* 单条消息最大可发送 4500 字符或 50 张图片.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
......
......@@ -29,7 +29,8 @@ import net.mamoe.mirai.utils.unsafeWeakRef
*/
@Suppress("FunctionName")
@OptIn(MiraiInternalAPI::class)
actual open class MessageReceipt<C : Contact> actual constructor(
actual open class MessageReceipt<C : Contact> @OptIn(ExperimentalMessageSource::class)
actual constructor(
actual val source: MessageSource,
target: C,
private val botAsMember: Member?
......@@ -56,6 +57,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
* @see Bot.recall
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
@OptIn(ExperimentalMessageSource::class)
actual suspend fun recall() {
@Suppress("BooleanLiteralArgument")
if (_isRecalled.compareAndSet(false, true)) {
......@@ -84,6 +86,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
* [确保 sequenceId可用][MessageSource.ensureSequenceIdAvailable] 然后引用这条消息.
* @see MessageChain.quote 引用一条消息
*/
@OptIn(ExperimentalMessageSource::class)
actual open suspend fun quote(): QuoteReplyToSend {
this.source.ensureSequenceIdAvailable()
@OptIn(LowLevelAPI::class)
......@@ -97,6 +100,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
*
* @see MessageChain.quote 引用一条消息
*/
@OptIn(ExperimentalMessageSource::class)
@LowLevelAPI
@Suppress("FunctionName")
actual fun _unsafeQuote(): QuoteReplyToSend {
......
......@@ -116,7 +116,7 @@ suspend fun InputStream.uploadAsImage(contact: Contact): OfflineImage =
*/
@Throws(OverFileSizeMaxException::class)
suspend fun File.uploadAsImage(contact: Contact): OfflineImage {
require(this.exists() && this.canRead())
require(this.isFile && this.exists() && this.canRead()) { "file ${this.path} is not readable" }
return withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
}
......
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