Commit 417276ac authored by Him188's avatar Him188

Add mute and unmute

parent 8fa98f59
......@@ -2,5 +2,7 @@
## Main version 0
### 0.3.0
- 更新
\ No newline at end of file
### 0.6.0
- 新增: 禁言群成员
- 新增: 解禁群成员
- 修复: ContactList key 无法匹配
\ No newline at end of file
# 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
......
......@@ -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)
}
\ No newline at end of file
......@@ -51,7 +51,7 @@ interface Group : Contact, Iterable<Member> {
/**
* 获取群成员. 若此 ID 的成员不存在, 则会抛出 [kotlin.NoSuchElementException]
*/
suspend fun getMember(id: UInt): Member
fun getMember(id: UInt): Member
/**
* 更新群资料. 群资料会与服务器事件同步事件更新, 一般情况下不需要手动更新.
......
@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.
/**
* 群成员的权限
*/
......
@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>()
}
}
\ No newline at end of file
......@@ -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
}
}
......
......@@ -50,7 +50,8 @@ data class RawGroupInfo(
MemberImpl(this@RawGroupInfo.owner.qq(), group, MemberPermission.OWNER),
this@RawGroupInfo.name,
this@RawGroupInfo.announcement,
ContactList(this@RawGroupInfo.members.mapValuesTo(MutableContactList()) { MemberImpl(it.key.qq(), group, MemberPermission.OWNER) })
ContactList(this@RawGroupInfo.members.mapValuesTo(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) {
......
......@@ -97,6 +97,7 @@ abstract class KnownEventParserAndHandler<TPacket : Packet>(override val id: USh
FriendAddRequestEventPacket,
MemberGoneEventPacketHandler,
ConnectionOccupiedPacketHandler,
MemberJoinPacketHandler
MemberJoinPacketHandler,
MemberMuteEventPacketParserAndHandler
)
}
\ No newline at end of file
@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)
}
}
}
}
\ No newline at end of file
......@@ -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)")
......
......@@ -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 {
......
......@@ -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 }
}
}
......
......@@ -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()) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment