Merge remote-tracking branch 'origin/master'

This commit is contained in:
jiahua.liu 2020-02-23 01:28:07 +08:00
commit d748a72372
22 changed files with 590 additions and 250 deletions

View File

@ -2,6 +2,22 @@
开发版本. 频繁更新, 不保证高稳定性 开发版本. 频繁更新, 不保证高稳定性
## `0.20.0` 2020/2/23
### mirai-core
- 支持图片下载: `image.channel(): ByteReadChannel`, `image.url()`
- 添加 `LockFreeLinkedList<E>.iterator`
- 添加 `LockFreeLinkedList<E>.forEachNode`
- 并行处理事件监听
- 添加 `nextMessageContaining` 和相关可空版本
- '撤回' 从 `Contact` 移动到 `Bot`
- 删除 `MessageSource.sourceMessage`
- 让 MessageSource 拥有唯一的 long 类型 id, 删除原 `uid``sequence` 结构.
- 修复 `Message.eq` 歧义
## `0.19.1` 2020/2/21 ## `0.19.1` 2020/2/21
### mirai-core ### mirai-core

View File

@ -1,8 +1,8 @@
# style guide # style guide
kotlin.code.style=official kotlin.code.style=official
# config # config
mirai_version=0.19.1 mirai_version=0.20.0
mirai_japt_version=1.0.1 mirai_japt_version=1.1.0
kotlin.incremental.multiplatform=true kotlin.incremental.multiplatform=true
kotlin.parallel.tasks.in.project=true kotlin.parallel.tasks.in.project=true
# kotlin # kotlin

View File

