diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt index 159082173..ada894249 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt @@ -166,7 +166,7 @@ internal class QQImpl( } -@Suppress("MemberVisibilityCanBePrivate", "DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE") +@Suppress("MemberVisibilityCanBePrivate") internal class MemberImpl( qq: QQImpl, group: GroupImpl, @@ -286,6 +286,20 @@ internal class MemberImpl( } } + override fun hashCode(): Int { + var result = bot.hashCode() + result = 31 * result + id.hashCode() + return result + } + + @Suppress("DuplicatedCode") + override fun equals(other: Any?): Boolean { // 不要删除. trust me + if (this === other) return true + if (other !is Contact) return false + if (this::class != other::class) return false + return this.id == other.id && this.bot == other.bot + } + override fun toString(): String { return "Member($id)" } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt index 57bddbfdb..9d415b113 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt @@ -9,7 +9,7 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive -import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.discardExact @@ -19,11 +19,10 @@ 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.event.subscribingGetAsync import net.mamoe.mirai.message.FriendMessage import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageSource @@ -279,17 +278,14 @@ internal class MessageSvc { override val groupId: Long, override val sourceMessage: MessageChain ) : MessageSource { - lateinit var sequenceIdDeferred: CompletableDeferred + lateinit var sequenceIdDeferred: Deferred + @UseExperimental(MiraiExperimentalAPI::class) fun startWaitingSequenceId(contact: Contact) { - sequenceIdDeferred = CompletableDeferred() - contact.subscribe { event -> - if (event.messageRandom == messageUid.toInt()) { - sequenceIdDeferred.complete(event.sequenceId) - return@subscribe ListeningStatus.STOPPED - } - - return@subscribe ListeningStatus.LISTENING + sequenceIdDeferred = contact.subscribingGetAsync { + if (it.messageRandom == messageUid.toInt()) { + it.sequenceId + } else null } } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt index 74f0a1431..bce7e10b7 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt @@ -72,6 +72,9 @@ interface Member : QQ, Contact { /** * 禁言. * + * QQ 中最小操作和显示的时间都是一分钟. + * 机器人可以实现精确到秒, 会被客户端显示为 1 分钟但不影响实际禁言时间. + * * 管理员可禁言成员, 群主可禁言管理员和群员. * * @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常. diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/linear.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/linear.kt index 115c95c3e..44b1b5e85 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/linear.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/linear.kt @@ -9,3 +9,104 @@ package net.mamoe.mirai.event +import kotlinx.coroutines.* +import net.mamoe.mirai.utils.MiraiExperimentalAPI +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * 挂起当前协程, 监听这个事件, 并尝试从这个事件中获取一个值. + * + * 若 [filter] 抛出了一个异常, 本函数会立即抛出这个异常. + * + * @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制 + * @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值 + * + * @see subscribingGetAsync 本函数的异步版本 + */ +@MiraiExperimentalAPI +suspend inline fun subscribingGet( + timeoutMillis: Long = -1, + noinline filter: E.(E) -> R? // 不要 crossinline: crossinline 后 stacktrace 会不正常 +): R { + require(timeoutMillis == -1L || timeoutMillis > 0) { "timeoutMillis must be -1 or > 0" } + return subscribingGetOrNull(timeoutMillis, filter) ?: error("timeout subscribingGet") +} + +/** + * 挂起当前协程, 监听这个事件, 并尝试从这个事件中获取一个值. + * + * 若 [filter] 抛出了一个异常, 本函数会立即抛出这个异常. + * + * @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制 + * @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值 + * + * @see subscribingGetAsync 本函数的异步版本 + */ +@MiraiExperimentalAPI +suspend inline fun subscribingGetOrNull( + timeoutMillis: Long = -1, + noinline filter: E.(E) -> R? // 不要 crossinline: crossinline 后 stacktrace 会不正常 +): R? { + require(timeoutMillis == -1L || timeoutMillis > 0) { "timeoutMillis must be -1 or > 0" } + var result: R? = null + var resultThrowable: Throwable? = null + + if (timeoutMillis == -1L) { + @Suppress("DuplicatedCode") // for better performance + coroutineScope { + var listener: Listener? = null + listener = this.subscribe { + val value = try { + filter.invoke(this, it) + } catch (e: Exception) { + resultThrowable = e + return@subscribe ListeningStatus.STOPPED.also { listener!!.complete() } + } + + if (value != null) { + result = value + return@subscribe ListeningStatus.STOPPED.also { listener!!.complete() } + } else return@subscribe ListeningStatus.LISTENING + } + } + } else { + withTimeoutOrNull(timeoutMillis) { + var listener: Listener? = null + @Suppress("DuplicatedCode") // for better performance + listener = this.subscribe { + val value = try { + filter.invoke(this, it) + } catch (e: Exception) { + resultThrowable = e + return@subscribe ListeningStatus.STOPPED.also { listener!!.complete() } + } + + if (value != null) { + result = value + return@subscribe ListeningStatus.STOPPED.also { listener!!.complete() } + } else return@subscribe ListeningStatus.LISTENING + } + } + } + resultThrowable?.let { throw it } + return result +} + +/** + * 异步监听这个事件, 并尝试从这个事件中获取一个值. + * + * 若 [filter] 抛出了一个异常, [Deferred.await] 会抛出这个异常或. + * + * @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制 + * @param coroutineContext 额外的 [CoroutineContext] + * @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值 + */ +@MiraiExperimentalAPI +inline fun CoroutineScope.subscribingGetAsync( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + timeoutMillis: Long = -1, + noinline filter: E.(E) -> R? // 不要 crossinline: crossinline 后 stacktrace 会不正常 +): Deferred = this.async(coroutineContext) { + subscribingGet(timeoutMillis, filter) +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/subscriber.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/subscriber.kt index eeca3a407..9547437fd 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/subscriber.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/subscriber.kt @@ -101,6 +101,9 @@ interface Listener : CompletableJob { * * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext] * + * @see subscribingGet 监听一个事件, 并尝试从这个事件中获取一个值. + * @see subscribingGetAsync 异步监听一个事件, 并尝试从这个事件中获取一个值. + * * @see subscribeAlways 一直监听 * @see subscribeOnce 只监听一次 * diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/GroupMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/GroupMessage.kt index 6f2b62a97..f14d0689e 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/GroupMessage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/GroupMessage.kt @@ -9,6 +9,7 @@ package net.mamoe.mirai.message +import kotlinx.coroutines.Job import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.* import net.mamoe.mirai.event.Event @@ -44,25 +45,25 @@ class GroupMessage( * 对于好友消息事件, 这个方法将会给好友 ([subject]) 发送消息 * 对于群消息事件, 这个方法将会给群 ([subject]) 发送消息 */ - suspend inline fun quoteReply(message: MessageChain) = reply(this.message.quote() + message) + suspend inline fun quoteReply(message: MessageChain): MessageReceipt = reply(this.message.quote() + message) - suspend inline fun quoteReply(message: Message) = reply(this.message.quote() + message) - suspend inline fun quoteReply(plain: String) = reply(this.message.quote() + plain) + suspend inline fun quoteReply(message: Message): MessageReceipt = reply(this.message.quote() + message) + suspend inline fun quoteReply(plain: String): MessageReceipt = reply(this.message.quote() + plain) @JvmName("reply2") - suspend inline fun String.quoteReply() = quoteReply(this) + suspend inline fun String.quoteReply(): MessageReceipt = quoteReply(this) @JvmName("reply2") - suspend inline fun Message.quoteReply() = quoteReply(this) + suspend inline fun Message.quoteReply(): MessageReceipt = quoteReply(this) @JvmName("reply2") - suspend inline fun MessageChain.quoteReply() = quoteReply(this) + suspend inline fun MessageChain.quoteReply(): MessageReceipt = 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) + inline fun MessageSource.recallIn(delay: Long): Job = group.recallIn(this, delay) + inline fun MessageChain.recallIn(delay: Long): Job = group.recallIn(this, delay) override fun toString(): String = "GroupMessage(group=${group.id}, senderName=$senderName, sender=${sender.id}, permission=${permission.name}, message=$message)" diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt index 5ad3974e6..b8c3eb8fc 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt @@ -21,6 +21,8 @@ import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.data.Packet import net.mamoe.mirai.event.events.BotEvent +import net.mamoe.mirai.event.subscribingGet +import net.mamoe.mirai.event.subscribingGetAsync import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.* import kotlin.jvm.JvmName @@ -109,6 +111,10 @@ abstract class MessagePacketBase(_bot: Bot) : suspend inline fun String.send() = this.toMessage().sendTo(subject) // endregion + operator fun get(at: Message.Key): M { + return this.message[at] + } + /** * 创建 @ 这个账号的消息. 当且仅当消息为群消息时可用. 否则将会抛出 [IllegalArgumentException] */ @@ -134,4 +140,47 @@ abstract class MessagePacketBase(_bot: Bot) : */ suspend inline fun Image.download(): ByteReadPacket = bot.run { download() } // endregion +} + +/** + * 判断两个 [MessagePacket] 的 [MessagePacket.sender] 和 [MessagePacket.subject] 是否相同 + */ +fun MessagePacket<*, *>.isContextIdenticalWith(another: MessagePacket<*, *>): Boolean { + return this.sender == another.sender && this.subject == another.subject +} + +/** + * 挂起当前协程, 等待下一条 [MessagePacket.sender] 和 [MessagePacket.subject] 与 [P] 相同且通过 [筛选][filter] 的 [MessagePacket] + * + * 若 [filter] 抛出了一个异常, 本函数会立即抛出这个异常. + * + * @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制 + * @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值 + * + * @see subscribingGetAsync 本函数的异步版本 + */ +suspend inline fun > P.nextMessage( + timeoutMillis: Long = -1, + crossinline filter: P.(P) -> Boolean +): P { + return subscribingGet(timeoutMillis) { + takeIf { this.isContextIdenticalWith(this@nextMessage) }?.takeIf { filter(it, it) } + } +} + +/** + * 挂起当前协程, 等待下一条 [MessagePacket.sender] 和 [MessagePacket.subject] 与 [P] 相同的 [MessagePacket] + * + * 若 [filter] 抛出了一个异常, 本函数会立即抛出这个异常. + * + * @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制 + * + * @see subscribingGetAsync 本函数的异步版本 + */ +suspend inline fun > P.nextMessage( + timeoutMillis: Long = -1 +): P { + return subscribingGet(timeoutMillis) { + takeIf { this.isContextIdenticalWith(this@nextMessage) } + } } \ No newline at end of file diff --git a/mirai-demos/mirai-demo-1/src/main/java/demo/subscribe/SubscribeSamples.kt b/mirai-demos/mirai-demo-1/src/main/java/demo/subscribe/SubscribeSamples.kt index 88931f882..dc35014f7 100644 --- a/mirai-demos/mirai-demo-1/src/main/java/demo/subscribe/SubscribeSamples.kt +++ b/mirai-demos/mirai-demo-1/src/main/java/demo/subscribe/SubscribeSamples.kt @@ -16,17 +16,14 @@ import net.mamoe.mirai.Bot import net.mamoe.mirai.BotAccount import net.mamoe.mirai.alsoLogin import net.mamoe.mirai.contact.QQ +import net.mamoe.mirai.contact.isOperator import net.mamoe.mirai.contact.sendMessage import net.mamoe.mirai.event.* import net.mamoe.mirai.message.FriendMessage import net.mamoe.mirai.message.GroupMessage -import net.mamoe.mirai.message.data.AtAll -import net.mamoe.mirai.message.data.Image -import net.mamoe.mirai.message.data.PlainText -import net.mamoe.mirai.message.data.firstOrNull +import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.message.nextMessage import net.mamoe.mirai.message.sendAsImageTo -import net.mamoe.mirai.qqandroid.Bot -import net.mamoe.mirai.qqandroid.QQAndroid import net.mamoe.mirai.utils.FileBasedDeviceInfo import net.mamoe.mirai.utils.MiraiInternalAPI import java.io.File @@ -49,7 +46,7 @@ private fun readTestAccount(): BotAccount? { @Suppress("UNUSED_VARIABLE") suspend fun main() { - val bot = QQAndroid.Bot( // JVM 下也可以不写 `QQAndroid.` 引用顶层函数 + val bot = Bot( // JVM 下也可以不写 `QQAndroid.` 引用顶层函数 123456789, "123456" ) { @@ -207,6 +204,25 @@ fun Bot.messageDSL() { // sender: QQ // it: String (来自 MessageChain.toString) // group: Group + + case("recall") { + reply("😎").recallIn(3000) // 3 秒后自动撤回这条消息 + } + + case("禁言") { + // 挂起当前协程, 等待下一条满足条件的消息. + // 发送 "禁言" 后需要再发送一条消息 at 一个人. + val value: At = nextMessage { message.any(At) }[At] + value.member().mute(10) + } + + startsWith("群名=") { + if (!sender.isOperator()) { + sender.mute(5) + return@startsWith + } + group.name = it + } } }