Support recall, add MessageReceipt

This commit is contained in:
Him188 2020-02-21 18:21:58 +08:00
parent ff8e6e8b80
commit 587fab23e1
23 changed files with 530 additions and 181 deletions

View File

@ -16,14 +16,13 @@ import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.data.CustomFaceFromFile
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.NotOnlineImageFromFile
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
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.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.image.ImgStore
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
@ -66,7 +65,7 @@ internal class QQImpl(
override val nick: String
get() = friendInfo.nick
override suspend fun sendMessage(message: MessageChain) {
override suspend fun sendMessage(message: MessageChain): MessageReceipt<QQ> {
val event = FriendMessageSendEvent(this, message).broadcast()
if (event.isCancelled) {
throw EventCancelledException("cancelled by FriendMessageSendEvent")
@ -80,6 +79,7 @@ internal class QQImpl(
).sendAndExpect<MessageSvc.PbSendMsg.Response>() is MessageSvc.PbSendMsg.Response.SUCCESS
) { "send message failed" }
}
return MessageReceipt(message, this)
}
override suspend fun uploadImage(image: ExternalImage): Image = try {
@ -325,6 +325,24 @@ internal class GroupImpl(
override lateinit var owner: Member
@UseExperimental(MiraiExperimentalAPI::class)
override val botAsMember: Member by lazy {
Member(object : MemberInfo {
override val nameCard: String
get() = bot.nick // TODO: 2020/2/21 机器人在群内的昵称获取
override val permission: MemberPermission
get() = botPermission
override val specialTitle: String
get() = "" // TODO: 2020/2/21 获取机器人在群里的头衔
override val muteTimestamp: Int
get() = botMuteRemaining
override val uin: Long
get() = bot.uin
override val nick: String
get() = bot.nick
})
}
@UseExperimental(MiraiExperimentalAPI::class)
override lateinit var botPermission: MemberPermission
@ -340,6 +358,9 @@ internal class GroupImpl(
override val members: ContactList<Member> = ContactList(members.mapNotNull {
if (it.uin == bot.uin) {
botPermission = it.permission
if (it.permission == MemberPermission.OWNER) {
owner = botAsMember
}
null
} else Member(it).also { member ->
if (member.permission == MemberPermission.OWNER) {
@ -475,6 +496,18 @@ internal class GroupImpl(
TODO("not implemented")
}
override suspend fun recall(source: MessageSource) {
if (source.senderId != bot.uin) {
checkBotPermissionOperator()
}
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)
override fun Member(memberInfo: MemberInfo): Member {
return MemberImpl(
@ -498,7 +531,7 @@ internal class GroupImpl(
return members.delegate.filteringGetOrNull { it.id == id }
}
override suspend fun sendMessage(message: MessageChain) {
override suspend fun sendMessage(message: MessageChain): MessageReceipt<Group> {
check(!isBotMuted) { "bot is muted. Remaining seconds=$botMuteRemaining" }
val event = GroupMessageSendEvent(this, message).broadcast()
if (event.isCancelled) {
@ -514,6 +547,11 @@ internal class GroupImpl(
response is MessageSvc.PbSendMsg.Response.SUCCESS
) { "send message failed: $response" }
}
((message.last() as MessageSource) as MessageSvc.PbSendMsg.MessageSourceFromSend)
.startWaitingSequenceId(this)
return MessageReceipt(message, this)
}
override suspend fun uploadImage(image: ExternalImage): Image = try {

View File

@ -0,0 +1,38 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.qqandroid.network.protocol.data.proto
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.io.ProtoBuf
class MsgRevokeUserDef : ProtoBuf {
@Serializable
class MsgInfoUserDef(
@SerialId(1) val longMessageFlag: Int = 0,
@SerialId(2) val longMsgInfo: List<MsgInfoDef>? = null,
@SerialId(3) val fileUuid: List<String> = listOf()
) : ProtoBuf {
@Serializable
class MsgInfoDef(
@SerialId(1) val msgSeq: Int = 0,
@SerialId(2) val longMsgId: Int = 0,
@SerialId(3) val longMsgNum: Int = 0,
@SerialId(4) val longMsgIndex: Int = 0
) : ProtoBuf
}
@Serializable
class UinTypeUserDef(
@SerialId(1) val fromUinType: Int = 0,
@SerialId(2) val fromGroupCode: Long = 0L,
@SerialId(3) val fileUuid: List<String> = listOf()
) : ProtoBuf
}

View File

@ -14,6 +14,7 @@ import kotlinx.io.pool.useInstance
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.qqandroid.QQAndroidBot
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.image.ImgStore
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
@ -138,7 +139,8 @@ internal object KnownPacketFactories {
TroopManagement.GetGroupInfo,
TroopManagement.EditGroupNametag,
TroopManagement.Kick,
Heartbeat.Alive
Heartbeat.Alive,
PbMessageSvc.PbMsgWithDraw
)
object IncomingFactories : List<IncomingPacketFactory<*>> by mutableListOf(

View File

@ -0,0 +1,91 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.io.serialization.writeProtoBuf
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgRevokeUserDef
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
internal class PbMessageSvc {
object PbMsgWithDraw : OutgoingPacketFactory<PbMsgWithDraw.Response>(
"PbMessageSvc.PbMsgWithDraw"
) {
sealed class Response : Packet {
object Success : Response() {
override fun toString(): String {
return "PbMessageSvc.PbMsgWithDraw.Response.Success"
}
}
data class Failed(
val result: Int,
val errorMessage: String
) : Response()
}
// 12 1A 08 01 10 00 18 E7 C1 AD B8 02 22 0A 08 BF BA 03 10 BF 81 CB B7 03 2A 02 08 00
fun Group(
client: QQAndroidClient,
groupCode: Long,
messageSequenceId: Int, // 56639
messageRandom: Int, // 921878719
messageType: Int = 0
): OutgoingPacket = buildOutgoingUniPacket(client) {
writeProtoBuf(
MsgSvc.PbMsgWithDrawReq.serializer(),
MsgSvc.PbMsgWithDrawReq(
groupWithDraw = listOf(
MsgSvc.PbGroupMsgWithDrawReq(
subCmd = 1,
groupType = 0, // 普通群
groupCode = groupCode,
msgList = listOf(
MsgSvc.PbGroupMsgWithDrawReq.MessageInfo(
msgSeq = messageSequenceId,
msgRandom = messageRandom,
msgType = messageType
)
),
userdef = MsgRevokeUserDef.MsgInfoUserDef(
longMessageFlag = 0
).toByteArray(MsgRevokeUserDef.MsgInfoUserDef.serializer())
)
)
)
)
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val resp = readProtoBuf(MsgSvc.PbMsgWithDrawResp.serializer())
resp.groupWithDraw?.firstOrNull()?.let {
if (it.result != 0) {
return Response.Failed(it.result, it.errmsg)
}
return Response.Success
}
resp.c2cWithDraw?.firstOrNull()?.let {
if (it.result != 0) {
return Response.Failed(it.result, it.errmsg)
}
return Response.Success
}
return Response.Failed(-1, "No response")
}
}
}

View File

@ -9,18 +9,25 @@
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.ListeningStatus
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.event.subscribe
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.addOrRemove
import net.mamoe.mirai.qqandroid.GroupImpl
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.io.serialization.decodeUniPacket
@ -262,6 +269,36 @@ internal class MessageSvc {
}
}
internal class MessageSourceFromSend(
override val messageUid: Long,
override val time: Long,
override val senderId: Long,
override val groupId: Long,
override val sourceMessage: MessageChain
) : MessageSource {
lateinit var sequenceIdDeferred: CompletableDeferred<Int>
fun startWaitingSequenceId(contact: Contact) {
sequenceIdDeferred = CompletableDeferred()
contact.subscribe<OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt> { event ->
if (event.messageRandom == messageUid.toInt()) {
sequenceIdDeferred.complete(event.sequenceId)
return@subscribe ListeningStatus.STOPPED
}
return@subscribe ListeningStatus.LISTENING
}
}
@UseExperimental(ExperimentalCoroutinesApi::class)
override val sequenceId: Int
get() = sequenceIdDeferred.getCompleted()
override fun toString(): String {
return ""
}
}
/**
* 发送好友消息
*/
@ -271,9 +308,17 @@ internal class MessageSvc {
toUin: Long,
message: MessageChain
): 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())
val source = MessageSourceFromSend(
messageUid = Random.nextInt().absoluteValue.toLong() and 0xffffffff,
senderId = client.uin,
time = currentTimeSeconds + client.timeDifference,
groupId = 0,
sourceMessage = message
)
message.addOrRemove(source)
///return@buildOutgoingUniPacket
writeProtoBuf(
MsgSvc.PbSendMsgReq.serializer(), MsgSvc.PbSendMsgReq(
@ -285,8 +330,8 @@ internal class MessageSvc {
)
),
msgSeq = client.atomicNextMessageSequenceId(),
msgRand = Random.nextInt().absoluteValue,
syncCookie = SyncCookie(time = currentTimeSeconds).toByteArray(SyncCookie.serializer())
msgRand = source.messageUid.toInt(),
syncCookie = SyncCookie(time = source.time).toByteArray(SyncCookie.serializer())
// msgVia = 1
)
)
@ -302,11 +347,19 @@ internal class MessageSvc {
message: MessageChain
): OutgoingPacket = buildOutgoingUniPacket(client) {
val source = MessageSourceFromSend(
messageUid = Random.nextInt().absoluteValue.toLong() and 0xffffffff,
senderId = client.uin,
time = currentTimeSeconds + client.timeDifference,
groupId = groupCode,
sourceMessage = message
)
message.addOrRemove(source)
///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())
// DebugLogger.debug("sending group message: " + message.toRichTextElems().contentToString())
val seq = client.atomicNextMessageSequenceId()
///return@buildOutgoingUniPacket
writeProtoBuf(
MsgSvc.PbSendMsgReq.serializer(), MsgSvc.PbSendMsgReq(
@ -317,8 +370,8 @@ internal class MessageSvc {
elems = message.toRichTextElems()
)
),
msgSeq = seq,
msgRand = Random.nextInt().absoluteValue,
msgSeq = client.atomicNextMessageSequenceId(),
msgRand = source.messageUid.toInt(),
syncCookie = EMPTY_BYTE_ARRAY,
msgVia = 1
)

View File

@ -19,6 +19,7 @@ import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.NoPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.qqandroid.GroupImpl
@ -46,9 +47,14 @@ internal class OnlinePush {
/**
* 接受群消息
*/
internal object PbPushGroupMsg : IncomingPacketFactory<GroupMessage?>("OnlinePush.PbPushGroupMsg") {
internal object PbPushGroupMsg : IncomingPacketFactory<Packet?>("OnlinePush.PbPushGroupMsg") {
internal class SendGroupMessageReceipt(
val messageRandom: Int,
val sequenceId: Int
) : Packet, Event
@UseExperimental(ExperimentalStdlibApi::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): GroupMessage? {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet? {
// 00 00 02 E4 0A D5 05 0A 4F 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 52 20 00 28 BC 3D 30 8C 82 AB F1 05 38 D2 80 E0 8C 80 80 80 80 02 4A 21 08 E7 C1 AD B8 02 10 01 18 BA 05 22 09 48 69 6D 31 38 38 6D 6F 65 30 06 38 02 42 05 4D 69 72 61 69 50 01 58 01 60 00 88 01 08 12 06 08 01 10 00 18 00 1A F9 04 0A F6 04 0A 26 08 00 10 87 82 AB F1 05 18 B7 B4 BF 30 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 E6 03 42 E3 03 12 2A 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 22 00 2A 04 03 00 00 00 32 60 15 36 20 39 36 6B 45 31 41 38 35 32 32 39 64 63 36 39 38 34 37 39 37 37 62 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 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 31 32 31 32 41 38 C6 BB 8A A9 08 40 FB AE 9E C2 09 48 50 50 41 5A 00 60 01 6A 10 4E 18 58 22 0E 7B F8 0F C5 B1 34 48 83 74 D3 9C 72 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 31 39 38 3F 74 65 72 6D 3D 32 82 01 57 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 30 3F 74 65 72 6D 3D 32 B0 01 4D B8 01 2E C8 01 FF 05 D8 01 4D E0 01 2E FA 01 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 34 30 30 3F 74 65 72 6D 3D 32 80 02 4D 88 02 2E 12 45 AA 02 42 50 03 60 00 68 00 9A 01 39 08 09 20 BF 50 80 01 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 04 08 02 08 01 90 04 80 80 80 10 B8 04 00 C0 04 00 12 06 4A 04 08 00 40 01 12 14 82 01 11 0A 09 48 69 6D 31 38 38 6D 6F 65 18 06 20 08 28 03 10 8A CA 9D A1 07 1A 00
if (!bot.firstLoginSucceed) return null
val pbPushMsg = readProtoBuf(MsgOnlinePush.PbPushMsg.serializer())
@ -56,7 +62,7 @@ internal class OnlinePush {
val extraInfo: ImMsgBody.ExtraInfo? = pbPushMsg.msg.msgBody.richText.elems.firstOrNull { it.extraInfo != null }?.extraInfo
if (pbPushMsg.msg.msgHead.fromUin == bot.uin) {
return null
return SendGroupMessageReceipt(pbPushMsg.msg.msgBody.richText.attr!!.random, pbPushMsg.msg.msgHead.msgSeq)
}
val group = bot.getGroup(pbPushMsg.msg.msgHead.groupInfo!!.groupCode)

View File

@ -107,6 +107,8 @@ fun main() {
* 顶层方法. TCP 切掉头后直接来这里
*/
fun ByteReadPacket.decodeMultiClientToServerPackets() {
DebugLogger.enable()
PacketLogger.enable()
println("=======================处理客户端到服务器=======================")
var count = 0
while (remaining != 0L) {

View File

@ -25,7 +25,7 @@ fun main() {
println(
File(
"""
E:\Projects\QQAndroidFF\app\src\main\java\tencent\im\statsvc\getonline
E:\Projects\QQAndroidFF\app\src\main\java\tencent\im\msgrevoke
""".trimIndent()
)
.generateUnarrangedClasses().toMutableList().arrangeClasses().joinToString("\n\n")

View File

@ -84,7 +84,7 @@ abstract class Bot : CoroutineScope {
*/
@MiraiExperimentalAPI("还未支持")
val nick: String
get() = TODO("bot 昵称获取")
get() = ""// TODO("bot 昵称获取")
/**
* 日志记录器
@ -175,8 +175,6 @@ abstract class Bot : CoroutineScope {
*/
abstract suspend fun queryGroupMemberList(groupUin: Long, groupCode: Long, ownerId: Long): Sequence<MemberInfo>
// TODO 目前还不能构造群对象. 这将在以后支持
// endregion
// region network

View File

@ -18,6 +18,7 @@ import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.ImageUploadEvent
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.WeakRefProperty
@ -40,19 +41,24 @@ interface Contact : CoroutineScope {
*
* 对于 [QQ], `uin` `id` 是相同的意思.
* 对于 [Group], `groupCode` `id` 是相同的意思.
*
* @see QQ.id
* @see Group.id
*/
val id: Long
/**
* 向这个对象发送消息.
* 向这个对象发送消息. 发送成功后 [message] 中会添加 [MessageSource], 此后可以 [引用回复][MessageReceipt.quote]仅群聊 [撤回][MessageReceipt.recall] 这条消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. [引用回复][MessageReceipt.quote]仅群聊 [撤回][MessageReceipt.recall] 这条消息.
*/
suspend fun sendMessage(message: MessageChain)
suspend fun sendMessage(message: MessageChain): MessageReceipt<out Contact>
/**
* 上传一个图片以备发送.
@ -88,4 +94,4 @@ interface Contact : CoroutineScope {
suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.toChain())
suspend inline fun Contact.sendMessage(plain: String) = sendMessage(plain.singleChain())
suspend inline fun Contact.sendMessage(plain: String) = sendMessage(plain.toMessage())

View File

@ -11,11 +11,22 @@
package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.quote
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmName
/**
@ -83,10 +94,18 @@ interface Group : Contact, CoroutineScope {
override val id: Long
/**
* 群主
* 群主.
*
* @return 若机器人是群主, 返回 [botAsMember]. 否则返回相应的成员
*/
val owner: Member
/**
* [Bot] 在群内的 [Member] 实例
*/
@MiraiExperimentalAPI
val botAsMember: Member
/**
* 机器人被禁言还剩余多少秒
*
@ -133,6 +152,17 @@ interface Group : Contact, CoroutineScope {
@MiraiExperimentalAPI("还未支持")
suspend fun quit(): Boolean
/**
* 撤回这条消息.
*
* [Bot] 撤回自己的消息不需要权限.
* [Bot] 撤回群员的消息需要管理员权限.
*
* @throws PermissionDeniedException [Bot] 无权限操作时
* @see Group.recall (扩展函数) 接受参数 [MessageChain]
*/
suspend fun recall(source: MessageSource)
/**
* 构造一个 [Member].
* 非特殊情况请不要使用这个函数. 优先使用 [get].
@ -142,6 +172,19 @@ interface Group : Contact, CoroutineScope {
@JvmName("newMember")
fun Member(memberInfo: MemberInfo): Member
/**
* 向这个对象发送消息. 发送成功后 [message] 中会添加 [MessageSource], 此后可以 [引用回复][quote] [撤回][recall] 这条消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. 可进行撤回 ([MessageReceipt.recall])
*/
override suspend fun sendMessage(message: MessageChain): MessageReceipt<Group>
companion object {
/**
@ -184,7 +227,52 @@ interface Group : Contact, CoroutineScope {
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 delay 延迟的时间, 单位为毫秒
* @param coroutineContext 额外的 [CoroutineContext]
* @see recall
*/
fun Group.recallIn(
message: MessageSource,
delay: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) {
kotlinx.coroutines.delay(delay)
recall(message)
}
/**
* 在一段时间后撤回这条消息.
*
* @param delay 延迟的时间, 单位为毫秒
* @param coroutineContext 额外的 [CoroutineContext]
* @see recall
*/
fun Group.recallIn(
message: MessageChain,
delay: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) {
kotlinx.coroutines.delay(delay)
recall(message)
}
/**
* 返回机器人是否正在被禁言
*
* @see Group.botMuteRemaining 剩余禁言时间
*/
val Group.isBotMuted: Boolean get() = this.botMuteRemaining != 0

View File

@ -16,6 +16,12 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.FriendNameRemark
import net.mamoe.mirai.data.PreviousNameList
import net.mamoe.mirai.data.Profile
import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.MiraiExperimentalAPI
/**
@ -68,4 +74,17 @@ interface QQ : Contact, CoroutineScope {
*/
@MiraiExperimentalAPI("还未支持")
suspend fun queryRemark(): FriendNameRemark
/**
* 向这个对象发送消息. 发送成功后 [message] 中会添加 [MessageSource], 此后可以 [撤回][recall] 这条消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. 可进行撤回 ([MessageReceipt.recall])
*/
override suspend fun sendMessage(message: MessageChain): MessageReceipt<QQ>
}

View File

@ -62,4 +62,15 @@ interface GroupInfo {
* 机器人被禁言还剩时间, .
*/
val botMuteRemaining: Int
/*
/**
* 机器人的头衔
*/
val botSpecialTitle: String
/**
* 机器人的昵称
*/
val botNameCard: String*/
}

View File

@ -0,0 +1,11 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.event

View File

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

View File

@ -35,7 +35,7 @@ expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>(bot: Bot)
/**
* 仅内部使用, 请使用 [MessagePacket]
*/ // Tips: 在 IntelliJ 中 (左侧边栏) 打开 `Structure`, 可查看类结构
@Suppress("NOTHING_TO_INLINE")
@Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST")
@MiraiInternalAPI
abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : Packet, BotEvent {
/**
@ -73,20 +73,19 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) :
* 对于好友消息事件, 这个方法将会给好友 ([subject]) 发送消息
* 对于群消息事件, 这个方法将会给群 ([subject]) 发送消息
*/
suspend inline fun reply(message: MessageChain) = subject.sendMessage(message)
suspend inline fun reply(message: MessageChain): MessageReceipt<TSubject> = subject.sendMessage(message) as MessageReceipt<TSubject>
suspend inline fun reply(message: Message) = subject.sendMessage(message.toChain())
suspend inline fun reply(plain: String) = subject.sendMessage(plain.singleChain())
suspend inline fun reply(message: Message): MessageReceipt<TSubject> = subject.sendMessage(message.toChain()) as MessageReceipt<TSubject>
suspend inline fun reply(plain: String): MessageReceipt<TSubject> = subject.sendMessage(plain.toMessage().toChain()) as MessageReceipt<TSubject>
@JvmName("reply1")
suspend inline fun String.reply() = reply(this)
suspend inline fun String.reply(): MessageReceipt<TSubject> = reply(this)
@JvmName("reply1")
suspend inline fun Message.reply() = reply(this)
suspend inline fun Message.reply(): MessageReceipt<TSubject> = reply(this)
@JvmName("reply1")
suspend inline fun MessageChain.reply() = reply(this)
suspend inline fun MessageChain.reply(): MessageReceipt<TSubject> = reply(this)
// endregion
// region
@ -135,16 +134,4 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) :
*/
suspend inline fun Image.download(): ByteReadPacket = bot.run { download() }
// endregion
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("bot.getFriend(this.target)"))
fun At.qq(): QQ = bot.getFriend(this.target)
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("bot.getFriend(this.toLong())"))
fun Int.qq(): QQ = bot.getFriend(this.coerceAtLeastOrFail(0).toLong())
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("bot.getFriend(this)"))
fun Long.qq(): QQ = bot.getFriend(this.coerceAtLeastOrFail(0))
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("bot.getGroup(this)"))
fun Long.group(): Group = bot.getGroup(this)
}

View File

@ -0,0 +1,108 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.message
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.quote
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
/**
* 发送消息后得到的回执. 可用于撤回.
*
* 此对象持有 [Contact] 的弱引用, [Bot] 离线后将会释放引用, 届时 [target] 将无法访问.
*
* @see Group.sendMessage 发送群消息, 返回回执此对象
* @see QQ.sendMessage 发送群消息, 返回回执此对象
*/
open class MessageReceipt<C : Contact>(
val originalMessage: MessageChain,
target: C
) {
init {
require(target is Group || target is QQ) { "target must be either Group or QQ" }
}
/**
* 发送目标, [Group] [QQ]
*/
val target: C by target.unsafeWeakRef()
private val _isRecalled = atomic(false)
/**
* 判断消息是否已被撤回.
*/
val isRecalled: Boolean get() = _isRecalled.value
/**
* 撤回这条消息. [recall] [recallIn] 只能被调用一次.
*
* @see Group.recall
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
@UseExperimental(MiraiExperimentalAPI::class)
suspend fun recall() {
@Suppress("BooleanLiteralArgument")
if (_isRecalled.compareAndSet(false, true)) {
when (val contact = target) {
is Group -> {
contact.recall(originalMessage)
}
is QQ -> {
TODO()
}
else -> error("Unknown contact type")
}
} else error("message is already or planned to be recalled")
}
/**
* 撤回这条消息. [recall] [recallIn] 只能被调用一次.
*
* @param delay 延迟时间, 单位为毫秒
*
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
@UseExperimental(MiraiExperimentalAPI::class)
fun recallIn(delay: Long): Job {
@Suppress("BooleanLiteralArgument")
if (_isRecalled.compareAndSet(false, true)) {
when (val contact = target) {
is Group -> {
return contact.recallIn(originalMessage, delay)
}
is QQ -> {
TODO()
}
else -> error("Unknown contact type")
}
} else error("message is already or planned to be recalled")
}
/**
* 引用这条消息. 仅群消息能被引用
*
* @see MessageChain.quote 引用一条消息
*
* @throws IllegalStateException 当此消息不是群消息时
*/
@UseExperimental(MiraiExperimentalAPI::class)
open fun quote(): MessageChain {
val target = target
check(target is Group) { "quote is only available for GroupMessage" }
return this.originalMessage.quote(target.botAsMember)
}
}

View File

@ -13,9 +13,6 @@
package net.mamoe.mirai.message.data
import net.mamoe.mirai.message.data.NullMessageChain.toString
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.js.JsName
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
@ -68,6 +65,15 @@ interface MessageChain : Message, MutableList<Message> {
}
}
/**
* 先删除同类型的消息, 再添加 [message]
*/
fun MessageChain.addOrRemove(message: Message) {
val clazz = message::class
this.removeAll { clazz.isInstance(it) }
this.add(message)
}
/**
* 遍历每一个有内容的消息, [At], [AtAll], [PlainText], [Image], [Face], [XMLMessage]
*/
@ -132,6 +138,16 @@ fun MessageChain(vararg messages: Message): MessageChain =
if (messages.isEmpty()) EmptyMessageChain()
else MessageChainImpl(messages.toMutableList())
/**
* 构造 [MessageChain] 的快速途径 ( [Array] 创建)
* 若仅提供一个参数, 请考虑使用 [Message.toChain] 以优化性能
*/
@JvmName("newChain")
@JsName("newChain")
@Suppress("FunctionName")
fun MessageChain(message: Message): MessageChain =
MessageChainImpl(mutableListOf(message))
/**
* 构造 [MessageChain]
*/
@ -141,30 +157,6 @@ fun MessageChain(vararg messages: Message): MessageChain =
fun MessageChain(messages: Iterable<Message>): MessageChain =
MessageChainImpl(messages.toMutableList())
/**
* 构造单元素的不可修改的 [MessageChain]. 内部类实现为 [SingleMessageChain]
*
* 参数 [delegate] 不能为 [MessageChain] 的实例, 否则将会抛出异常.
* 使用 [Message.toChain] 将帮助提前处理这个问题.
*
* @param delegate 所构造的单元素 [MessageChain] 代表的 [Message]
* @throws IllegalArgumentException [delegate] [MessageChain] 的实例时
*
* @see Message.toChain receiver 模式
*/
@JvmName("newSingleMessageChain")
@JsName("newChain")
@MiraiExperimentalAPI
@UseExperimental(ExperimentalContracts::class)
@Suppress("FunctionName")
fun SingleMessageChain(delegate: Message): MessageChain {
contract {
returns() implies (delegate !is MessageChain)
}
require(delegate !is MessageChain) { "delegate for SingleMessageChain should not be any instance of MessageChain" }
return SingleMessageChainImpl(delegate)
}
/**
* 得到包含 [this] [MessageChain].
@ -387,97 +379,3 @@ internal inline class MessageChainImpl constructor(
// endregion
}
/**
* 单个成员的不可修改的 [MessageChain].
*
* 在连接时将会把它当做一个普通 [Message] 看待, 但它不能被 [plusAssign]
*/
@PublishedApi
internal inline class SingleMessageChainImpl(
private val delegate: Message
) : Message, MutableList<Message>,
MessageChain {
// region Message override
override operator fun contains(sub: String): Boolean = delegate.contains(sub)
override fun followedBy(tail: Message): MessageChain {
require(tail !is SingleOnly) { "SingleOnly Message cannot follow another message" }
return if (tail is MessageChain) tail.apply { followedBy(delegate) }
else MessageChain(delegate, tail)
}
override fun plusAssign(message: Message) =
throw UnsupportedOperationException("SingleMessageChainImpl cannot be plusAssigned")
override fun toString(): String = delegate.toString()
// endregion
// region MutableList override
override fun containsAll(elements: Collection<Message>): Boolean = elements.all { it == delegate }
override operator fun get(index: Int): Message = if (index == 0) delegate else throw NoSuchElementException()
override fun indexOf(element: Message): Int = if (delegate == element) 0 else -1
override fun isEmpty(): Boolean = false
override fun lastIndexOf(element: Message): Int = if (delegate == element) 0 else -1
override fun add(element: Message): Boolean = throw UnsupportedOperationException()
override fun add(index: Int, element: Message) = throw UnsupportedOperationException()
override fun addAll(index: Int, elements: Collection<Message>): Boolean = throw UnsupportedOperationException()
override fun addAll(elements: Collection<Message>): Boolean = throw UnsupportedOperationException()
override fun clear() = throw UnsupportedOperationException()
override fun listIterator(): MutableListIterator<Message> = object : MutableListIterator<Message> {
private var hasNext = true
override fun hasPrevious(): Boolean = !hasNext
override fun nextIndex(): Int = if (hasNext) 0 else -1
override fun previous(): Message =
if (hasPrevious()) {
hasNext = true
delegate
} else throw NoSuchElementException()
override fun previousIndex(): Int = if (!hasNext) 0 else -1
override fun add(element: Message) = throw UnsupportedOperationException()
override fun hasNext(): Boolean = hasNext
override fun next(): Message =
if (hasNext) {
hasNext = false
delegate
} else throw NoSuchElementException()
override fun remove() = throw UnsupportedOperationException()
override fun set(element: Message) = throw UnsupportedOperationException()
}
override fun listIterator(index: Int): MutableListIterator<Message> =
if (index == 0) listIterator() else throw UnsupportedOperationException()
override fun remove(element: Message): Boolean = throw UnsupportedOperationException()
override fun removeAll(elements: Collection<Message>): Boolean = throw UnsupportedOperationException()
override fun removeAt(index: Int): Message = throw UnsupportedOperationException()
override fun retainAll(elements: Collection<Message>): Boolean = throw UnsupportedOperationException()
override fun set(index: Int, element: Message): Message = throw UnsupportedOperationException()
override fun subList(fromIndex: Int, toIndex: Int): MutableList<Message> {
return if (fromIndex == 0) when (toIndex) {
1 -> mutableListOf<Message>(this)
0 -> mutableListOf()
else -> throw UnsupportedOperationException()
}
else throw UnsupportedOperationException()
}
override fun iterator(): MutableIterator<Message> = object : MutableIterator<Message> {
private var hasNext = true
override fun hasNext(): Boolean = hasNext
override fun next(): Message =
if (hasNext) {
hasNext = false
delegate
} else throw NoSuchElementException()
override fun remove() = throw UnsupportedOperationException()
}
override operator fun contains(element: Message): Boolean = element == delegate
override val size: Int get() = 1
// endregion
}

View File

@ -47,7 +47,7 @@ interface MessageSource : Message {
val senderId: Long
/**
* 群号码
* 群号码, 0 时则来自好友消息
*/
val groupId: Long

View File

@ -39,14 +39,3 @@ inline class PlainText(val stringValue: String) : Message {
*/
@Suppress("NOTHING_TO_INLINE")
inline fun String.toMessage(): PlainText = PlainText(this)
/**
* 得到包含作为 [PlainText] [this] [MessageChain].
*
* @return 唯一成员且不可修改的 [SingleMessageChainImpl]
*
* @see SingleMessageChain
* @see SingleMessageChainImpl
*/
@Suppress("NOTHING_TO_INLINE")
inline fun String.singleChain(): MessageChain = SingleMessageChainImpl(this.toMessage())