@ -99,11 +99,9 @@ fun MessageChain.toDTOChain() = mutableListOf(this[MessageSource].toDTO()).apply
fun MessageChainDTO.toMessageChain(contact: Contact) = fun MessageChainDTO.toMessageChain(contact: Contact) =
MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage(contact)) } } MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage(contact)) } }
internal fun MessageSource.calMessageId() = (messageUid.toLong() shl 32) or (sequenceId.toLong() and 0xFFFFFFFF)
@UseExperimental(ExperimentalUnsignedTypes::class) @UseExperimental(ExperimentalUnsignedTypes::class)
fun Message.toDTO() = when (this) { fun Message.toDTO() = when (this) {
is MessageSource -> MessageSourceDTO(calMessageId()) is MessageSource -> MessageSourceDTO(id)
is At -> AtDTO(target, display) is At -> AtDTO(target, display)
is AtAll -> AtAllDTO(0L) is AtAll -> AtAllDTO(0L)
is Face -> FaceDTO(id) is Face -> FaceDTO(id)

View File

@ -11,7 +11,6 @@ package net.mamoe.mirai.api.http.queue
import net.mamoe.mirai.api.http.data.common.EventDTO import net.mamoe.mirai.api.http.data.common.EventDTO
import net.mamoe.mirai.api.http.data.common.IgnoreEventDTO import net.mamoe.mirai.api.http.data.common.IgnoreEventDTO
import net.mamoe.mirai.api.http.data.common.calMessageId
import net.mamoe.mirai.api.http.data.common.toDTO import net.mamoe.mirai.api.http.data.common.toDTO
import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.message.GroupMessage import net.mamoe.mirai.message.GroupMessage
@ -47,7 +46,7 @@ class MessageQueue : ConcurrentLinkedDeque<BotEvent>() {
} }
private fun addQuoteCache(msg: GroupMessage) { private fun addQuoteCache(msg: GroupMessage) {
quoteCache[msg.message[MessageSource].calMessageId()] = msg quoteCache[msg.message[MessageSource].id] = msg
if (quoteCache.size > quoteCacheSize) { if (quoteCache.size > quoteCacheSize) {
quoteCache.remove(quoteCache.firstKey()) quoteCache.remove(quoteCache.firstKey())
} }

View File

@ -22,7 +22,6 @@ import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
import net.mamoe.mirai.qqandroid.network.highway.postImage import net.mamoe.mirai.qqandroid.network.highway.postImage
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.StTroopMemberInfo import net.mamoe.mirai.qqandroid.network.protocol.data.jce.StTroopMemberInfo
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x352 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x352
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
@ -80,7 +79,7 @@ internal class QQImpl(
) { source = it }.sendAndExpect<MessageSvc.PbSendMsg.Response>() is MessageSvc.PbSendMsg.Response.SUCCESS ) { source = it }.sendAndExpect<MessageSvc.PbSendMsg.Response>() is MessageSvc.PbSendMsg.Response.SUCCESS
) { "send message failed" } ) { "send message failed" }
} }
return MessageReceipt(message, source, this) return MessageReceipt(source, this)
} }
override suspend fun uploadImage(image: ExternalImage): Image = try { override suspend fun uploadImage(image: ExternalImage): Image = try {
@ -505,25 +504,12 @@ internal class GroupImpl(
} }
} }
@MiraiExperimentalAPI
override suspend fun quit(): Boolean { override suspend fun quit(): Boolean {
check(botPermission != MemberPermission.OWNER) { "An owner cannot quit from a owning group" } check(botPermission != MemberPermission.OWNER) { "An owner cannot quit from a owning group" }
TODO("not implemented") TODO("not implemented")
} }
override suspend fun recall(source: MessageSource) {
if (source.senderId != bot.uin) {
checkBotPermissionOperator()
}
source.ensureSequenceIdAvailable()
bot.network.run {
val response = PbMessageSvc.PbMsgWithDraw.Group(bot.client, this@GroupImpl.id, source.sequenceId, source.messageUid.toInt())
.sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #${source.sequenceId}: $response" }
}
}
@UseExperimental(MiraiExperimentalAPI::class) @UseExperimental(MiraiExperimentalAPI::class)
override fun Member(memberInfo: MemberInfo): Member { override fun Member(memberInfo: MemberInfo): Member {
return MemberImpl( return MemberImpl(
@ -567,7 +553,7 @@ internal class GroupImpl(
source.startWaitingSequenceId(this) source.startWaitingSequenceId(this)
return MessageReceipt(message, source, this) return MessageReceipt(source, this)
} }
override suspend fun uploadImage(image: ExternalImage): Image = try { override suspend fun uploadImage(image: ExternalImage): Image = try {

View File

@ -9,21 +9,23 @@
package net.mamoe.mirai.qqandroid package net.mamoe.mirai.qqandroid
import kotlinx.io.core.ByteReadPacket import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.utils.io.ByteReadChannel
import net.mamoe.mirai.BotAccount import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.BotImpl import net.mamoe.mirai.BotImpl
import net.mamoe.mirai.contact.ContactList import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.contact.filteringGetOrNull
import net.mamoe.mirai.data.AddFriendResult import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.data.FriendInfo import net.mamoe.mirai.data.FriendInfo
import net.mamoe.mirai.data.GroupInfo import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.message.CustomFaceFromServer
import net.mamoe.mirai.qqandroid.message.NotOnlineImageFromServer
import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
@ -119,13 +121,50 @@ internal abstract class QQAndroidBotBase constructor(
TODO("not implemented") TODO("not implemented")
} }
override suspend fun Image.download(): ByteReadPacket { override suspend fun recall(source: MessageSource) {
TODO("not implemented") if (source.senderId != uin) {
getGroup(source.groupId).checkBotPermissionOperator()
} }
@Suppress("OverridingDeprecatedMember") source.ensureSequenceIdAvailable()
override suspend fun Image.downloadAsByteArray(): ByteArray {
TODO("not implemented") network.run {
val response: PbMessageSvc.PbMsgWithDraw.Response =
PbMessageSvc.PbMsgWithDraw.Group(bot.client, source.groupId, source.sequenceId, source.messageRandom)
.sendAndExpect()
check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #${source.sequenceId}: $response" }
}
}
override suspend fun recall(groupId: Long, senderId: Long, messageId: Long) {
if (senderId != uin) {
getGroup(groupId).checkBotPermissionOperator()
}
val sequenceId = (messageId shr 32).toInt()
network.run {
val response: PbMessageSvc.PbMsgWithDraw.Response =
PbMessageSvc.PbMsgWithDraw.Group(bot.client, groupId, sequenceId, messageId.toInt())
.sendAndExpect()
check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #$sequenceId: $response" }
}
}
override suspend fun queryImageUrl(image: Image): String = "http://gchat.qpic.cn" + when (image) {
is NotOnlineImageFromServer -> image.delegate.origUrl
is CustomFaceFromServer -> image.delegate.origUrl
is CustomFaceFromFile -> {
TODO()
}
is NotOnlineImageFromFile -> {
TODO()
}
else -> error("unsupported image class: ${image::class.simpleName}")
}
override suspend fun openChannel(image: Image): ByteReadChannel {
return Http.get<HttpResponse>(queryImageUrl(image)).content
} }
override suspend fun approveFriendAddRequest(id: Long, remark: String?) { override suspend fun approveFriendAddRequest(id: Long, remark: String?) {

View File

@ -11,6 +11,7 @@ package net.mamoe.mirai.qqandroid.message
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.messageRandom
import net.mamoe.mirai.qqandroid.io.serialization.loadAs import net.mamoe.mirai.qqandroid.io.serialization.loadAs
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
@ -21,13 +22,16 @@ internal inline class MessageSourceFromServer(
val delegate: ImMsgBody.SourceMsg val delegate: ImMsgBody.SourceMsg
) : MessageSource { ) : MessageSource {
override val time: Long get() = delegate.time.toLong() and 0xFFFFFFFF override val time: Long get() = delegate.time.toLong() and 0xFFFFFFFF
override val sequenceId: Int get() = delegate.origSeqs?.firstOrNull() ?: error("cannot find sequenceId from ImMsgBody.SourceMsg")
override val id: Long
get() = (delegate.origSeqs?.firstOrNull() ?: error("cannot find sequenceId from ImMsgBody.SourceMsg")).toLong().shl(32) or
(delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!.toInt()).toLong().and(0xFFFFFFFF)
override suspend fun ensureSequenceIdAvailable() { override suspend fun ensureSequenceIdAvailable() {
// nothing to do // nothing to do
} }
override val messageUid: Int get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!.toInt()
// override val sourceMessage: MessageChain get() = delegate.toMessageChain() // override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.senderUin override val senderId: Long get() = delegate.senderUin
override val groupId: Long get() = Group.calculateGroupCodeByGroupUin(delegate.toUin) override val groupId: Long get() = Group.calculateGroupCodeByGroupUin(delegate.toUin)
@ -39,12 +43,14 @@ internal inline class MessageSourceFromMsg(
val delegate: MsgComm.Msg val delegate: MsgComm.Msg
) : MessageSource { ) : MessageSource {
override val time: Long get() = delegate.msgHead.msgTime.toLong() and 0xFFFFFFFF override val time: Long get() = delegate.msgHead.msgTime.toLong() and 0xFFFFFFFF
override val sequenceId: Int get() = delegate.msgHead.msgSeq override val id: Long
get() = delegate.msgHead.msgSeq.toLong().shl(32) or
delegate.msgBody.richText.attr!!.random.toLong().and(0xFFFFFFFF)
override suspend fun ensureSequenceIdAvailable() { override suspend fun ensureSequenceIdAvailable() {
// nothing to do // nothing to do
} }
override val messageUid: Int get() = delegate.msgBody.richText.attr!!.random
// override val sourceMessage: MessageChain get() = delegate.toMessageChain() // override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.msgHead.fromUin override val senderId: Long get() = delegate.msgHead.fromUin
override val groupId: Long get() = delegate.msgHead.groupInfo!!.groupCode override val groupId: Long get() = delegate.msgHead.groupInfo!!.groupCode
@ -62,7 +68,7 @@ internal inline class MessageSourceFromMsg(
type = 0, type = 0,
time = delegate.msgHead.msgTime, time = delegate.msgHead.msgTime,
pbReserve = SourceMsg.ResvAttr( pbReserve = SourceMsg.ResvAttr(
origUids = messageUid.toLong() and 0xffFFffFF origUids = messageRandom.toLong() and 0xffFFffFF
).toByteArray(SourceMsg.ResvAttr.serializer()), ).toByteArray(SourceMsg.ResvAttr.serializer()),
srcMsg = MsgComm.Msg( srcMsg = MsgComm.Msg(
msgHead = MsgComm.MsgHead( msgHead = MsgComm.MsgHead(
@ -72,7 +78,7 @@ internal inline class MessageSourceFromMsg(
c2cCmd = delegate.msgHead.c2cCmd, c2cCmd = delegate.msgHead.c2cCmd,
msgSeq = delegate.msgHead.msgSeq, msgSeq = delegate.msgHead.msgSeq,
msgTime = delegate.msgHead.msgTime, msgTime = delegate.msgHead.msgTime,
msgUid = messageUid.toLong() and 0xffFFffFF msgUid = messageRandom.toLong() and 0xffFFffFF
, // ok , // ok
groupInfo = MsgComm.GroupInfo(groupCode = delegate.msgHead.groupInfo.groupCode), groupInfo = MsgComm.GroupInfo(groupCode = delegate.msgHead.groupInfo.groupCode),
isSrcMsg = true isSrcMsg = true

View File

@ -272,7 +272,7 @@ internal class MessageSvc {
} }
internal class MessageSourceFromSend( internal class MessageSourceFromSend(
override val messageUid: Int, val messageRandom: Int,
override val time: Long, override val time: Long,
override val senderId: Long, override val senderId: Long,
override val groupId: Long// , override val groupId: Long// ,
@ -280,19 +280,20 @@ internal class MessageSvc {
) : MessageSource { ) : MessageSource {
private lateinit var sequenceIdDeferred: Deferred<Int> private lateinit var sequenceIdDeferred: Deferred<Int>
@UseExperimental(ExperimentalCoroutinesApi::class)
override val id: Long
get() = sequenceIdDeferred.getCompleted().toLong().shl(32) or
messageRandom.toLong().and(0xFFFFFFFF)
@UseExperimental(MiraiExperimentalAPI::class) @UseExperimental(MiraiExperimentalAPI::class)
fun startWaitingSequenceId(contact: Contact) { fun startWaitingSequenceId(contact: Contact) {
sequenceIdDeferred = contact.subscribingGetAsync<OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt, Int> { sequenceIdDeferred = contact.subscribingGetAsync<OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt, Int> {
if (it.messageRandom == messageUid) { if (it.messageRandom == this@MessageSourceFromSend.messageRandom) {
it.sequenceId it.sequenceId
} else null } else null
} }
} }
@UseExperimental(ExperimentalCoroutinesApi::class)
override val sequenceId: Int
get() = sequenceIdDeferred.getCompleted()
override suspend fun ensureSequenceIdAvailable() { override suspend fun ensureSequenceIdAvailable() {
sequenceIdDeferred.join() sequenceIdDeferred.join()
} }
@ -309,7 +310,7 @@ internal class MessageSvc {
crossinline sourceCallback: (MessageSource) -> Unit crossinline sourceCallback: (MessageSource) -> Unit
): OutgoingPacket { ): OutgoingPacket {
val source = MessageSourceFromSend( val source = MessageSourceFromSend(
messageUid = Random.nextInt().absoluteValue, messageRandom = Random.nextInt().absoluteValue,
senderId = client.uin, senderId = client.uin,
time = currentTimeSeconds + client.timeDifference, time = currentTimeSeconds + client.timeDifference,
groupId = 0// groupId = 0//
@ -327,7 +328,7 @@ internal class MessageSvc {
client: QQAndroidClient, client: QQAndroidClient,
toUin: Long, toUin: Long,
message: MessageChain, message: MessageChain,
source: MessageSource source: MessageSourceFromSend
): OutgoingPacket = buildOutgoingUniPacket(client) { ): 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()) ///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())
@ -342,7 +343,7 @@ internal class MessageSvc {
) )
), ),
msgSeq = client.atomicNextMessageSequenceId(), msgSeq = client.atomicNextMessageSequenceId(),
msgRand = source.messageUid, msgRand = source.messageRandom,
syncCookie = SyncCookie(time = source.time).toByteArray(SyncCookie.serializer()) syncCookie = SyncCookie(time = source.time).toByteArray(SyncCookie.serializer())
// msgVia = 1 // msgVia = 1
) )
@ -358,7 +359,7 @@ internal class MessageSvc {
): OutgoingPacket { ): OutgoingPacket {
val source = MessageSourceFromSend( val source = MessageSourceFromSend(
messageUid = Random.nextInt().absoluteValue, messageRandom = Random.nextInt().absoluteValue,
senderId = client.uin, senderId = client.uin,
time = currentTimeSeconds + client.timeDifference, time = currentTimeSeconds + client.timeDifference,
groupId = groupCode//, groupId = groupCode//,
@ -372,11 +373,11 @@ internal class MessageSvc {
* 发送群消息 * 发送群消息
*/ */
@Suppress("FunctionName") @Suppress("FunctionName")
fun ToGroup( private fun ToGroup(
client: QQAndroidClient, client: QQAndroidClient,
groupCode: Long, groupCode: Long,
message: MessageChain, message: MessageChain,
source: MessageSource source: MessageSourceFromSend
): OutgoingPacket = buildOutgoingUniPacket(client) { ): 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()) ///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())
@ -393,7 +394,7 @@ internal class MessageSvc {
) )
), ),
msgSeq = client.atomicNextMessageSequenceId(), msgSeq = client.atomicNextMessageSequenceId(),
msgRand = source.messageUid, msgRand = source.messageRandom,
syncCookie = EMPTY_BYTE_ARRAY, syncCookie = EMPTY_BYTE_ARRAY,
msgVia = 1 msgVia = 1
) )

View File

@ -11,22 +11,24 @@
package net.mamoe.mirai package net.mamoe.mirai
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.io.OutputStream import kotlinx.coroutines.launch
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.IoBuffer
import kotlinx.io.core.use
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.AddFriendResult import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.data.FriendInfo import net.mamoe.mirai.data.FriendInfo
import net.mamoe.mirai.data.GroupInfo import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.message.data.Image 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.network.BotNetworkHandler import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.LoginFailedException import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.transferTo import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmStatic import kotlin.jvm.JvmStatic
/** /**
@ -208,13 +210,44 @@ abstract class Bot : CoroutineScope {
// region actions // region actions
@Deprecated("内存使用效率十分低下", ReplaceWith("this.download()"), DeprecationLevel.WARNING) /**
abstract suspend fun Image.downloadAsByteArray(): ByteArray * 撤回这条消息.
*
* [Bot] 撤回自己的消息不需要权限.
* [Bot] 撤回群员的消息需要管理员权限.
*
* @throws PermissionDeniedException [Bot] 无权限操作时
* @see Bot.recall (扩展函数) 接受参数 [MessageChain]
*/// source.groupId, source.sequenceId, source.messageUid
abstract suspend fun recall(source: MessageSource)
/** /**
* 将图片下载到内存中 (使用 [IoBuffer.Pool]) * 撤回一条消息. 可撤回自己 2 分钟内发出的消息, 和任意时间的群成员的消息.
*
* [Bot] 撤回自己的消息不需要权限.
* [Bot] 撤回群员的消息需要管理员权限.
*
* @param senderId 这条消息的发送人. 可以为 [Bot.uin] [Member.id]
* @param messageId [MessageSource.id]
*
* @throws PermissionDeniedException [Bot] 无权限操作时
* @see Bot.recall (扩展函数) 接受参数 [MessageChain]
* @see recall 请优先使用这个函数
*/ */
abstract suspend fun Image.download(): ByteReadPacket abstract suspend fun recall(groupId: Long, senderId: Long, messageId: Long)
/**
* 获取图片下载链接
*/
abstract suspend fun queryImageUrl(image: Image): String
/**
* 获取图片下载链接并开始下载.
*
* @see ByteReadChannel.copyAndClose
* @see ByteReadChannel.copyTo
*/
abstract suspend fun openChannel(image: Image): ByteReadChannel
/** /**
* 添加一个好友 * 添加一个好友
@ -222,11 +255,13 @@ abstract class Bot : CoroutineScope {
* @param message 若需要验证请求时的验证消息. * @param message 若需要验证请求时的验证消息.
* @param remark 好友备注 * @param remark 好友备注
*/ */
@MiraiExperimentalAPI("未支持")
abstract suspend fun addFriend(id: Long, message: String? = null, remark: String? = null): AddFriendResult abstract suspend fun addFriend(id: Long, message: String? = null, remark: String? = null): AddFriendResult
/** /**
* 同意来自陌生人的加好友请求 * 同意来自陌生人的加好友请求
*/ */
@MiraiExperimentalAPI("未支持")
abstract suspend fun approveFriendAddRequest(id: Long, remark: String?) abstract suspend fun approveFriendAddRequest(id: Long, remark: String?)
// endregion // endregion
@ -243,19 +278,54 @@ abstract class Bot : CoroutineScope {
*/ */
abstract fun close(cause: Throwable? = null) abstract fun close(cause: Throwable? = null)
// region extensions final override fun toString(): String = "Bot(${uin})"
final override fun toString(): String {
return "Bot(${uin})"
} }
/** /**
* 需要调用者自行 close [output] * 撤回这条消息.
* 根据 [message] 内的 [MessageSource] 进行相关判断.
*
* [Bot] 撤回自己的消息不需要权限.
* [Bot] 撤回群员的消息需要管理员权限.
*
* @throws PermissionDeniedException [Bot] 无权限操作时
* @see Bot.recall
*/ */
suspend inline fun Image.downloadTo(output: OutputStream) = @MiraiExperimentalAPI
download().use { input -> input.transferTo(output) } suspend inline fun Bot.recall(message: MessageChain) = this.recall(message[MessageSource])
// endregion /**
* 在一段时间后撤回这条消息.
* 将根据 [MessageSource.groupId] 判断消息是群消息还是好友消息.
*
* @param millis 延迟的时间, 单位为毫秒
* @param coroutineContext 额外的 [CoroutineContext]
* @see recall
*/
fun Bot.recallIn(
source: MessageSource,
millis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) {
kotlinx.coroutines.delay(millis)
recall(source)
}
/**
* 在一段时间后撤回这条消息.
*
* @param millis 延迟的时间, 单位为毫秒
* @param coroutineContext 额外的 [CoroutineContext]
* @see recall
*/
@MiraiExperimentalAPI
fun Bot.recallIn(
message: MessageChain,
millis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) {
kotlinx.coroutines.delay(millis)
recall(message)
} }
/** /**

View File

@ -12,6 +12,7 @@
package net.mamoe.mirai.contact package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.events.BeforeImageUploadEvent import net.mamoe.mirai.event.events.BeforeImageUploadEvent
import net.mamoe.mirai.event.events.EventCancelledException import net.mamoe.mirai.event.events.EventCancelledException
@ -20,8 +21,13 @@ import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.recall
import net.mamoe.mirai.recallIn
import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.WeakRefProperty import net.mamoe.mirai.utils.WeakRefProperty
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/** /**
@ -92,6 +98,44 @@ interface Contact : CoroutineScope {
override fun toString(): String override fun toString(): String
} }
suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.toChain()) /**
* @see Bot.recall
*/
@MiraiExperimentalAPI
suspend inline fun Contact.recall(source: MessageChain) = this.bot.recall(source)
suspend inline fun Contact.sendMessage(plain: String) = sendMessage(plain.toMessage()) /**
* @see Bot.recall
*/
suspend inline fun Contact.recall(source: MessageSource) = this.bot.recall(source)
/**
* @see Bot.recallIn
*/
@MiraiExperimentalAPI
fun Contact.recallIn(
message: MessageChain,
millis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.bot.recallIn(message, millis, coroutineContext)
/**
* @see Bot.recallIn
*/
fun Contact.recallIn(
source: MessageSource,
millis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.bot.recallIn(source, millis, coroutineContext)
/**
* @see Contact.sendMessage
*/
@Suppress("UNCHECKED_CAST")
suspend inline fun <C : Contact> C.sendMessage(message: Message): MessageReceipt<C> =
sendMessage(message.toChain()) as? MessageReceipt<C> ?: error("Internal class cast mistake")
/**
* @see Contact.sendMessage
*/
suspend inline fun <C : Contact> C.sendMessage(plain: String): MessageReceipt<C> = sendMessage(plain.toMessage())

View File

@ -11,10 +11,7 @@
package net.mamoe.mirai.contact package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.* import net.mamoe.mirai.event.events.*
@ -22,10 +19,7 @@ import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
/** /**
@ -151,17 +145,6 @@ interface Group : Contact, CoroutineScope {
@MiraiExperimentalAPI("还未支持") @MiraiExperimentalAPI("还未支持")
suspend fun quit(): Boolean suspend fun quit(): Boolean
/**
* 撤回这条消息.
*
* [Bot] 撤回自己的消息不需要权限.
* [Bot] 撤回群员的消息需要管理员权限.
*
* @throws PermissionDeniedException [Bot] 无权限操作时
* @see Group.recall (扩展函数) 接受参数 [MessageChain]
*/
suspend fun recall(source: MessageSource)
/** /**
* 构造一个 [Member]. * 构造一个 [Member].
* 非特殊情况请不要使用这个函数. 优先使用 [get]. * 非特殊情况请不要使用这个函数. 优先使用 [get].
@ -226,49 +209,6 @@ interface Group : Contact, CoroutineScope {
fun toFullString(): String = "Group(id=${this.id}, name=$name, owner=${owner.id}, members=${members.idContentString})" fun toFullString(): String = "Group(id=${this.id}, name=$name, owner=${owner.id}, members=${members.idContentString})"
} }
/**
* 撤回这条消息.
*
* [Bot] 撤回自己的消息不需要权限.
* [Bot] 撤回群员的消息需要管理员权限.
*
* @throws PermissionDeniedException [Bot] 无权限操作时
* @see Group.recall
*/
suspend inline fun Group.recall(message: MessageChain) = this.recall(message[MessageSource])
/**
* 在一段时间后撤回这条消息.
*
* @param millis 延迟的时间, 单位为毫秒
* @param coroutineContext 额外的 [CoroutineContext]
* @see recall
*/
fun Group.recallIn(
message: MessageSource,
millis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) {
kotlinx.coroutines.delay(millis)
recall(message)
}
/**
* 在一段时间后撤回这条消息.
*
* @param millis 延迟的时间, 单位为毫秒
* @param coroutineContext 额外的 [CoroutineContext]
* @see recall
*/
fun Group.recallIn(
message: MessageChain,
millis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) {
kotlinx.coroutines.delay(millis)
recall(message)
}
/** /**
* 返回机器人是否正在被禁言 * 返回机器人是否正在被禁言
* *

View File

@ -10,6 +10,8 @@
package net.mamoe.mirai.event.internal package net.mamoe.mirai.event.internal
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.EventDisabled import net.mamoe.mirai.event.EventDisabled
import net.mamoe.mirai.event.Listener import net.mamoe.mirai.event.Listener
@ -73,6 +75,9 @@ internal class Handler<in E : Event>
ListeningStatus.LISTENING ListeningStatus.LISTENING
} }
} }
@MiraiInternalAPI
override val lock: Mutex = Mutex()
} }
/** /**
@ -138,23 +143,29 @@ internal object EventListenerManager {
// inline: NO extra Continuation // inline: NO extra Continuation
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
internal suspend inline fun Event.broadcastInternal() { internal suspend inline fun Event.broadcastInternal() = coroutineScope {
if (EventDisabled) return if (EventDisabled) return@coroutineScope
EventLogger.info { "Event broadcast: $this" } EventLogger.info { "Event broadcast: $this" }
val listeners = this::class.listeners() val listeners = this@broadcastInternal::class.listeners()
callAndRemoveIfRequired(listeners) callAndRemoveIfRequired(this@broadcastInternal, listeners)
listeners.supertypes.forEach { listeners.supertypes.forEach {
callAndRemoveIfRequired(it.listeners()) callAndRemoveIfRequired(this@broadcastInternal, it.listeners())
} }
} }
private suspend inline fun <E : Event> E.callAndRemoveIfRequired(listeners: EventListeners<E>) { @UseExperimental(MiraiInternalAPI::class)
private fun <E : Event> CoroutineScope.callAndRemoveIfRequired(event: E, listeners: EventListeners<E>) {
// atomic foreach // atomic foreach
listeners.forEach { listeners.forEachNode { node ->
if (it.onEvent(this) == ListeningStatus.STOPPED) { launch {
listeners.remove(it) // atomic remove val listener = node.nodeValue
listener.lock.withLock {
if (!node.isRemoved() && listener.onEvent(event) == ListeningStatus.STOPPED) {
listeners.remove(listener) // atomic remove
}
}
} }
} }
} }

View File

@ -13,6 +13,7 @@ import kotlinx.coroutines.CompletableJob
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.sync.Mutex
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.internal.Handler import net.mamoe.mirai.event.internal.Handler
@ -51,6 +52,12 @@ enum class ListeningStatus {
* 取消监听: [complete] * 取消监听: [complete]
*/ */
interface Listener<in E : Event> : CompletableJob { interface Listener<in E : Event> : CompletableJob {
/**
* [onEvent] 的锁
*/
@MiraiInternalAPI
val lock: Mutex
suspend fun onEvent(event: E): ListeningStatus suspend fun onEvent(event: E): ListeningStatus
} }

View File

@ -11,11 +11,16 @@ package net.mamoe.mirai.message
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.Event
import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.recall
import net.mamoe.mirai.recallIn
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.getValue import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef import net.mamoe.mirai.utils.unsafeWeakRef
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
@ -59,10 +64,13 @@ class GroupMessage(
@JvmName("reply2") @JvmName("reply2")
suspend inline fun MessageChain.quoteReply(): MessageReceipt<Group> = quoteReply(this) suspend inline fun MessageChain.quoteReply(): MessageReceipt<Group> = quoteReply(this)
suspend inline fun MessageChain.recall() = group.recall(this) @MiraiExperimentalAPI
suspend inline fun MessageSource.recall() = group.recall(this) suspend inline fun MessageChain.recall() = bot.recall(this)
inline fun MessageSource.recallIn(delay: Long): Job = group.recallIn(this, delay)
inline fun MessageChain.recallIn(delay: Long): Job = group.recallIn(this, delay) suspend inline fun MessageSource.recall() = bot.recall(this)
inline fun MessageSource.recallIn(delay: Long): Job = bot.recallIn(this, delay)
@MiraiExperimentalAPI
inline fun MessageChain.recallIn(delay: Long): Job = bot.recallIn(this, delay)
override fun toString(): String = override fun toString(): String =
"GroupMessage(group=${group.id}, senderName=$senderName, sender=${sender.id}, permission=${permission.name}, message=$message)" "GroupMessage(group=${group.id}, senderName=$senderName, sender=${sender.id}, permission=${permission.name}, message=$message)"

View File

@ -11,9 +11,7 @@
package net.mamoe.mirai.message package net.mamoe.mirai.message
import kotlinx.io.core.ByteReadPacket import io.ktor.utils.io.ByteReadChannel
import kotlinx.io.core.IoBuffer
import kotlinx.io.core.readBytes
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
@ -22,7 +20,7 @@ import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.Packet import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.subscribingGet import net.mamoe.mirai.event.subscribingGet
import net.mamoe.mirai.event.subscribingGetAsync import net.mamoe.mirai.event.subscribingGetOrNull
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
@ -128,20 +126,22 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact> : Packet, Bot
// endregion // endregion
// region 下载图片 // region 下载图片
/** /**
* 将图片下载到内存. * 获取图片下载链接
* *
* 非常不推荐这样做. * @return "http://gchat.qpic.cn/gchatpic_new/..."
*/ */
@Deprecated("内存使用效率十分低下", ReplaceWith("this.download()"), DeprecationLevel.WARNING) suspend inline fun Image.url(): String = bot.queryImageUrl(this@url)
suspend inline fun Image.downloadAsByteArray(): ByteArray = bot.run { download().readBytes() }
// TODO: 2020/2/5 为下载图片添加文件系统的存储方式
/** /**
* 将图片下载到内存缓存中 (使用 [IoBuffer.Pool]) * 获取图片下载链接并开始下载.
*
* @see ByteReadChannel.copyAndClose
* @see ByteReadChannel.copyTo
*/ */
suspend inline fun Image.download(): ByteReadPacket = bot.run { download() } suspend inline fun Image.channel(): ByteReadChannel = bot.openChannel(this)
// endregion // endregion
} }
@ -153,14 +153,14 @@ fun MessagePacket<*, *>.isContextIdenticalWith(another: MessagePacket<*, *>): Bo
} }
/** /**
* 挂起当前协程, 等待下一条 [MessagePacket.sender] [MessagePacket.subject] [P] 相同且通过 [筛选][filter] [MessagePacket] * 挂起当前协程, 等待下一条 [MessagePacket.sender] [MessagePacket.subject] [this] 相同且通过 [筛选][filter] [MessagePacket]
* *
* [filter] 抛出了一个异常, 本函数会立即抛出这个异常. * [filter] 抛出了一个异常, 本函数会立即抛出这个异常.
* *
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制 * @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
* @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值 * @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值
* *
* @see subscribingGetAsync 本函数的异步版本 * @see subscribingGet
*/ */
suspend inline fun <reified P : MessagePacket<*, *>> P.nextMessage( suspend inline fun <reified P : MessagePacket<*, *>> P.nextMessage(
timeoutMillis: Long = -1, timeoutMillis: Long = -1,
@ -172,13 +172,33 @@ suspend inline fun <reified P : MessagePacket<*, *>> P.nextMessage(
} }
/** /**
* 挂起当前协程, 等待下一条 [MessagePacket.sender] [MessagePacket.subject] [P] 相同的 [MessagePacket] * 挂起当前协程, 等待下一条 [MessagePacket.sender] [MessagePacket.subject] [this] 相同且通过 [筛选][filter] [MessagePacket]
*
* [filter] 抛出了一个异常, 本函数会立即抛出这个异常.
*
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
* @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值
* @return 消息链. 超时时返回 `null`
*
* @see subscribingGetOrNull
*/
suspend inline fun <reified P : MessagePacket<*, *>> P.nextMessageOrNull(
timeoutMillis: Long = -1,
crossinline filter: P.(P) -> Boolean
): MessageChain? {
return subscribingGetOrNull<P, P>(timeoutMillis) {
takeIf { this.isContextIdenticalWith(this@nextMessageOrNull) }?.takeIf { filter(it, it) }
}?.message
}
/**
* 挂起当前协程, 等待下一条 [MessagePacket.sender] [MessagePacket.subject] [this] 相同的 [MessagePacket]
* *
* [filter] 抛出了一个异常, 本函数会立即抛出这个异常. * [filter] 抛出了一个异常, 本函数会立即抛出这个异常.
* *
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制 * @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
* *
* @see subscribingGetAsync 本函数的异步版本 * @see subscribingGet
*/ */
suspend inline fun <reified P : MessagePacket<*, *>> P.nextMessage( suspend inline fun <reified P : MessagePacket<*, *>> P.nextMessage(
timeoutMillis: Long = -1 timeoutMillis: Long = -1
@ -187,3 +207,56 @@ suspend inline fun <reified P : MessagePacket<*, *>> P.nextMessage(
takeIf { this.isContextIdenticalWith(this@nextMessage) } takeIf { this.isContextIdenticalWith(this@nextMessage) }
}.message }.message
} }
/**
* 挂起当前协程, 等待下一条 [MessagePacket.sender] [MessagePacket.subject] [this] 相同的 [MessagePacket]
*
* [filter] 抛出了一个异常, 本函数会立即抛出这个异常.
*
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
* @return 消息链. 超时时返回 `null`
*
* @see subscribingGetOrNull
*/
suspend inline fun <reified P : MessagePacket<*, *>> P.nextMessageOrNull(
timeoutMillis: Long = -1
): MessageChain? {
return subscribingGetOrNull<P, P>(timeoutMillis) {
takeIf { this.isContextIdenticalWith(this@nextMessageOrNull) }
}?.message
}
/**
* 挂起当前协程, 等待下一条 [MessagePacket.sender] [MessagePacket.subject] [this] 相同的 [MessagePacket]
*
* [filter] 抛出了一个异常, 本函数会立即抛出这个异常.
*
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
*
* @see subscribingGet
*/
suspend inline fun <reified M : Message> MessagePacket<*, *>.nextMessageContaining(
timeoutMillis: Long = -1
): M {
return subscribingGet<MessagePacket<*, *>, MessagePacket<*, *>>(timeoutMillis) {
takeIf { this.isContextIdenticalWith(this@nextMessageContaining) }
}.message.first()
}
/**
* 挂起当前协程, 等待下一条 [MessagePacket.sender] [MessagePacket.subject] [this] 相同并含有 [M] 类型的消息的 [MessagePacket]
*
* [filter] 抛出了一个异常, 本函数会立即抛出这个异常.
*
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
* @return 指定类型的消息. 超时时返回 `null`
*
* @see subscribingGetOrNull
*/
suspend inline fun <reified M : Message> MessagePacket<*, *>.nextMessageContainingOrNull(
timeoutMillis: Long = -1
): M? {
return subscribingGetOrNull<MessagePacket<*, *>, MessagePacket<*, *>>(timeoutMillis) {
takeIf { this.isContextIdenticalWith(this@nextMessageContainingOrNull) }
}?.message?.first()
}

View File

@ -12,8 +12,11 @@ package net.mamoe.mirai.message
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.recallIn
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.getValue import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef import net.mamoe.mirai.utils.unsafeWeakRef
@ -27,7 +30,6 @@ import net.mamoe.mirai.utils.unsafeWeakRef
* @see QQ.sendMessage 发送群消息, 返回回执此对象 * @see QQ.sendMessage 发送群消息, 返回回执此对象
*/ */
open class MessageReceipt<C : Contact>( open class MessageReceipt<C : Contact>(
val originalMessage: MessageChain,
private val source: MessageSource, private val source: MessageSource,
target: C target: C
) { ) {
@ -54,7 +56,7 @@ open class MessageReceipt<C : Contact>(
if (_isRecalled.compareAndSet(false, true)) { if (_isRecalled.compareAndSet(false, true)) {
when (val contact = target) { when (val contact = target) {
is Group -> { is Group -> {
contact.recall(source) contact.bot.recall(source)
} }
is QQ -> { is QQ -> {
TODO() TODO()
@ -77,7 +79,7 @@ open class MessageReceipt<C : Contact>(
if (_isRecalled.compareAndSet(false, true)) { if (_isRecalled.compareAndSet(false, true)) {
when (val contact = target) { when (val contact = target) {
is Group -> { is Group -> {
return contact.recallIn(source, millis) return contact.bot.recallIn(source, millis)
} }
is QQ -> { is QQ -> {
TODO() TODO()

View File

@ -27,9 +27,11 @@ interface MessageSource : Message {
companion object Key : Message.Key<MessageSource> companion object Key : Message.Key<MessageSource>
/** /**
* 序列号. 若是机器人发出去的消息, 请先 [确保 sequenceId 可用][ensureSequenceIdAvailable] * Mirai 中使用的 id.
* 32 位为 [sequenceId],
* 32 位为 [messageRandom]
*/ */
val sequenceId: Int val id: Long
/** /**
* 等待 [sequenceId] 获取, 确保其可用. * 等待 [sequenceId] 获取, 确保其可用.
@ -38,11 +40,6 @@ interface MessageSource : Message {
*/ */
suspend fun ensureSequenceIdAvailable() suspend fun ensureSequenceIdAvailable()
/**
* 实际上是个随机数, 但服务器确实是用它当做 uid
*/
val messageUid: Int
/** /**
* 发送时间, 单位为秒 * 发送时间, 单位为秒
*/ */
@ -65,11 +62,33 @@ interface MessageSource : Message {
} }
/** /**
* 消息唯一标识符. 实际上是个随机数, 但服务器确实是用它当做 uid * 序列号. 若是机器人发出去的消息, 请先 [确保 sequenceId 可用][MessageSource.ensureSequenceIdAvailable]
* @see MessageSource.id
*/ */
val MessageChain.messageUid get() = this[MessageSource].messageUid val MessageSource.sequenceId: Int get() = (this.id shr 32).toInt()
/**
* 消息随机数. 由服务器或客户端指定后不能更改. 它是消息 id 的一部分.
* @see MessageSource.id
*/
val MessageSource.messageRandom: Int get() = this.id.toInt()
// For MessageChain
/**
* 消息 id.
* @see MessageSource.id
*/
val MessageChain.id: Long get() = this[MessageSource].id
/** /**
* 消息序列号, 可能来自服务器也可以发送时赋值, 不唯一. * 消息序列号, 可能来自服务器也可以发送时赋值, 不唯一.
* @see MessageSource.id
*/ */
val MessageChain.sequenceId get() = this[MessageSource].sequenceId val MessageChain.sequenceId: Int get() = this[MessageSource].sequenceId
/**
* 消息随机数. 由服务器或客户端指定后不能更改. 它是消息 id 的一部分.
* @see MessageSource.id
*/
val MessageChain.messageRandom: Int get() = this[MessageSource].messageRandom

View File

@ -132,7 +132,7 @@ open class LockFreeLinkedList<E> {
addLastNode(element.asNode(tail)) addLastNode(element.asNode(tail))
} }
private fun addLastNode(node: Node<E>) { private fun addLastNode(node: LockFreeLinkedListNode<E>) {
while (true) { while (true) {
val tail = head.iterateBeforeFirst { it === tail } // find the last node. val tail = head.iterateBeforeFirst { it === tail } // find the last node.
if (tail.nextNodeRef.compareAndSet(this.tail, node)) { // ensure the last node is the last node if (tail.nextNodeRef.compareAndSet(this.tail, node)) { // ensure the last node is the last node
@ -146,9 +146,9 @@ open class LockFreeLinkedList<E> {
*/ */
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
open fun addAll(iterable: Iterable<E>) { open fun addAll(iterable: Iterable<E>) {
var firstNode: Node<E>? = null var firstNode: LockFreeLinkedListNode<E>? = null
var currentNode: Node<E>? = null var currentNode: LockFreeLinkedListNode<E>? = null
iterable.forEach { iterable.forEach {
val nextNode = it.asNode(tail) val nextNode = it.asNode(tail)
if (firstNode == null) { if (firstNode == null) {
@ -166,9 +166,9 @@ open class LockFreeLinkedList<E> {
*/ */
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
open fun addAll(iterable: Sequence<E>) { open fun addAll(iterable: Sequence<E>) {
var firstNode: Node<E>? = null var firstNode: LockFreeLinkedListNode<E>? = null
var currentNode: Node<E>? = null var currentNode: LockFreeLinkedListNode<E>? = null
iterable.forEach { iterable.forEach {
val nextNode = it.asNode(tail) val nextNode = it.asNode(tail)
if (firstNode == null) { if (firstNode == null) {
@ -190,7 +190,7 @@ open class LockFreeLinkedList<E> {
val node = LazyNode(tail, supplier) val node = LazyNode(tail, supplier)
while (true) { while (true) {
var current: Node<E> = head var current: LockFreeLinkedListNode<E> = head
findLastNode@ while (true) { findLastNode@ while (true) {
if (current.isValidElementNode() && filter(current.nodeValue)) if (current.isValidElementNode() && filter(current.nodeValue))
@ -208,13 +208,14 @@ open class LockFreeLinkedList<E> {
} }
@PublishedApi // limitation by atomicfu @PublishedApi // limitation by atomicfu
internal fun <E> Node<E>.compareAndSetNextNodeRef(expect: Node<E>, update: Node<E>) = this.nextNodeRef.compareAndSet(expect, update) internal fun <E> LockFreeLinkedListNode<E>.compareAndSetNextNodeRef(expect: LockFreeLinkedListNode<E>, update: LockFreeLinkedListNode<E>) =
this.nextNodeRef.compareAndSet(expect, update)
override fun toString(): String = joinToString() override fun toString(): String = joinToString()
@Suppress("unused") @Suppress("unused")
internal fun getLinkStructure(): String = buildString { internal fun getLinkStructure(): String = buildString {
head.childIterateReturnsLastSatisfying<Node<*>>({ head.childIterateReturnsLastSatisfying<LockFreeLinkedListNode<*>>({
append(it.toString()) append(it.toString())
append(" <- ") append(" <- ")
it.nextNode it.nextNode
@ -240,7 +241,7 @@ open class LockFreeLinkedList<E> {
// physically remove: try to fix the link // physically remove: try to fix the link
var next: Node<E> = toRemove.nextNode var next: LockFreeLinkedListNode<E> = toRemove.nextNode
while (next !== tail && next.isRemoved()) { while (next !== tail && next.isRemoved()) {
next = next.nextNode next = next.nextNode
} }
@ -269,7 +270,7 @@ open class LockFreeLinkedList<E> {
// physically remove: try to fix the link // physically remove: try to fix the link
var next: Node<E> = toRemove.nextNode var next: LockFreeLinkedListNode<E> = toRemove.nextNode
while (next !== tail && next.isRemoved()) { while (next !== tail && next.isRemoved()) {
next = next.nextNode next = next.nextNode
} }
@ -282,7 +283,7 @@ open class LockFreeLinkedList<E> {
/** /**
* 动态计算的大小 * 动态计算的大小
*/ */
val size: Int get() = head.countChildIterate<Node<E>>({ it.nextNode }, { it !is Tail }) - 1 // empty head is always included val size: Int get() = head.countChildIterate<LockFreeLinkedListNode<E>>({ it.nextNode }, { it !is Tail }) - 1 // empty head is always included
open operator fun contains(element: E): Boolean { open operator fun contains(element: E): Boolean {
forEach { if (it == element) return true } forEach { if (it == element) return true }
@ -295,7 +296,7 @@ open class LockFreeLinkedList<E> {
open fun isEmpty(): Boolean = head.allMatching { it.isValidElementNode().not() } open fun isEmpty(): Boolean = head.allMatching { it.isValidElementNode().not() }
inline fun forEach(block: (E) -> Unit) { inline fun forEach(block: (E) -> Unit) {
var node: Node<E> = head var node: LockFreeLinkedListNode<E> = head
while (true) { while (true) {
if (node === tail) return if (node === tail) return
node.letValueIfValid(block) node.letValueIfValid(block)
@ -303,6 +304,15 @@ open class LockFreeLinkedList<E> {
} }
} }
inline fun forEachNode(block: (LockFreeLinkedListNode<E>) -> Unit) {
var node: LockFreeLinkedListNode<E> = head
while (true) {
if (node === tail) return
node.letValueIfValid { block(node) }
node = node.nextNode
}
}
@Suppress("unused") @Suppress("unused")
open fun clear() { open fun clear() {
val first = head.nextNode val first = head.nextNode
@ -638,14 +648,14 @@ open class LockFreeLinkedList<E> {
// region internal // region internal
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
private inline fun <E> E.asNode(nextNode: Node<E>): Node<E> = Node(nextNode, this) private inline fun <E> E.asNode(nextNode: LockFreeLinkedListNode<E>): LockFreeLinkedListNode<E> = LockFreeLinkedListNode(nextNode, this)
/** /**
* Self-iterate using the [iterator], until [mustBeTrue] returns `false`. * Self-iterate using the [iterator], until [mustBeTrue] returns `false`.
* Returns the element at the last time when the [mustBeTrue] returns `true` * Returns the element at the last time when the [mustBeTrue] returns `true`
*/ */
@PublishedApi @PublishedApi
internal inline fun <N : Node<*>> N.childIterateReturnsLastSatisfying(iterator: (N) -> N, mustBeTrue: (N) -> Boolean): N { internal inline fun <N : LockFreeLinkedListNode<*>> N.childIterateReturnsLastSatisfying(iterator: (N) -> N, mustBeTrue: (N) -> Boolean): N {
if (!mustBeTrue(this)) return this if (!mustBeTrue(this)) return this
var value: N = this var value: N = this
@ -703,9 +713,9 @@ private inline fun <E> E.countChildIterate(iterator: (E) -> E, mustBeTrue: (E) -
@PublishedApi @PublishedApi
internal class LazyNode<E> @PublishedApi internal constructor( internal class LazyNode<E> @PublishedApi internal constructor(
nextNode: Node<E>, nextNode: LockFreeLinkedListNode<E>,
private val valueComputer: () -> E private val valueComputer: () -> E
) : Node<E>(nextNode, null) { ) : LockFreeLinkedListNode<E>(nextNode, null) {
private val initialized = atomic(false) private val initialized = atomic(false)
private val value: AtomicRef<E?> = atomic(null) private val value: AtomicRef<E?> = atomic(null)
@ -727,20 +737,19 @@ internal class LazyNode<E> @PublishedApi internal constructor(
} }
@PublishedApi @PublishedApi
internal class Head<E>(nextNode: Node<E>) : Node<E>(nextNode, null) { internal class Head<E>(nextNode: LockFreeLinkedListNode<E>) : LockFreeLinkedListNode<E>(nextNode, null) {
override fun toString(): String = "Head" override fun toString(): String = "Head"
override val nodeValue: Nothing get() = error("Internal error: trying to get the value of a Head") override val nodeValue: Nothing get() = error("Internal error: trying to get the value of a Head")
} }
@PublishedApi @PublishedApi
internal open class Tail<E> : Node<E>(null, null) { internal open class Tail<E> : LockFreeLinkedListNode<E>(null, null) {
override fun toString(): String = "Tail" override fun toString(): String = "Tail"
override val nodeValue: Nothing get() = error("Internal error: trying to get the value of a Tail") override val nodeValue: Nothing get() = error("Internal error: trying to get the value of a Tail")
} }
@PublishedApi open class LockFreeLinkedListNode<E>(
internal open class Node<E>( nextNode: LockFreeLinkedListNode<E>?,
nextNode: Node<E>?,
private var initialNodeValue: E? private var initialNodeValue: E?
) { ) {
/* /*
@ -754,10 +763,11 @@ internal open class Node<E>(
open val nodeValue: E get() = initialNodeValue ?: error("Internal error: nodeValue is not initialized") open val nodeValue: E get() = initialNodeValue ?: error("Internal error: nodeValue is not initialized")
val removed = atomic(false) @PublishedApi
internal val removed = atomic(false)
@Suppress("LeakingThis") @Suppress("LeakingThis")
val nextNodeRef: AtomicRef<Node<E>> = atomic(nextNode ?: this) internal val nextNodeRef: AtomicRef<LockFreeLinkedListNode<E>> = atomic(nextNode ?: this)
inline fun <R> letValueIfValid(block: (E) -> R): R? { inline fun <R> letValueIfValid(block: (E) -> R): R? {
if (!this.isValidElementNode()) { if (!this.isValidElementNode()) {
@ -770,7 +780,8 @@ internal open class Node<E>(
/** /**
* Short cut for accessing [nextNodeRef] * Short cut for accessing [nextNodeRef]
*/ */
var nextNode: Node<E> @PublishedApi
internal var nextNode: LockFreeLinkedListNode<E>
get() = nextNodeRef.value get() = nextNodeRef.value
set(value) { set(value) {
nextNodeRef.value = value nextNodeRef.value = value
@ -779,7 +790,7 @@ internal open class Node<E>(
/** /**
* Returns the former node of the last node whence [filter] returns true * Returns the former node of the last node whence [filter] returns true
*/ */
inline fun iterateBeforeFirst(filter: (Node<E>) -> Boolean): Node<E> = inline fun iterateBeforeFirst(filter: (LockFreeLinkedListNode<E>) -> Boolean): LockFreeLinkedListNode<E> =
this.childIterateReturnsLastSatisfying({ it.nextNode }, { !filter(it) }) this.childIterateReturnsLastSatisfying({ it.nextNode }, { !filter(it) })
/** /**
@ -788,7 +799,8 @@ internal open class Node<E>(
* Head, which is this, is also being tested. * Head, which is this, is also being tested.
* [Tail], is not being tested. * [Tail], is not being tested.
*/ */
inline fun allMatching(condition: (Node<E>) -> Boolean): Boolean = this.childIterateReturnsLastSatisfying({ it.nextNode }, condition) !is Tail inline fun allMatching(condition: (LockFreeLinkedListNode<E>) -> Boolean): Boolean =
this.childIterateReturnsLastSatisfying({ it.nextNode }, condition) !is Tail
/** /**
* Stop on and returns the former element of the element that is [equals] to the [element] * Stop on and returns the former element of the element that is [equals] to the [element]
@ -796,23 +808,23 @@ internal open class Node<E>(
* E.g.: for `head <- 1 <- 2 <- 3 <- tail`, `iterateStopOnNodeValue(2)` returns the node whose value is 1 * E.g.: for `head <- 1 <- 2 <- 3 <- tail`, `iterateStopOnNodeValue(2)` returns the node whose value is 1
*/ */
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
internal inline fun iterateBeforeNodeValue(element: E): Node<E> = this.iterateBeforeFirst { it.isValidElementNode() && it.nodeValue == element } internal inline fun iterateBeforeNodeValue(element: E): LockFreeLinkedListNode<E> =
this.iterateBeforeFirst { it.isValidElementNode() && it.nodeValue == element }
} }
@PublishedApi // DO NOT INLINE: ATOMIC OPERATION fun <E> LockFreeLinkedListNode<E>.isRemoved() = this.removed.value
internal fun <E> Node<E>.isRemoved() = this.removed.value
@PublishedApi @PublishedApi
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
internal inline fun Node<*>.isValidElementNode(): Boolean = !isHead() && !isTail() && !isRemoved() internal inline fun LockFreeLinkedListNode<*>.isValidElementNode(): Boolean = !isHead() && !isTail() && !isRemoved()
@PublishedApi @PublishedApi
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
internal inline fun Node<*>.isHead(): Boolean = this is Head internal inline fun LockFreeLinkedListNode<*>.isHead(): Boolean = this is Head
@PublishedApi @PublishedApi
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
internal inline fun Node<*>.isTail(): Boolean = this is Tail internal inline fun LockFreeLinkedListNode<*>.isTail(): Boolean = this is Tail
// end region // end region

View File

@ -0,0 +1,117 @@
/*
* 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
*/
@file:JvmName("Utils")
@file:JvmMultifileClass
package net.mamoe.mirai.utils
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.readAvailable
import kotlinx.io.OutputStream
import kotlinx.io.core.Output
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.utils.io.ByteArrayPool
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
// copyTo
/**
* 从接收者管道读取所有数据并写入 [dst]. 不会关闭 [dst]
*/
suspend fun ByteReadChannel.copyTo(dst: OutputStream) {
ByteArrayPool.useInstance {
do {
val size = this.readAvailable(it)
dst.write(it, 0, size)
} while (size != 0)
}
}
/**
* 从接收者管道读取所有数据并写入 [dst]. 不会关闭 [dst]
*/
suspend fun ByteReadChannel.copyTo(dst: Output) {
ByteArrayPool.useInstance {
do {
val size = this.readAvailable(it)
dst.writeFully(it, 0, size)
} while (size != 0)
}
}
/* // 垃圾 kotlin, Unresolved reference: ByteWriteChannel
/**
* 从接收者管道读取所有数据并写入 [dst]. 不会关闭 [dst]
*/
suspend fun ByteReadChannel.copyTo(dst: kotlinx.coroutines.io.ByteWriteChannel) {
ByteArrayPool.useInstance {
do {
val size = this.readAvailable(it)
dst.writeFully(it, 0, size)
} while (size != 0)
}
}
*/
// copyAndClose
/**
* 从接收者管道读取所有数据并写入 [dst], 最终关闭 [dst]
*/
suspend fun ByteReadChannel.copyAndClose(dst: OutputStream) {
try {
ByteArrayPool.useInstance {
do {
val size = this.readAvailable(it)
dst.write(it, 0, size)
} while (size != 0)
}
} finally {
dst.close()
}
}
/**
* 从接收者管道读取所有数据并写入 [dst], 最终关闭 [dst]
*/
suspend fun ByteReadChannel.copyAndClose(dst: Output) {
try {
ByteArrayPool.useInstance {
do {
val size = this.readAvailable(it)
dst.writeFully(it, 0, size)
} while (size != 0)
}
} finally {
dst.close()
}
}
/*// 垃圾 kotlin, Unresolved reference: ByteWriteChannel
/**
* 从接收者管道读取所有数据并写入 [dst], 最终关闭 [dst]
*/
suspend fun ByteReadChannel.copyAndClose(dst: kotlinx.coroutines.io.ByteWriteChannel) {
dst.close(kotlin.runCatching {
ByteArrayPool.useInstance {
do {
val size = this.readAvailable(it)
dst.writeFully(it, 0, size)
} while (size != 0)
}
}.exceptionOrNull())
}
*/

View File

@ -11,23 +11,19 @@
package net.mamoe.mirai.message package net.mamoe.mirai.message
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.io.core.Input import kotlinx.io.core.Input
import kotlinx.io.core.use import kotlinx.io.core.use
import kotlinx.io.streams.inputStream
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.toExternalImage import net.mamoe.mirai.utils.copyAndClose
import net.mamoe.mirai.utils.copyTo
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.net.URL import java.net.URL
import javax.imageio.ImageIO
/** /**
* 一条从服务器接收到的消息事件. * 一条从服务器接收到的消息事件.
@ -72,16 +68,22 @@ actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> actual con
// endregion 发送图片 (扩展) // endregion 发送图片 (扩展)
// region 下载图片 (扩展) // region 下载图片 (扩展)
suspend inline fun Image.downloadTo(file: File): Long = file.outputStream().use { downloadTo(it) } suspend inline fun Image.downloadTo(file: File) = file.outputStream().use { downloadTo(it) }
/** /**
* 这个函数结束后不会关闭 [output]. 请务必解决好 [OutputStream.close] * 下载图片到 [output] 但不关闭这个 [output]
*/ */
suspend inline fun Image.downloadTo(output: OutputStream): Long = suspend inline fun Image.downloadTo(output: OutputStream) = channel().copyTo(output)
download().inputStream().use { input -> withContext(Dispatchers.IO) { input.copyTo(output) } }
suspend inline fun Image.downloadAsStream(): InputStream = download().inputStream() /**
suspend inline fun Image.downloadAsExternalImage(): ExternalImage = withContext(Dispatchers.IO) { download().toExternalImage() } * 下载图片到 [output] 并关闭这个 [output]
*/
suspend inline fun Image.downloadAndClose(output: OutputStream) = channel().copyAndClose(output)
/*
suspend inline fun Image.downloadAsStream(): InputStream = channel().asInputStream()
suspend inline fun Image.downloadAsExternalImage(): ExternalImage = withContext(Dispatchers.IO) { downloadAsStream().toExternalImage() }
suspend inline fun Image.downloadAsBufferedImage(): BufferedImage = withContext(Dispatchers.IO) { ImageIO.read(downloadAsStream()) } suspend inline fun Image.downloadAsBufferedImage(): BufferedImage = withContext(Dispatchers.IO) { ImageIO.read(downloadAsStream()) }
*/
// endregion // endregion
} }

View File

@ -1,6 +1,5 @@
package net.mamoe.mirai.japt; package net.mamoe.mirai.japt;
import kotlinx.io.core.ByteReadPacket;
import net.mamoe.mirai.Bot; import net.mamoe.mirai.Bot;
import net.mamoe.mirai.BotAccount; import net.mamoe.mirai.BotAccount;
import net.mamoe.mirai.BotFactoryJvmKt; import net.mamoe.mirai.BotFactoryJvmKt;
@ -153,18 +152,16 @@ public interface BlockingBot {
// region actions // region actions
@NotNull
byte[] downloadAsByteArray(@NotNull Image image);
@NotNull
ByteReadPacket download(@NotNull Image image);
/** /**
* 下载图片到 {@code outputStream}. * 下载图片到 {@code outputStream}.
* <p>
* 不会自动关闭 {@code outputStream} * 不会自动关闭 {@code outputStream}
*/ */
void download(@NotNull Image image, @NotNull OutputStream outputStream); void downloadTo(@NotNull Image image, @NotNull OutputStream outputStream);
/**
* 下载图片到 {@code outputStream} 并关闭 stream
*/
void downloadAndClose(@NotNull Image image, @NotNull OutputStream outputStream);
/** /**
* 添加一个好友 * 添加一个好友

View File

@ -10,8 +10,6 @@
package net.mamoe.mirai.japt.internal package net.mamoe.mirai.japt.internal
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readBytes
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotAccount import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.contact.QQ
@ -23,14 +21,12 @@ import net.mamoe.mirai.japt.BlockingGroup
import net.mamoe.mirai.japt.BlockingQQ import net.mamoe.mirai.japt.BlockingQQ
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.toList
import java.io.OutputStream import java.io.OutputStream
import java.util.stream.Stream import java.util.stream.Stream
import kotlin.streams.asStream import kotlin.streams.asStream
@UseExperimental(MiraiInternalAPI::class, MiraiExperimentalAPI::class)
internal class BlockingBotImpl(private val bot: Bot) : BlockingBot { internal class BlockingBotImpl(private val bot: Bot) : BlockingBot {
@MiraiInternalAPI @MiraiInternalAPI
override fun getAccount(): BotAccount = bot.account override fun getAccount(): BotAccount = bot.account
@ -51,7 +47,6 @@ internal class BlockingBotImpl(private val bot: Bot) : BlockingBot {
override fun getFriend(id: Long): BlockingQQ = bot.getFriend(id).blocking() override fun getFriend(id: Long): BlockingQQ = bot.getFriend(id).blocking()
override fun queryGroupList(): Stream<Long> = runBlocking { bot.queryGroupList() }.asStream() override fun queryGroupList(): Stream<Long> = runBlocking { bot.queryGroupList() }.asStream()
@UseExperimental(MiraiInternalAPI::class)
override fun getGroupList(): List<BlockingGroup> = bot.groups.delegate.toList().map { it.blocking() } override fun getGroupList(): List<BlockingGroup> = bot.groups.delegate.toList().map { it.blocking() }
override fun queryGroupInfo(code: Long): GroupInfo = runBlocking { bot.queryGroupInfo(code) } override fun queryGroupInfo(code: Long): GroupInfo = runBlocking { bot.queryGroupInfo(code) }
@ -59,10 +54,8 @@ internal class BlockingBotImpl(private val bot: Bot) : BlockingBot {
override fun getGroup(id: Long): BlockingGroup = runBlocking { bot.getGroup(id).blocking() } override fun getGroup(id: Long): BlockingGroup = runBlocking { bot.getGroup(id).blocking() }
override fun getNetwork(): BotNetworkHandler = bot.network override fun getNetwork(): BotNetworkHandler = bot.network
override fun login() = runBlocking { bot.login() } override fun login() = runBlocking { bot.login() }
override fun downloadAsByteArray(image: Image): ByteArray = bot.run { runBlocking { image.download().readBytes() } } override fun downloadTo(image: Image, outputStream: OutputStream) = bot.run { runBlocking { openChannel(image).copyTo(outputStream) } }
override fun download(image: Image): ByteReadPacket = bot.run { runBlocking { image.download() } } override fun downloadAndClose(image: Image, outputStream: OutputStream) = bot.run { runBlocking { openChannel(image).copyAndClose(outputStream) } }
override fun download(image: Image, outputStream: OutputStream) = bot.run { runBlocking { image.downloadTo(outputStream) } }
override fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult = runBlocking { bot.addFriend(id, message, remark) } override fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult = runBlocking { bot.addFriend(id, message, remark) }
override fun approveFriendAddRequest(id: Long, remark: String?) = runBlocking { bot.approveFriendAddRequest(id, remark) } override fun approveFriendAddRequest(id: Long, remark: String?) = runBlocking { bot.approveFriendAddRequest(id, remark) }
override fun close(throwable: Throwable?) = bot.close(throwable) override fun close(throwable: Throwable?) = bot.close(throwable)