Jelajahi Sumber

Support Nudge (#600)

* Support Nudge message

* Delete duplicated code

* Renamed NudgeManager and using boolean return value in Nudge method

* Fix document and remove JvmSynthetic annotation

* Remove test code

* Add document for bot object

* use checkIsFriendImpl to instead cast operation

* Add a space between char and number

* Change the text of bot and member to reference

* Revert change in QQAndroidBotNetworkHandler

* Make debug log more clearly

* Support tracking chat target in FriendNudgeEvent

* Add LICENSE in NudgePacket.kt
sandtechnology 5 tahun lalu
induk
melakukan
59f465f66b

+ 34 - 4
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/FriendImpl.kt

@@ -38,13 +38,11 @@ import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.network.highway.postImage
 import net.mamoe.mirai.qqandroid.network.highway.sizeToString
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x352
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.NudgePacket
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
 import net.mamoe.mirai.qqandroid.utils.MiraiPlatformUtils
 import net.mamoe.mirai.qqandroid.utils.toUHexString
-import net.mamoe.mirai.utils.ExternalImage
-import net.mamoe.mirai.utils.getValue
-import net.mamoe.mirai.utils.unsafeWeakRef
-import net.mamoe.mirai.utils.verbose
+import net.mamoe.mirai.utils.*
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.contract
 import kotlin.coroutines.CoroutineContext
@@ -85,6 +83,9 @@ internal class FriendImpl(
     override val nick: String
         get() = friendInfo.nick
 
+    @Suppress("PropertyName")
+    var _nudgeTimestamp: Long = 0L
+
     @JvmSynthetic
     @Suppress("DuplicatedCode")
     override suspend fun sendMessage(message: Message): MessageReceipt<Friend> {
@@ -98,6 +99,35 @@ internal class FriendImpl(
         }
     }
 
+    override suspend fun nudge(): Boolean {
+        return nudge(this@FriendImpl)
+    }
+
+    override suspend fun nudgeBot(): Boolean {
+        return bot.selfQQ.checkIsFriendImpl().nudge(this@FriendImpl)
+    }
+
+    private suspend fun nudge(chatTarget: Friend): Boolean {
+        if (bot.configuration.protocol != BotConfiguration.MiraiProtocol.ANDROID_PHONE) {
+            throw UnsupportedOperationException("nudge is supported only with protocol ANDROID_PHONE")
+        }
+        val coolDown = currentTimeMillis - _nudgeTimestamp;
+        check(coolDown > 10000L) {
+            "Cooling, Please wait $coolDown ms and try again"
+        }
+        bot.network.run {
+            return NudgePacket.friendInvoke(
+                client = bot.client,
+                targetUin = [email protected],
+                chatTargetUin = chatTarget.id
+            ).sendAndExpect<NudgePacket.Response>().success.also { success ->
+                if (success) {
+                    _nudgeTimestamp = currentTimeMillis
+                }
+            }
+        }
+    }
+
     @JvmSynthetic
     override suspend fun uploadImage(image: ExternalImage): Image = try {
         @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")

+ 26 - 4
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/MemberImpl.kt

@@ -26,13 +26,11 @@ import net.mamoe.mirai.qqandroid.message.MessageSourceToTempImpl
 import net.mamoe.mirai.qqandroid.message.ensureSequenceIdAvailable
 import net.mamoe.mirai.qqandroid.message.firstIsInstanceOrNull
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.StTroopMemberInfo
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.NudgePacket
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.createToTemp
-import net.mamoe.mirai.utils.ExternalImage
-import net.mamoe.mirai.utils.currentTimeSeconds
-import net.mamoe.mirai.utils.getValue
-import net.mamoe.mirai.utils.unsafeWeakRef
+import net.mamoe.mirai.utils.*
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.contract
 import kotlin.coroutines.CoroutineContext
@@ -124,6 +122,9 @@ internal class MemberImpl constructor(
     @Suppress("PropertyName")
     var _muteTimestamp: Int = memberInfo.muteTimestamp
 
+    @Suppress("PropertyName")
+    var _nudgeTimestamp: Long = 0L
+
     override val muteTimeRemaining: Int
         get() = if (_muteTimestamp == 0 || _muteTimestamp == 0xFFFFFFFF.toInt()) {
             0
@@ -219,6 +220,27 @@ internal class MemberImpl constructor(
         net.mamoe.mirai.event.events.MemberUnmuteEvent(this@MemberImpl, null).broadcast()
     }
 
+    override suspend fun nudge(): Boolean {
+        if (bot.configuration.protocol != BotConfiguration.MiraiProtocol.ANDROID_PHONE) {
+            throw UnsupportedOperationException("nudge is supported only with protocol ANDROID_PHONE")
+        }
+        val coolDown = currentTimeMillis - _nudgeTimestamp;
+        check(coolDown > 10000L) {
+            "Cooling, Please wait $coolDown ms and try again"
+        }
+        bot.network.run {
+            return NudgePacket.troopInvoke(
+                client = bot.client,
+                groupCode = group.id,
+                targetUin = [email protected],
+            ).sendAndExpect<NudgePacket.Response>().success.also { success ->
+                if (success) {
+                    _nudgeTimestamp = currentTimeMillis
+                }
+            }
+        }
+    }
+
 
     @JvmSynthetic
     override suspend fun kick(message: String) {

+ 14 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/OIDB.kt

@@ -2422,3 +2422,17 @@ internal class Cmd0x6ce : ProtoBuf {
     ) : ProtoBuf
 }
 
+@Serializable
+internal class Cmd0xed3 : ProtoBuf {
+    @Serializable
+    internal class RspBody : ProtoBuf
+
+    @Serializable
+    internal class ReqBody(
+        @ProtoNumber(1) @JvmField val toUin: Long = 0L,
+        @ProtoNumber(2) @JvmField val groupCode: Long = 0L,
+        @ProtoNumber(3) @JvmField val msgSeq: Int = 0,
+        @ProtoNumber(4) @JvmField val msgRandom: Int = 0,
+        @ProtoNumber(5) @JvmField val aioUin: Long = 0L
+    ) : ProtoBuf
+}

+ 2 - 4
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt

@@ -13,10 +13,7 @@ import kotlinx.io.core.*
 import net.mamoe.mirai.event.Event
 import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.network.Packet
-import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.MultiMsg
-import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.NewContact
-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.*
 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.receive.*
@@ -151,6 +148,7 @@ internal object KnownPacketFactories {
         //  TroopManagement.GetGroupInfo,
         TroopManagement.EditGroupNametag,
         TroopManagement.Kick,
+        NudgePacket,
         Heartbeat.Alive,
         PbMessageSvc.PbMsgWithDraw,
         MultiMsg.ApplyUp,

+ 79 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/NudgePacket.kt

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2019-2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via 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 kotlinx.io.core.readBytes
+import net.mamoe.mirai.qqandroid.QQAndroidBot
+import net.mamoe.mirai.qqandroid.network.Packet
+import net.mamoe.mirai.qqandroid.network.QQAndroidClient
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0xed3
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.OidbSso
+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
+import net.mamoe.mirai.qqandroid.utils.io.serialization.loadAs
+import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
+import net.mamoe.mirai.qqandroid.utils.io.serialization.writeProtoBuf
+
+internal object NudgePacket : OutgoingPacketFactory<NudgePacket.Response>("OidbSvc.0xed3") {
+    override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
+        with(readBytes().loadAs(OidbSso.OIDBSSOPkg.serializer())) {
+            return Response(result == 0, result);
+        }
+    }
+
+    class Response(val success: Boolean, val code: Int) : Packet {
+        override fun toString(): String = "NudgeResponse(success=$success,code=$code)"
+    }
+
+    fun friendInvoke(
+        client: QQAndroidClient,
+        targetUin: Long,
+        chatTargetUin: Long,
+    ): OutgoingPacket {
+        return buildOutgoingUniPacket(client) {
+            writeProtoBuf(
+                OidbSso.OIDBSSOPkg.serializer(),
+                OidbSso.OIDBSSOPkg(
+                    command = 3795,
+                    serviceType = 1,
+                    result = 0,
+                    bodybuffer = Cmd0xed3.ReqBody(
+                        toUin = targetUin,
+                        aioUin = chatTargetUin
+                    ).toByteArray(Cmd0xed3.ReqBody.serializer())
+                )
+            )
+        }
+    }
+
+    fun troopInvoke(
+        client: QQAndroidClient,
+        groupCode: Long,
+        targetUin: Long,
+    ): OutgoingPacket {
+        return buildOutgoingUniPacket(client) {
+            writeProtoBuf(
+                OidbSso.OIDBSSOPkg.serializer(),
+                OidbSso.OIDBSSOPkg(
+                    command = 3795,
+                    serviceType = 1,
+                    result = 0,
+                    bodybuffer = Cmd0xed3.ReqBody(
+                        toUin = targetUin,
+                        groupCode = groupCode
+                    ).toByteArray(Cmd0xed3.ReqBody.serializer())
+                )
+            )
+        }
+    }
+
+}

+ 91 - 7
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.ReqPush.kt

@@ -23,6 +23,8 @@ import kotlinx.io.core.readUInt
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.protobuf.ProtoNumber
 import net.mamoe.mirai.JavaFriendlyAPI
+import net.mamoe.mirai.contact.Friend
+import net.mamoe.mirai.contact.Member
 import net.mamoe.mirai.data.FriendInfo
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.getFriendOrNull
@@ -37,8 +39,9 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.jce.MsgType0x210
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.OnlinePushPack
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0x115
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0x122
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0x27.SubMsgType0x27.*
-import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0x44
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0x44.Submsgtype0x44
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0xb3
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.TroopTips0x857
 import net.mamoe.mirai.qqandroid.network.protocol.packet.IncomingPacketFactory
@@ -101,7 +104,7 @@ internal object OnlinePushReqPush : IncomingPacketFactory<OnlinePushReqPush.ReqP
                 528 -> {
                     val notifyMsgBody = readJceStruct(MsgType0x210.serializer())
                     Transformers528[notifyMsgBody.uSubMsgType]
-                        ?.let { processor -> processor(notifyMsgBody, bot) }
+                        ?.let { processor -> processor(notifyMsgBody, bot, msgInfo) }
                         ?: kotlin.run {
                             bot.network.logger.debug {
                                 "unknown group 528 type 0x${notifyMsgBody.uSubMsgType.toUHexString("")}, data: " + notifyMsgBody.vProtobuf.toUHexString()
@@ -231,6 +234,41 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
         return@lambda732 sequenceOf(GroupAllowAnonymousChatEvent(!new, new, group, operator))
     },
 
+    //系统提示
+    0x14 to lambda732 { group: GroupImpl, bot: QQAndroidBot ->
+
+        discardExact(1)
+        val grayTip = readProtoBuf(TroopTips0x857.NotifyMsgBody.serializer()).optGeneralGrayTip
+        when (grayTip?.templId) {
+            //戳一戳
+            10043L, 1134L, 1135L -> {
+                //预置数据,服务器将不会提供己方已知消息
+                var action = ""
+                var from: Member = group.botAsMember
+                var target: Member = group.botAsMember
+                var suffix = ""
+                grayTip.msgTemplParam?.map {
+                    Pair(it.name.decodeToString(), it.value.decodeToString())
+                }?.asSequence()?.forEach { (key, value) ->
+                    run {
+                        when (key) {
+                            "action_str" -> action = value
+                            "uin_str1" -> from = group[value.toLong()]
+                            "uin_str2" -> target = group[value.toLong()]
+                            "suffix_str" -> suffix = value
+                        }
+                    }
+                }
+                return@lambda732 sequenceOf(MemberNudgeEvent(from, action, target, suffix))
+            }
+            else -> {
+                bot.logger.debug {
+                    "Unknown Transformers528 0x14 template\ntemplId=${grayTip?.templId}\nPermList=${grayTip?.msgTemplParam?._miraiContentToString()}"
+                }
+                return@lambda732 emptySequence()
+            }
+        }
+    },
     // 传字符串信息
     0x10 to lambda732 { group: GroupImpl, bot: QQAndroidBot ->
         val dataBytes = readBytes(26)
@@ -329,17 +367,28 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
     }
 )
 
-internal val ignoredLambda528: Lambda528 = lambda528 { emptySequence() }
+internal val ignoredLambda528: Lambda528 = lambda528 { _, _ -> emptySequence() }
 
 internal interface Lambda528 {
-    operator fun invoke(msg: MsgType0x210, bot: QQAndroidBot): Sequence<Packet>
+    operator fun invoke(msg: MsgType0x210, bot: QQAndroidBot, msgInfo: MsgInfo): Sequence<Packet>
 }
 
[email protected]
 internal inline fun lambda528(crossinline block: MsgType0x210.(QQAndroidBot) -> Sequence<Packet>): Lambda528 {
     return object : Lambda528 {
-        override fun invoke(msg: MsgType0x210, bot: QQAndroidBot): Sequence<Packet> {
+        override fun invoke(msg: MsgType0x210, bot: QQAndroidBot, msgInfo: MsgInfo): Sequence<Packet> {
             return block(msg, bot)
         }
+
+    }
+}
+
+internal inline fun lambda528(crossinline block: MsgType0x210.(QQAndroidBot, MsgInfo) -> Sequence<Packet>): Lambda528 {
+    return object : Lambda528 {
+        override fun invoke(msg: MsgType0x210, bot: QQAndroidBot, msgInfo: MsgInfo): Sequence<Packet> {
+            return block(msg, bot, msgInfo)
+        }
+
     }
 }
 
@@ -403,7 +452,7 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
         bot.friends.delegate.addLast(new)
         return@lambda528 sequenceOf(FriendAddEvent(new))
     },
-    0xE2L to lambda528 {
+    0xE2L to lambda528 { _ ->
         // TODO: unknown. maybe messages.
         // 0A 35 08 00 10 A2 FF 8C F0 03 1A 1B E5 90 8C E6 84 8F E4 BD A0 E7 9A 84 E5 8A A0 E5 A5 BD E5 8F 8B E8 AF B7 E6 B1 82 22 0C E6 BD 9C E6 B1 9F E7 BE A4 E5 8F 8B 28 01
         // vProtobuf.loadAs(Msgtype0x210.serializer())
@@ -411,7 +460,7 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
         return@lambda528 emptySequence()
     },
     0x44L to lambda528 { bot ->
-        val msg = vProtobuf.loadAs(Submsgtype0x44.Submsgtype0x44.MsgBody.serializer())
+        val msg = vProtobuf.loadAs(Submsgtype0x44.MsgBody.serializer())
         when {
             msg.msgCleanCountMsg != null -> {
 
@@ -441,6 +490,41 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
             sequenceOf(BotLeaveEvent.Active(group))
         } else emptySequence()
     },
+    //戳一戳信息等
+    0x122L to lambda528 { bot, msgInfo ->
+        val body = vProtobuf.loadAs(Submsgtype0x122.Submsgtype0x122.MsgBody.serializer())
+        when (body.templId) {
+            //戳一戳
+            1134L, 1135L, 1136L, 10043L -> {
+                //预置数据,服务器将不会提供己方已知消息
+                val chatTarget: Friend = bot.getFriend(msgInfo.lFromUin)
+                var from: Friend = bot.selfQQ
+                var action = ""
+                var target: Friend = bot.selfQQ
+                var suffix = ""
+                body.msgTemplParam?.map {
+                    Pair(it.name.decodeToString(), it.value.decodeToString())
+                }?.asSequence()?.forEach { (key, value) ->
+                    run {
+                        when (key) {
+                            "action_str" -> action = value
+                            "uin_str1" -> from = bot.getFriend(value.toLong())
+                            "uin_str2" -> target = bot.getFriend(value.toLong())
+                            "suffix_str" -> suffix = value
+                        }
+                    }
+                }
+                return@lambda528 sequenceOf(FriendNudgeEvent(chatTarget, from, action, target, suffix))
+
+            }
+            else -> {
+                bot.logger.debug {
+                    "Unknown Transformers528 0x122L template\ntemplId=${body.templId}\nPermList=${body.msgTemplParam?._miraiContentToString()}"
+                }
+                return@lambda528 emptySequence()
+            }
+        }
+    },
     //好友输入状态
     0x115L to lambda528 { bot ->
         val body = vProtobuf.loadAs(Submsgtype0x115.SubMsgType0x115.MsgBody.serializer())

+ 37 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Friend.kt

@@ -12,16 +12,19 @@
 package net.mamoe.mirai.contact
 
 import kotlinx.coroutines.CoroutineScope
+import net.mamoe.kjbb.JvmBlockingBridge
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.event.events.EventCancelledException
 import net.mamoe.mirai.event.events.FriendMessagePostSendEvent
 import net.mamoe.mirai.event.events.FriendMessagePreSendEvent
+import net.mamoe.mirai.event.events.FriendNudgeEvent
 import net.mamoe.mirai.message.FriendMessageEvent
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.Message
 import net.mamoe.mirai.message.data.PlainText
 import net.mamoe.mirai.message.data.isContentEmpty
 import net.mamoe.mirai.message.recall
+import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
 import kotlin.jvm.JvmSynthetic
 
 /**
@@ -77,5 +80,39 @@ public abstract class Friend : User(), CoroutineScope {
         return sendMessage(PlainText(message))
     }
 
+    /**
+     * 发送戳一戳好友的消息,冷却时间为 10 秒。
+     * 如对方已禁用该功能,发送将会失败且不会抛出异常。
+     * 调用需要使用协议 [MiraiProtocol.ANDROID_PHONE]
+     *
+     *
+     * @see nudgeBot 戳一戳自己
+     * @see FriendNudgeEvent 好友戳一戳事件
+     *
+     * @throws IllegalStateException 当仍处于冷却状态时
+     * @throws UnsupportedOperationException 当未使用安卓协议时 ([MiraiProtocol.ANDROID_PHONE])
+     *
+     * @return 是否成功发送
+     */
+    @JvmBlockingBridge
+    public abstract suspend fun nudge(): Boolean
+
+    /**
+     * 发送戳一戳自己的消息,冷却时间为 10 秒。
+     * 如Bot已禁用该功能,发送将会失败且不会抛出异常。
+     * 调用需要使用协议 [MiraiProtocol.ANDROID_PHONE]
+     *
+     *
+     * @see nudge 戳一戳好友
+     * @see FriendNudgeEvent 好友戳一戳事件
+     *
+     * @throws IllegalStateException 当仍处于冷却状态时
+     * @throws UnsupportedOperationException 当未使用安卓协议时 ([MiraiProtocol.ANDROID_PHONE])
+     *
+     * @return 是否成功发送
+     */
+    @JvmBlockingBridge
+    public abstract suspend fun nudgeBot(): Boolean
+
     final override fun toString(): String = "Friend($id)"
 }

+ 17 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt

@@ -11,6 +11,7 @@
 
 package net.mamoe.mirai.contact
 
+import net.mamoe.kjbb.JvmBlockingBridge
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.JavaFriendlyAPI
 import net.mamoe.mirai.event.events.*
@@ -20,6 +21,7 @@ import net.mamoe.mirai.message.data.Message
 import net.mamoe.mirai.message.data.PlainText
 import net.mamoe.mirai.message.data.isContentEmpty
 import net.mamoe.mirai.message.recall
+import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
 import net.mamoe.mirai.utils.WeakRefProperty
 import kotlin.jvm.JvmSynthetic
 import kotlin.time.Duration
@@ -124,6 +126,21 @@ public abstract class Member : MemberJavaFriendlyAPI, User() {
     @JvmSynthetic
     public abstract suspend fun unmute()
 
+    /**
+     * 发送戳一戳该成员的消息,冷却时间为 10 秒。
+     * 如对方已禁用该功能,发送将会失败且不会抛出异常。
+     * 调用需要使用协议 [MiraiProtocol.ANDROID_PHONE]
+     *
+     * @see MemberNudgeEvent 成员戳一戳事件
+     *
+     * @throws IllegalStateException 当仍处于冷却状态时
+     * @throws UnsupportedOperationException 当未使用安卓协议时 ([MiraiProtocol.ANDROID_PHONE])
+     *
+     * @return 是否成功发送
+     */
+    @JvmBlockingBridge
+    public abstract suspend fun nudge(): Boolean
+
     /**
      * 踢出该成员.
      *

+ 25 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/friend.kt

@@ -112,7 +112,31 @@ public data class FriendAvatarChangedEvent internal constructor(
     public override val friend: Friend
 ) : FriendEvent, Packet, AbstractEvent()
 
-
+/**
+ * [Friend] 的戳一戳事件.
+ */
+public data class FriendNudgeEvent internal constructor(
+    /**
+     * 发起戳一戳的好友会话,此处使用 [Friend] 指定
+     */
+    public override val friend: Friend,
+    /**
+     * 戳一戳的发起者,可为 [Bot] 自身或好友
+     */
+    public val from: Friend,
+    /**
+     * 戳一戳的动作
+     */
+    public val action: String,
+    /**
+     * 戳一戳的目标,可为 [Bot] 自身或好友
+     */
+    public val target: Friend,
+    /**
+     * 戳一戳中设置的自定义后缀
+     */
+    public val suffix: String
+) : FriendEvent, Packet, AbstractEvent()
 
 /**
  * [Friend] 昵称改变事件, 在此事件广播时好友已经完成改名

+ 26 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/group.kt

@@ -510,4 +510,30 @@ public data class MemberUnmuteEvent internal constructor(
 
 // endregion
 
+// region 戳一戳
+/**
+ * 群成员戳一戳事件.
+ *
+ */
+public data class MemberNudgeEvent internal constructor(
+    /**
+     * 戳一戳的发起者,如果对象是 [Bot] 则为 [Bot] 的 [Member] 对象
+     */
+    public override val member: Member,
+    /**
+     * 戳一戳的动作
+     */
+    public val action: String,
+    /**
+     * 戳一戳的目标,如果对象是 [Bot] 则为 [Bot] 的 [Member] 对象
+     */
+    public val target: Member,
+    /**
+     * 戳一戳中设置的自定义后缀
+     */
+    public val suffix: String
+) : GroupMemberEvent, BotPassiveEvent, Packet, AbstractEvent()
+
+// endregion
+
 // endregion