Forráskód Böngészése

Add mute and unmute

Him188 6 éve
szülő
commit
417276acda

+ 4 - 2
UpdateLog.md

@@ -2,5 +2,7 @@
 
 ## Main version 0
 
-### 0.3.0
-- 更新
+### 0.6.0
+- 新增: 禁言群成员
+- 新增: 解禁群成员
+- 修复: ContactList key 无法匹配

+ 1 - 1
gradle.properties

@@ -1,7 +1,7 @@
 # style guide
 kotlin.code.style=official
 # config
-mirai_version=0.5.1
+mirai_version=0.6.0
 kotlin.incremental.multiplatform=true
 kotlin.parallel.tasks.in.project=true
 # kotlin

+ 31 - 2
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt

@@ -59,7 +59,7 @@ inline fun <R> Contact.withSession(block: BotSession.() -> R): R {
 /**
  * 只读联系人列表
  */
-class ContactList<C : Contact> @PublishedApi internal constructor(internal val delegate: MutableContactList<C>) : Map<UInt, C> by delegate {
+class ContactList<C : Contact> @PublishedApi internal constructor(internal val delegate: MutableContactList<C>) : Map<UInt, C> {
     /**
      * ID 列表的字符串表示.
      * 如:
@@ -70,12 +70,41 @@ class ContactList<C : Contact> @PublishedApi internal constructor(internal val d
     val idContentString: String get() = this.keys.joinToString(prefix = "[", postfix = "]") { it.toLong().toString() }
 
     override fun toString(): String = delegate.toString()
+
+
+    // TODO: 2019/12/2 应该使用属性代理, 但属性代理会导致 UInt 内联错误. 等待 kotlin 修复后替换
+
+    override val size: Int get() = delegate.size
+    override fun containsKey(key: UInt): Boolean = delegate.containsKey(key)
+    override fun containsValue(value: C): Boolean = delegate.containsValue(value)
+    override fun get(key: UInt): C? = delegate[key]
+    override fun isEmpty(): Boolean = delegate.isEmpty()
+    override val entries: MutableSet<MutableMap.MutableEntry<UInt, C>> get() = delegate.entries
+    override val keys: MutableSet<UInt> get() = delegate.keys
+    override val values: MutableCollection<C> get() = delegate.values
 }
 
 /**
  * 可修改联系人列表. 只会在内部使用.
  */
 @PublishedApi
-internal class MutableContactList<C : Contact> : MutableMap<UInt, C> by mutableMapOf() {
+internal class MutableContactList<C : Contact> : MutableMap<UInt, C> {
     override fun toString(): String = asIterable().joinToString(separator = ", ", prefix = "ContactList(", postfix = ")") { it.value.toString() }
+
+
+    // TODO: 2019/12/2 应该使用属性代理, 但属性代理会导致 UInt 内联错误. 等待 kotlin 修复后替换
+    private val delegate = mutableMapOf<UInt, C>()
+
+    override val size: Int get() = delegate.size
+    override fun containsKey(key: UInt): Boolean = delegate.containsKey(key)
+    override fun containsValue(value: C): Boolean = delegate.containsValue(value)
+    override fun get(key: UInt): C? = delegate[key]
+    override fun isEmpty(): Boolean = delegate.isEmpty()
+    override val entries: MutableSet<MutableMap.MutableEntry<UInt, C>> get() = delegate.entries
+    override val keys: MutableSet<UInt> get() = delegate.keys
+    override val values: MutableCollection<C> get() = delegate.values
+    override fun clear() = delegate.clear()
+    override fun put(key: UInt, value: C): C? = delegate.put(key, value)
+    override fun putAll(from: Map<out UInt, C>) = delegate.putAll(from)
+    override fun remove(key: UInt): C? = delegate.remove(key)
 }

+ 1 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt

@@ -51,7 +51,7 @@ interface Group : Contact, Iterable<Member> {
     /**
      * 获取群成员. 若此 ID 的成员不存在, 则会抛出 [kotlin.NoSuchElementException]
      */
-    suspend fun getMember(id: UInt): Member
+    fun getMember(id: UInt): Member
 
     /**
      * 更新群资料. 群资料会与服务器事件同步事件更新, 一般情况下不需要手动更新.

+ 44 - 2
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt

@@ -1,9 +1,14 @@
+@file:Suppress("unused")
+
 package net.mamoe.mirai.contact
 
+import com.soywiz.klock.MonthSpan
+import com.soywiz.klock.TimeSpan
+import kotlin.time.Duration
+import kotlin.time.ExperimentalTime
+
 /**
  * 群成员.
- *
- * 使用 [QQ.equals]. 因此同 ID 的群成员和 QQ 是 `==` 的
  */
 interface Member : QQ, Contact {
     /**
@@ -15,8 +20,45 @@ interface Member : QQ, Contact {
      * 权限
      */
     val permission: MemberPermission
+
+    /**
+     * 禁言
+     *
+     * @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
+     * @return 若机器人无权限禁言这个群成员, 返回 `false`
+     */
+    suspend fun mute(durationSeconds: Int): Boolean
+
+    /**
+     * 解除禁言
+     */
+    suspend fun unmute()
 }
 
+@ExperimentalTime
+suspend inline fun Member.mute(duration: Duration) {
+    require(duration.inDays > 30) { "duration must be at most 1 month" }
+    require(duration.inSeconds > 0) { "duration must be greater than 0 second" }
+    this.mute(duration.inSeconds.toInt())
+}
+
+suspend inline fun Member.mute(duration: TimeSpan) {
+    require(duration.days > 30) { "duration must be at most 1 month" }
+    require(duration.microseconds > 0) { "duration must be greater than 0 second" }
+    this.mute(duration.seconds.toInt())
+}
+
+suspend inline fun Member.mute(duration: MonthSpan) {
+    require(duration.totalMonths == 1) { "if you pass a MonthSpan, it must be 1 month" }
+    this.mute(duration.totalMonths * 30 * 24 * 3600)
+}
+
+@ExperimentalUnsignedTypes
+suspend inline fun Member.mute(durationSeconds: UInt) {
+    require(durationSeconds.toInt() <= 30 * 24 * 3600) { "duration must be at most 1 month" }
+    this.mute(durationSeconds.toInt())
+} // same bin rep.
+
 /**
  * 群成员的权限
  */

+ 26 - 4
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/internal/ContactImpl.kt

@@ -1,4 +1,4 @@
-@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "MemberVisibilityCanBePrivate")
+@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "MemberVisibilityCanBePrivate", "EXPERIMENTAL_UNSIGNED_LITERALS")
 
 package net.mamoe.mirai.contact.internal
 
@@ -19,6 +19,7 @@ import net.mamoe.mirai.network.sessionKey
 import net.mamoe.mirai.qqAccount
 import net.mamoe.mirai.sendPacket
 import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.io.logStacktrace
 import net.mamoe.mirai.withSession
 import kotlin.coroutines.CoroutineContext
 
@@ -45,6 +46,7 @@ internal suspend fun Group(bot: Bot, groupId: GroupId, context: CoroutineContext
     val info: RawGroupInfo = try {
         bot.withSession { GroupPacket.QueryGroupInfo(qqAccount, groupId.toInternalId(), sessionKey).sendAndExpect() }
     } catch (e: Exception) {
+        e.logStacktrace()
         error("Cannot obtain group info for id ${groupId.value}")
     }
     return GroupImpl(bot, groupId, context).apply { this.info = info.parseBy(this) }
@@ -62,9 +64,9 @@ internal data class GroupImpl internal constructor(override val bot: Bot, val gr
     override val announcement: String get() = info.announcement
     override val members: ContactList<Member> get() = info.members
 
-    override suspend fun getMember(id: UInt): Member =
+    override fun getMember(id: UInt): Member =
         if (members.containsKey(id)) members[id]!!
-        else throw NoSuchElementException("No such member whose id is $id in group ${groupId.value.toLong()}")
+        else throw NoSuchElementException("No such member whose id is ${id.toLong()} in group ${groupId.value.toLong()}")
 
     override suspend fun sendMessage(message: MessageChain) {
         bot.sendPacket(GroupPacket.Message(bot.qqAccount, internalId, bot.sessionKey, message))
@@ -124,5 +126,25 @@ internal data class QQImpl internal constructor(override val bot: Bot, override
  */
 @PublishedApi
 internal data class MemberImpl(private val delegate: QQ, override val group: Group, override val permission: MemberPermission) : QQ by delegate, Member {
-    override fun toString(): String = "Member(id=${this.id}, permission=$permission)"
+    override fun toString(): String = "Member(id=${this.id}, group=${group.id}, permission=$permission)"
+
+    override suspend fun mute(durationSeconds: Int): Boolean = bot.withSession {
+        require(durationSeconds > 0) { "duration must be greater than 0 second" }
+
+        if (permission == MemberPermission.OWNER) return false
+
+        when (group.getMember(bot.qqAccount).permission) {
+            MemberPermission.MEMBER -> return false
+            MemberPermission.OPERATOR -> if (permission == MemberPermission.OPERATOR) return false
+            MemberPermission.OWNER -> {
+            }
+        }
+
+        GroupPacket.Mute(qqAccount, group.internalId, sessionKey, id, durationSeconds.toUInt()).sendAndExpect<GroupPacket.MuteResponse>()
+        return true
+    }
+
+    override suspend fun unmute(): Unit = bot.withSession {
+        GroupPacket.Mute(qqAccount, group.internalId, sessionKey, id, 0u).sendAndExpect<GroupPacket.MuteResponse>()
+    }
 }

+ 5 - 2
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/internal/InternalEventListeners.kt

@@ -9,6 +9,7 @@ import net.mamoe.mirai.event.ListeningStatus
 import net.mamoe.mirai.event.Subscribable
 import net.mamoe.mirai.event.events.BotEvent
 import net.mamoe.mirai.utils.internal.inlinedRemoveIf
+import net.mamoe.mirai.utils.io.logStacktrace
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.coroutineContext
 import kotlin.jvm.JvmField
@@ -98,7 +99,8 @@ internal class Handler<in E : Subscribable>
         return try {
             withContext(context) { handler.invoke(event) }.also { if (it == ListeningStatus.STOPPED) this.complete() }
         } catch (e: Throwable) {
-            this.completeExceptionally(e)
+            e.logStacktrace()
+            //this.completeExceptionally(e)
             ListeningStatus.STOPPED
         }
     }
@@ -131,7 +133,8 @@ internal class HandlerWithBot<E : Subscribable> @PublishedApi internal construct
         return try {
             withContext(context) { bot.handler(event) }.also { if (it == ListeningStatus.STOPPED) complete() }
         } catch (e: Throwable) {
-            completeExceptionally(e)
+            e.logStacktrace()
+            //completeExceptionally(e)
             ListeningStatus.STOPPED
         }
     }

+ 32 - 2
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupPacket.kt

@@ -50,7 +50,8 @@ data class RawGroupInfo(
             MemberImpl([email protected](), group, MemberPermission.OWNER),
             [email protected],
             [email protected],
-            ContactList([email protected](MutableContactList()) { MemberImpl(it.key.qq(), group, MemberPermission.OWNER) })
+            ContactList([email protected](MutableContactList<Member>()) { MemberImpl(it.key.qq(), group, it.value) }
+                .apply { put(owner, MemberImpl(owner.qq(), group, MemberPermission.OWNER)) })
         )
     }
 }
@@ -119,6 +120,29 @@ object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketResponse>() {
         writeZero(4)
     }
 
+    /**
+     * 禁言群成员
+     */
+    @PacketVersion(date = "2019.12.2", timVersion = "2.3.2 (21173)")
+    fun Mute(
+        bot: UInt,
+        groupInternalId: GroupInternalId,
+        sessionKey: SessionKey,
+        target: UInt,
+        /**
+         * 0 为取消
+         */
+        timeSeconds: UInt
+    ): OutgoingPacket = buildSessionPacket(bot, sessionKey, name = "MuteMember") {
+        writeUByte(0x7Eu)
+        writeGroup(groupInternalId)
+        writeByte(0x20)
+        writeByte(0x00)
+        writeByte(0x01)
+        writeQQ(target)
+        writeUInt(timeSeconds)
+    }
+
     interface GroupPacketResponse : Packet
 
     @NoLog
@@ -126,11 +150,17 @@ object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketResponse>() {
         override fun toString(): String = "GroupPacket.MessageResponse"
     }
 
+    @NoLog
+    object MuteResponse : Packet, GroupPacketResponse {
+        override fun toString(): String = "GroupPacket.MuteResponse"
+    }
+
     @PacketVersion(date = "2019.11.27", timVersion = "2.3.2 (21173)")
     @UseExperimental(ExperimentalStdlibApi::class)
-    override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): GroupPacketResponse = handler.bot.withSession {
+    override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): GroupPacketResponse {
         return when (readUByte().toUInt()) {
             0x2Au -> MessageResponse
+            0x7Eu -> MuteResponse // 成功: 7E 00 22 96 29 7B;
 
             0x09u -> {
                 if (readByte().toInt() == 0) {

+ 2 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/event/EventPacketFactory.kt

@@ -97,6 +97,7 @@ abstract class KnownEventParserAndHandler<TPacket : Packet>(override val id: USh
         FriendAddRequestEventPacket,
         MemberGoneEventPacketHandler,
         ConnectionOccupiedPacketHandler,
-        MemberJoinPacketHandler
+        MemberJoinPacketHandler,
+        MemberMuteEventPacketParserAndHandler
     )
 }

+ 136 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/event/MemberMute.kt

@@ -0,0 +1,136 @@
+@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
+
+package net.mamoe.mirai.network.protocol.tim.packet.event
+
+import com.soywiz.klock.TimeSpan
+import com.soywiz.klock.seconds
+import com.soywiz.klock.toTimeString
+import kotlinx.io.core.ByteReadPacket
+import kotlinx.io.core.discardExact
+import kotlinx.io.core.readUInt
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.Member
+import net.mamoe.mirai.getGroup
+import net.mamoe.mirai.qqAccount
+
+// region mute
+/**
+ * 某群成员被禁言事件
+ */
+@Suppress("unused")
+class MemberMuteEvent(
+    val member: Member,
+    override val duration: TimeSpan,
+    override val operator: Member
+) : MuteEvent() {
+    override val group: Group get() = operator.group
+    override fun toString(): String = "MemberMuteEvent(member=${member.id}, group=${group.id}, operator=${operator.id}, duration=${duration.toTimeString()}"
+}
+
+/**
+ * 机器人被禁言事件
+ */
+class BeingMutedEvent(
+    override val duration: TimeSpan,
+    override val operator: Member
+) : MuteEvent() {
+    override val group: Group get() = operator.group
+    override fun toString(): String = "BeingMutedEvent(group=${group.id}, operator=${operator.id}, duration=${duration.toTimeString()}"
+}
+
+sealed class MuteEvent : EventOfMute() {
+    abstract override val operator: Member
+    abstract override val group: Group
+    abstract val duration: TimeSpan
+}
+// endregion
+
+// region unmute
+/**
+ * 某群成员被解除禁言事件
+ */
+@Suppress("unused")
+class MemberUnmuteEvent(
+    val member: Member,
+    override val operator: Member
+) : UnmuteEvent() {
+    override val group: Group get() = operator.group
+    override fun toString(): String = "MemberUnmuteEvent(member=${member.id}, group=${group.id}, operator=${operator.id}"
+}
+
+/**
+ * 机器人被解除禁言事件
+ */
+class BeingUnmutedEvent(
+    override val operator: Member
+) : UnmuteEvent() {
+    override val group: Group get() = operator.group
+    override fun toString(): String = "BeingUnmutedEvent(group=${group.id}, operator=${operator.id}"
+}
+
+sealed class UnmuteEvent : EventOfMute() {
+    abstract override val operator: Member
+    abstract override val group: Group
+}
+
+// endregion
+
+sealed class EventOfMute : EventPacket {
+    abstract val operator: Member
+    abstract val group: Group
+}
+
+internal object MemberMuteEventPacketParserAndHandler : KnownEventParserAndHandler<EventOfMute>(0x02DCu) {
+    override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): EventOfMute {
+        //取消
+        //00 00 00 11 00 0A 00 04 01 00 00 00 00 0C 00 05 00 01 00
+        // 01 01
+        // 22 96 29 7B
+        // 0C 01
+        // 3E 03 3F A2
+        // 5D E5 12 EB
+        // 00 01
+        // 76 E4 B8 DD
+        // 00 00 00 00
+
+        // 禁言
+        //00 00 00 11 00 0A 00 04 01 00 00 00 00 0C 00 05 00 01 00
+        // 01
+        // 01
+        // 22 96 29 7B
+        // 0C
+        // 01
+        // 3E 03 3F A2
+        // 5D E5 07 85
+        // 00
+        // 01
+        // 76 E4 B8 DD
+        // 00 27 8D 00
+        discardExact(19)
+        discardExact(2)
+        val group = bot.getGroup(readUInt())
+        discardExact(2)
+        val operator = group.getMember(readUInt())
+        discardExact(4) //time
+        discardExact(2)
+        val memberQQ = readUInt()
+
+        val durationSeconds = readUInt().toInt()
+        return if (durationSeconds == 0) {
+            if (memberQQ == bot.qqAccount) {
+                BeingUnmutedEvent(operator)
+            } else {
+                MemberUnmuteEvent(group.getMember(memberQQ), operator)
+            }
+        } else {
+            val duration = durationSeconds.seconds
+
+            if (memberQQ == bot.qqAccount) {
+                BeingMutedEvent(duration, operator)
+            } else {
+                MemberMuteEvent(group.getMember(memberQQ), duration, operator)
+            }
+        }
+    }
+}

+ 7 - 4
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/event/Message.kt

@@ -7,10 +7,7 @@ import kotlinx.io.core.String
 import kotlinx.io.core.discardExact
 import kotlinx.io.core.readUInt
 import net.mamoe.mirai.Bot
-import net.mamoe.mirai.contact.Contact
-import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.contact.MemberPermission
-import net.mamoe.mirai.contact.QQ
+import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.event.BroadcastControllable
 import net.mamoe.mirai.event.events.BotEvent
 import net.mamoe.mirai.getGroup
@@ -98,6 +95,7 @@ abstract class MessagePacketBase<TSubject : Contact> : EventPacket, BotEvent() {
 
 // region group message
 
+@Suppress("unused")
 data class GroupMessage(
     val group: Group,
     val senderName: String,
@@ -110,6 +108,11 @@ data class GroupMessage(
 ) : MessagePacket<Group>() {
 
     override val subject: Group get() = group
+
+
+    suspend inline fun At.member(): Member = group.getMember(this.target)
+    suspend inline fun UInt.member(): Member = group.getMember(this)
+    suspend inline fun Long.member(): Member = group.getMember(this.toUInt())
 }
 
 @PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")

+ 2 - 2
mirai-debug/src/main/kotlin/HexDebuggerGui.kt

@@ -22,7 +22,7 @@ import java.awt.datatransfer.DataFlavor
 /**
  * How to run:
  *
- * `gradle run`
+ * `gradle :mirai-debug:run`
  */
 class Application : App(HexDebuggerGui::class, Styles::class)
 
@@ -179,7 +179,7 @@ class HexDebuggerGui : View("s") {
 
     override val root = hbox {
         //prefWidth = 735.0
-        minHeight = 240.0
+        minHeight = 300.0
         prefHeight = minHeight
 
         input = textarea {

+ 5 - 3
mirai-debug/src/main/kotlin/PacketDebuger.kt

@@ -116,8 +116,8 @@ object PacketDebugger {
      * 7. 运行完 `mov eax,dword ptr ss:[ebp+10]`
      * 8. 查看内存, `eax` 到 `eax+10` 的 16 字节就是 `sessionKey`
      */
-    val sessionKey: SessionKey = SessionKey("F7 3C 31 B5 E1 F1 E5 6A FA F7 95 79 AE 19 30 01".hexToBytes())
-    const val qq: UInt = 761025446u
+    val sessionKey: SessionKey = SessionKey("06 23 F8 09 0D 2D 37 BE 2E FE 90 3A 7D E5 8F B1".hexToBytes())
+    const val qq: UInt = 1040400290u
 
     val IgnoredPacketIdList: List<PacketId> = listOf(
         KnownPacketId.FRIEND_ONLINE_STATUS_CHANGE,
@@ -152,7 +152,9 @@ object PacketDebugger {
                                 decodedBody = it.readBytes()
                                 ByteReadPacket(decodedBody)
                             }
-                            .decode(id, sequenceId, DebugNetworkHandler)
+                            .runCatching {
+                                decode(id, sequenceId, DebugNetworkHandler)
+                            }.getOrElse { it.printStackTrace(); null }
                     }
                 }
 

+ 25 - 6
mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt

@@ -2,6 +2,8 @@
 
 package demo.gentleman
 
+import com.soywiz.klock.months
+import com.soywiz.klock.seconds
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.delay
@@ -9,8 +11,10 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import net.mamoe.mirai.*
 import net.mamoe.mirai.contact.MemberPermission
+import net.mamoe.mirai.contact.mute
 import net.mamoe.mirai.event.Subscribable
 import net.mamoe.mirai.event.subscribeAlways
+import net.mamoe.mirai.event.subscribeGroupMessages
 import net.mamoe.mirai.event.subscribeMessages
 import net.mamoe.mirai.message.At
 import net.mamoe.mirai.message.Image
@@ -59,17 +63,32 @@ suspend fun main() {
         it.approve()
     }
 
+    bot.subscribeGroupMessages {
+        "群资料" reply {
+            group.updateGroupInfo().toString().reply()
+        }
+
+        startsWith("mt2months") {
+            val at: At by message
+            at.target.member().mute(1.months)
+        }
+
+        startsWith("mute") {
+            val at: At by message
+            at.target.member().mute(30.seconds)
+        }
+
+        startsWith("unmute") {
+            val at: At by message
+            at.target.member().unmute()
+        }
+    }
+
     bot.subscribeMessages {
         case("at me") { At(sender).reply() }
 
         "你好" reply "你好!"
 
-        "群资料" reply {
-            if (this is GroupMessage) {
-                group.updateGroupInfo().toString().reply()
-            }
-        }
-
         startsWith("profile", removePrefix = true) {
             val account = it.trim()
             if (account.isNotEmpty()) {