mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-30 02:30:12 +08:00
Merge remote-tracking branch 'origin/dev' into anonymous
# Conflicts: # mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt
This commit is contained in:
commit
de3fd9d698
@ -141,7 +141,10 @@ fun Project.configureJvmTarget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlinTargets.orEmpty().filterIsInstance<KotlinJvmTarget>().forEach { target ->
|
kotlinTargets.orEmpty().filterIsInstance<KotlinJvmTarget>().forEach { target ->
|
||||||
target.compilations.all { kotlinOptions.jvmTarget = "1.8" }
|
target.compilations.all {
|
||||||
|
kotlinOptions.jvmTarget = "1.8"
|
||||||
|
kotlinOptions.languageVersion = "1.4"
|
||||||
|
}
|
||||||
target.testRuns["test"].executionTask.configure { useJUnitPlatform() }
|
target.testRuns["test"].executionTask.configure { useJUnitPlatform() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +119,9 @@ inline fun Project.configurePublishing(
|
|||||||
setPublications("mavenJava")
|
setPublications("mavenJava")
|
||||||
setConfigurations("archives")
|
setConfigurations("archives")
|
||||||
|
|
||||||
|
publish = true
|
||||||
|
override = true
|
||||||
|
|
||||||
pkg.apply {
|
pkg.apply {
|
||||||
repo = bintrayRepo
|
repo = bintrayRepo
|
||||||
name = bintrayPkgName
|
name = bintrayPkgName
|
||||||
|
@ -28,6 +28,10 @@ mirai 已经可以用于各项工作,但我们并不保证完全的稳定。
|
|||||||
**警告:** mirai 开发者自愿花费其休息时间,无偿维护 mirai 系列项目,**但没有义务提供任何方面的帮助**。
|
**警告:** mirai 开发者自愿花费其休息时间,无偿维护 mirai 系列项目,**但没有义务提供任何方面的帮助**。
|
||||||
使用者应秉持对开发者无私贡献的尊重,而不应该期望得到帮助。
|
使用者应秉持对开发者无私贡献的尊重,而不应该期望得到帮助。
|
||||||
|
|
||||||
|
### 启动 Mirai 时遇到 java.security.NoSuchProviderException: JCE cannot authenticate the provider BC
|
||||||
|
请更换 OpenJDK。该问题是由于BouncyCastle在重打包后丢失Jar签名引起的。
|
||||||
|
~~多发于Oracle JDK~~
|
||||||
|
|
||||||
## 开发相关
|
## 开发相关
|
||||||
|
|
||||||
> 欢迎帮助维护这个问题列表: 提交 PR 或者 issue.
|
> 欢迎帮助维护这个问题列表: 提交 PR 或者 issue.
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
publishing {
|
publishing {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
url = "https://api.bintray.com/maven/him188moe/mirai/mirai-core/;publish=0"
|
url = "https://api.bintray.com/maven/him188moe/mirai/mirai-core/;publish=1"
|
||||||
|
|
||||||
credentials {
|
credentials {
|
||||||
username = upload.Bintray.getUser(project)
|
username = upload.Bintray.getUser(project)
|
||||||
|
@ -73,7 +73,8 @@ public sealed class BotOfflineEvent : BotEvent, AbstractEvent() {
|
|||||||
* 因 returnCode = -10008 等原因掉线
|
* 因 returnCode = -10008 等原因掉线
|
||||||
*/
|
*/
|
||||||
@MiraiInternalApi("This is very experimental and might be changed")
|
@MiraiInternalApi("This is very experimental and might be changed")
|
||||||
public data class PacketFactory10008 internal constructor(
|
public data class PacketFactoryErrorCode internal constructor(
|
||||||
|
val returnCode: Int,
|
||||||
public override val bot: Bot,
|
public override val bot: Bot,
|
||||||
public override val cause: Throwable
|
public override val cause: Throwable
|
||||||
) : BotOfflineEvent(), Packet, BotPassiveEvent, CauseAware
|
) : BotOfflineEvent(), Packet, BotPassiveEvent, CauseAware
|
||||||
|
@ -262,6 +262,11 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
|||||||
*/
|
*/
|
||||||
public abstract val authorId: Long
|
public abstract val authorId: Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息原发送人, 为 `null` 表示原消息发送人已经不是 bot 的好友或已经被移出群。
|
||||||
|
*/
|
||||||
|
public abstract val author: UserOrBot?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息 ids.
|
* 消息 ids.
|
||||||
* @see MessageSource.ids
|
* @see MessageSource.ids
|
||||||
@ -282,7 +287,7 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
|||||||
/**
|
/**
|
||||||
* 好友消息撤回事件
|
* 好友消息撤回事件
|
||||||
*/
|
*/
|
||||||
public data class FriendRecall internal constructor(
|
public data class FriendRecall @MiraiInternalApi public constructor(
|
||||||
public override val bot: Bot,
|
public override val bot: Bot,
|
||||||
public override val messageIds: IntArray,
|
public override val messageIds: IntArray,
|
||||||
public override val messageInternalIds: IntArray,
|
public override val messageInternalIds: IntArray,
|
||||||
@ -290,11 +295,25 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
|||||||
/**
|
/**
|
||||||
* 撤回操作人, 好友的 [User.id]
|
* 撤回操作人, 好友的 [User.id]
|
||||||
*/
|
*/
|
||||||
public val operator: Long
|
public val operatorId: Long
|
||||||
) : MessageRecallEvent(), Packet {
|
) : MessageRecallEvent(), Packet {
|
||||||
public override val authorId: Long
|
@Deprecated("Binary compatibility.", level = DeprecationLevel.HIDDEN)
|
||||||
get() = bot.id
|
public fun getOperator(): Long = operatorId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回操作人. 为 `null` 表示该用户已经不是 bot 的好友
|
||||||
|
*/
|
||||||
|
public val operator: Friend? get() = bot.getFriend(operatorId)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息原发送人, 等于 [operator]
|
||||||
|
*/
|
||||||
|
override val author: Friend? get() = operator
|
||||||
|
|
||||||
|
public override val authorId: Long
|
||||||
|
get() = operatorId
|
||||||
|
|
||||||
|
@Suppress("DuplicatedCode")
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
@ -305,7 +324,7 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
|||||||
if (!messageIds.contentEquals(other.messageIds)) return false
|
if (!messageIds.contentEquals(other.messageIds)) return false
|
||||||
if (!messageInternalIds.contentEquals(other.messageInternalIds)) return false
|
if (!messageInternalIds.contentEquals(other.messageInternalIds)) return false
|
||||||
if (messageTime != other.messageTime) return false
|
if (messageTime != other.messageTime) return false
|
||||||
if (operator != other.operator) return false
|
if (operatorId != other.operatorId) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -315,7 +334,7 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
|||||||
result = 31 * result + messageIds.contentHashCode()
|
result = 31 * result + messageIds.contentHashCode()
|
||||||
result = 31 * result + messageInternalIds.contentHashCode()
|
result = 31 * result + messageInternalIds.contentHashCode()
|
||||||
result = 31 * result + messageTime
|
result = 31 * result + messageTime
|
||||||
result = 31 * result + operator.hashCode()
|
result = 31 * result + operatorId.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -335,6 +354,8 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
|||||||
public override val operator: Member?,
|
public override val operator: Member?,
|
||||||
public override val group: Group
|
public override val group: Group
|
||||||
) : MessageRecallEvent(), GroupOperableEvent, Packet {
|
) : MessageRecallEvent(), GroupOperableEvent, Packet {
|
||||||
|
override val author: NormalMember? get() = group[authorId]
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
@ -365,10 +386,12 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||||
|
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||||
public val MessageRecallEvent.GroupRecall.author: Member
|
public val MessageRecallEvent.GroupRecall.author: Member
|
||||||
get() = if (authorId == bot.id) group.botAsMember else group.getOrFail(authorId)
|
get() = if (authorId == bot.id) group.botAsMember else group.getOrFail(authorId)
|
||||||
|
|
||||||
public val MessageRecallEvent.FriendRecall.isByBot: Boolean get() = this.operator == bot.id
|
public val MessageRecallEvent.FriendRecall.isByBot: Boolean get() = this.operatorId == bot.id
|
||||||
// val MessageRecallEvent.GroupRecall.isByBot: Boolean get() = (this as GroupOperableEvent).isByBot
|
// val MessageRecallEvent.GroupRecall.isByBot: Boolean get() = (this as GroupOperableEvent).isByBot
|
||||||
// no need
|
// no need
|
||||||
|
|
||||||
|
@ -14,9 +14,9 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import net.mamoe.mirai.event.*
|
import net.mamoe.mirai.event.*
|
||||||
import net.mamoe.mirai.event.events.BotEvent
|
import net.mamoe.mirai.event.events.BotEvent
|
||||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
|
||||||
import net.mamoe.mirai.utils.MiraiLogger
|
import net.mamoe.mirai.utils.MiraiLogger
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
@ -26,7 +26,7 @@ internal fun <L : Listener<E>, E : Event> KClass<out E>.subscribeInternal(listen
|
|||||||
with(GlobalEventListeners[listener.priority]) {
|
with(GlobalEventListeners[listener.priority]) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val node = ListenerRegistry(listener as Listener<Event>, this@subscribeInternal)
|
val node = ListenerRegistry(listener as Listener<Event>, this@subscribeInternal)
|
||||||
addLast(node)
|
add(node)
|
||||||
listener.invokeOnCompletion {
|
listener.invokeOnCompletion {
|
||||||
this.remove(node)
|
this.remove(node)
|
||||||
}
|
}
|
||||||
@ -98,18 +98,18 @@ internal class ListenerRegistry(
|
|||||||
|
|
||||||
|
|
||||||
internal object GlobalEventListeners {
|
internal object GlobalEventListeners {
|
||||||
private val ALL_LEVEL_REGISTRIES: Map<EventPriority, LockFreeLinkedList<ListenerRegistry>>
|
private val ALL_LEVEL_REGISTRIES: Map<EventPriority, ConcurrentLinkedQueue<ListenerRegistry>>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val map =
|
val map =
|
||||||
EnumMap<Listener.EventPriority, LockFreeLinkedList<ListenerRegistry>>(Listener.EventPriority::class.java)
|
EnumMap<Listener.EventPriority, ConcurrentLinkedQueue<ListenerRegistry>>(Listener.EventPriority::class.java)
|
||||||
EventPriority.values().forEach {
|
EventPriority.values().forEach {
|
||||||
map[it] = LockFreeLinkedList()
|
map[it] = ConcurrentLinkedQueue()
|
||||||
}
|
}
|
||||||
this.ALL_LEVEL_REGISTRIES = map
|
this.ALL_LEVEL_REGISTRIES = map
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun get(priority: Listener.EventPriority): LockFreeLinkedList<ListenerRegistry> =
|
operator fun get(priority: Listener.EventPriority): ConcurrentLinkedQueue<ListenerRegistry> =
|
||||||
ALL_LEVEL_REGISTRIES[priority]!!
|
ALL_LEVEL_REGISTRIES[priority]!!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,54 +121,56 @@ internal suspend inline fun AbstractEvent.broadcastInternal() {
|
|||||||
callAndRemoveIfRequired(this@broadcastInternal)
|
callAndRemoveIfRequired(this@broadcastInternal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal inline fun <E, T : Iterable<E>> T.forEach0(block: T.(E) -> Unit) {
|
||||||
|
forEach { block(it) }
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode")
|
||||||
internal suspend inline fun <E : AbstractEvent> callAndRemoveIfRequired(
|
internal suspend inline fun <E : AbstractEvent> callAndRemoveIfRequired(
|
||||||
event: E
|
event: E
|
||||||
) {
|
) {
|
||||||
for (p in Listener.EventPriority.prioritiesExcludedMonitor) {
|
for (p in Listener.EventPriority.prioritiesExcludedMonitor) {
|
||||||
GlobalEventListeners[p].forEachNode { registeredRegistryNode ->
|
GlobalEventListeners[p].forEach0 { registeredRegistry ->
|
||||||
if (event.isIntercepted) {
|
if (event.isIntercepted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val listenerRegistry = registeredRegistryNode.nodeValue
|
if (!registeredRegistry.type.isInstance(event)) return@forEach0
|
||||||
if (!listenerRegistry.type.isInstance(event)) return@forEachNode
|
val listener = registeredRegistry.listener
|
||||||
val listener = listenerRegistry.listener
|
|
||||||
when (listener.concurrencyKind) {
|
when (listener.concurrencyKind) {
|
||||||
Listener.ConcurrencyKind.LOCKED -> {
|
Listener.ConcurrencyKind.LOCKED -> {
|
||||||
(listener as Handler).lock!!.withLock {
|
(listener as Handler).lock!!.withLock {
|
||||||
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
||||||
removeNode(registeredRegistryNode)
|
remove(registeredRegistry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Listener.ConcurrencyKind.CONCURRENT -> {
|
Listener.ConcurrencyKind.CONCURRENT -> {
|
||||||
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
||||||
removeNode(registeredRegistryNode)
|
remove(registeredRegistry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
GlobalEventListeners[EventPriority.MONITOR].forEachNode { registeredRegistryNode ->
|
GlobalEventListeners[EventPriority.MONITOR].forEach0 { registeredRegistry ->
|
||||||
if (event.isIntercepted) {
|
if (event.isIntercepted) {
|
||||||
return@coroutineScope
|
return@coroutineScope
|
||||||
}
|
}
|
||||||
val listenerRegistry = registeredRegistryNode.nodeValue
|
if (!registeredRegistry.type.isInstance(event)) return@forEach0
|
||||||
if (!listenerRegistry.type.isInstance(event)) return@forEachNode
|
val listener = registeredRegistry.listener
|
||||||
val listener = listenerRegistry.listener
|
|
||||||
launch {
|
launch {
|
||||||
when (listener.concurrencyKind) {
|
when (listener.concurrencyKind) {
|
||||||
Listener.ConcurrencyKind.LOCKED -> {
|
Listener.ConcurrencyKind.LOCKED -> {
|
||||||
(listener as Handler).lock!!.withLock {
|
(listener as Handler).lock!!.withLock {
|
||||||
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
||||||
removeNode(registeredRegistryNode)
|
remove(registeredRegistry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Listener.ConcurrencyKind.CONCURRENT -> {
|
Listener.ConcurrencyKind.CONCURRENT -> {
|
||||||
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
||||||
removeNode(registeredRegistryNode)
|
remove(registeredRegistry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,11 +56,12 @@ import net.mamoe.mirai.utils.MiraiExperimentalApi
|
|||||||
* @see nextMessage 挂起协程并等待下一条消息
|
* @see nextMessage 挂起协程并等待下一条消息
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
|
@BuilderInference
|
||||||
public suspend inline fun <reified T : MessageEvent> T.whileSelectMessages(
|
public suspend inline fun <reified T : MessageEvent> T.whileSelectMessages(
|
||||||
timeoutMillis: Long = -1,
|
timeoutMillis: Long = -1,
|
||||||
filterContext: Boolean = true,
|
filterContext: Boolean = true,
|
||||||
priority: Listener.EventPriority = EventPriority.MONITOR,
|
priority: Listener.EventPriority = EventPriority.MONITOR,
|
||||||
crossinline selectBuilder: @MessageDsl MessageSelectBuilder<T, Boolean>.() -> Unit
|
@BuilderInference crossinline selectBuilder: @MessageDsl MessageSelectBuilder<T, Boolean>.() -> Unit
|
||||||
): Unit = whileSelectMessagesImpl(timeoutMillis, filterContext, priority, selectBuilder)
|
): Unit = whileSelectMessagesImpl(timeoutMillis, filterContext, priority, selectBuilder)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,11 +69,12 @@ public suspend inline fun <reified T : MessageEvent> T.whileSelectMessages(
|
|||||||
*/
|
*/
|
||||||
@MiraiExperimentalApi
|
@MiraiExperimentalApi
|
||||||
@JvmName("selectMessages1")
|
@JvmName("selectMessages1")
|
||||||
|
@BuilderInference
|
||||||
public suspend inline fun <reified T : MessageEvent> T.selectMessagesUnit(
|
public suspend inline fun <reified T : MessageEvent> T.selectMessagesUnit(
|
||||||
timeoutMillis: Long = -1,
|
timeoutMillis: Long = -1,
|
||||||
filterContext: Boolean = true,
|
filterContext: Boolean = true,
|
||||||
priority: Listener.EventPriority = EventPriority.MONITOR,
|
priority: Listener.EventPriority = EventPriority.MONITOR,
|
||||||
crossinline selectBuilder: @MessageDsl MessageSelectBuilderUnit<T, Unit>.() -> Unit
|
@BuilderInference crossinline selectBuilder: @MessageDsl MessageSelectBuilderUnit<T, Unit>.() -> Unit
|
||||||
): Unit = selectMessagesImpl(timeoutMillis, true, filterContext, priority, selectBuilder)
|
): Unit = selectMessagesImpl(timeoutMillis, true, filterContext, priority, selectBuilder)
|
||||||
|
|
||||||
|
|
||||||
@ -97,12 +99,12 @@ public suspend inline fun <reified T : MessageEvent> T.selectMessagesUnit(
|
|||||||
* @see nextMessage 挂起协程并等待下一条消息
|
* @see nextMessage 挂起协程并等待下一条消息
|
||||||
*/
|
*/
|
||||||
@Suppress("unused") // false positive
|
@Suppress("unused") // false positive
|
||||||
// @BuilderInference // https://youtrack.jetbrains.com/issue/KT-37716
|
@BuilderInference
|
||||||
public suspend inline fun <reified T : MessageEvent, R> T.selectMessages(
|
public suspend inline fun <reified T : MessageEvent, R> T.selectMessages(
|
||||||
timeoutMillis: Long = -1,
|
timeoutMillis: Long = -1,
|
||||||
filterContext: Boolean = true,
|
filterContext: Boolean = true,
|
||||||
priority: Listener.EventPriority = EventPriority.MONITOR,
|
priority: Listener.EventPriority = EventPriority.MONITOR,
|
||||||
// @BuilderInference
|
@BuilderInference
|
||||||
crossinline selectBuilder: @MessageDsl MessageSelectBuilder<T, R>.() -> Unit
|
crossinline selectBuilder: @MessageDsl MessageSelectBuilder<T, R>.() -> Unit
|
||||||
): R =
|
): R =
|
||||||
selectMessagesImpl(
|
selectMessagesImpl(
|
||||||
|
@ -157,12 +157,19 @@ public inline fun MessageChain.noneContent(block: (MessageContent) -> Boolean):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取第一个 [M] 类型的 [Message] 实例
|
||||||
|
*/
|
||||||
|
@JvmSynthetic
|
||||||
|
public inline fun <reified M : SingleMessage?> MessageChain.findIsInstance(): M? =
|
||||||
|
this.find { it is M } as M?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取第一个 [M] 类型的 [Message] 实例
|
* 获取第一个 [M] 类型的 [Message] 实例
|
||||||
*/
|
*/
|
||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
public inline fun <reified M : SingleMessage?> MessageChain.firstIsInstanceOrNull(): M? =
|
public inline fun <reified M : SingleMessage?> MessageChain.firstIsInstanceOrNull(): M? =
|
||||||
this.firstOrNull { it is M } as M?
|
this.find { it is M } as M?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取第一个 [M] 类型的 [Message] 实例
|
* 获取第一个 [M] 类型的 [Message] 实例
|
||||||
|
@ -39,6 +39,7 @@ public class Voice @MiraiInternalApi constructor(
|
|||||||
public override val fileName: String,
|
public override val fileName: String,
|
||||||
public override val md5: ByteArray,
|
public override val md5: ByteArray,
|
||||||
public override val fileSize: Long,
|
public override val fileSize: Long,
|
||||||
|
public val codec: Int = 0,
|
||||||
private val _url: String
|
private val _url: String
|
||||||
) : PttMessage() {
|
) : PttMessage() {
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ public class RetryLaterException @MiraiInternalApi constructor() :
|
|||||||
* 无标准输入或 Kotlin 不支持此输入.
|
* 无标准输入或 Kotlin 不支持此输入.
|
||||||
*/
|
*/
|
||||||
public class NoStandardInputForCaptchaException @MiraiInternalApi constructor(
|
public class NoStandardInputForCaptchaException @MiraiInternalApi constructor(
|
||||||
public override val cause: Throwable?
|
public override val cause: Throwable? = null
|
||||||
) : LoginFailedException(true, "no standard input for captcha")
|
) : LoginFailedException(true, "no standard input for captcha")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,8 +90,17 @@ public open class BotConfiguration { // open for Java
|
|||||||
/** 最多尝试多少次重连 */
|
/** 最多尝试多少次重连 */
|
||||||
public var reconnectionRetryTimes: Int = Int.MAX_VALUE
|
public var reconnectionRetryTimes: Int = Int.MAX_VALUE
|
||||||
|
|
||||||
/** 验证码处理器 */
|
/**
|
||||||
public var loginSolver: LoginSolver = LoginSolver.Default
|
* 验证码处理器
|
||||||
|
*
|
||||||
|
* - 在 Android 需要手动提供 [LoginSolver]
|
||||||
|
* - 在 JVM, Mirai 会根据环境支持情况选择 Swing/CLI 实现
|
||||||
|
*
|
||||||
|
* 详见 [LoginSolver.Default]
|
||||||
|
*
|
||||||
|
* @see LoginSolver
|
||||||
|
*/
|
||||||
|
public var loginSolver: LoginSolver? = LoginSolver.Default
|
||||||
|
|
||||||
/** 使用协议类型 */
|
/** 使用协议类型 */
|
||||||
public var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE
|
public var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE
|
||||||
@ -115,6 +124,7 @@ public open class BotConfiguration { // open for Java
|
|||||||
Json {
|
Json {
|
||||||
isLenient = true
|
isLenient = true
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
|
prettyPrint = true
|
||||||
}
|
}
|
||||||
}.getOrElse { Json {} }
|
}.getOrElse { Json {} }
|
||||||
|
|
||||||
@ -133,6 +143,7 @@ public open class BotConfiguration { // open for Java
|
|||||||
*
|
*
|
||||||
* @see deviceInfo
|
* @see deviceInfo
|
||||||
*/
|
*/
|
||||||
|
@ConfigurationDsl
|
||||||
public fun loadDeviceInfoJson(json: String) {
|
public fun loadDeviceInfoJson(json: String) {
|
||||||
deviceInfo = {
|
deviceInfo = {
|
||||||
this.json.decodeFromString(DeviceInfo.serializer(), json)
|
this.json.decodeFromString(DeviceInfo.serializer(), json)
|
||||||
|
@ -20,6 +20,8 @@ import kotlinx.coroutines.withContext
|
|||||||
import net.mamoe.mirai.Bot
|
import net.mamoe.mirai.Bot
|
||||||
import net.mamoe.mirai.network.LoginFailedException
|
import net.mamoe.mirai.network.LoginFailedException
|
||||||
import net.mamoe.mirai.network.NoStandardInputForCaptchaException
|
import net.mamoe.mirai.network.NoStandardInputForCaptchaException
|
||||||
|
import net.mamoe.mirai.utils.LoginSolver.Companion.Default
|
||||||
|
import net.mamoe.mirai.utils.StandardCharImageLoginSolver.Companion.createBlocking
|
||||||
import java.awt.Image
|
import java.awt.Image
|
||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -29,6 +31,9 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证码, 设备锁解决器
|
* 验证码, 设备锁解决器
|
||||||
|
*
|
||||||
|
* @see Default
|
||||||
|
* @see BotConfiguration.loginSolver
|
||||||
*/
|
*/
|
||||||
public abstract class LoginSolver {
|
public abstract class LoginSolver {
|
||||||
/**
|
/**
|
||||||
@ -61,109 +66,134 @@ public abstract class LoginSolver {
|
|||||||
public abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
|
public abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public val Default: LoginSolver = kotlin.run {
|
/**
|
||||||
if (WindowHelperJvm.isDesktopSupported) {
|
* 当前平台默认的 [LoginSolver]。
|
||||||
SwingSolver
|
*
|
||||||
} else {
|
* 检测策略:
|
||||||
DefaultLoginSolver({ readLine() ?: throw NoStandardInputForCaptchaException(null) })
|
* 1. 检测 `android.util.Log`, 如果存在, 返回 `null`.
|
||||||
}
|
* 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 [StandardCharImageLoginSolver]
|
||||||
}
|
* 3. 检测 JVM 桌面环境, 若支持, 返回 [SwingSolver]
|
||||||
}
|
* 4. 返回 [StandardCharImageLoginSolver]
|
||||||
}
|
*
|
||||||
|
* @return [SwingSolver] 或 [StandardCharImageLoginSolver] 或 `null`
|
||||||
|
|
||||||
/**
|
|
||||||
* 自动选择 [SwingSolver] 或 [StandardCharImageLoginSolver]
|
|
||||||
*/
|
*/
|
||||||
@MiraiExperimentalApi
|
@JvmField
|
||||||
public class DefaultLoginSolver(
|
public val Default: LoginSolver? = when (WindowHelperJvm.platformKind) {
|
||||||
public val input: suspend () -> String,
|
WindowHelperJvm.PlatformKind.ANDROID -> null
|
||||||
overrideLogger: MiraiLogger? = null
|
WindowHelperJvm.PlatformKind.SWING -> SwingSolver
|
||||||
) : LoginSolver() {
|
WindowHelperJvm.PlatformKind.CLI -> StandardCharImageLoginSolver()
|
||||||
private val delegate: LoginSolver = StandardCharImageLoginSolver(input, overrideLogger)
|
|
||||||
|
|
||||||
override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? {
|
|
||||||
return delegate.onSolvePicCaptcha(bot, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? {
|
@Suppress("unused")
|
||||||
return delegate.onSolveSliderCaptcha(bot, url)
|
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||||
}
|
public fun getDefault(): LoginSolver = Default
|
||||||
|
?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
|
||||||
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
|
|
||||||
return delegate.onSolveUnsafeDeviceLoginVerify(bot, url)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用字符图片展示验证码, 使用 [input] 获取输入, 使用 [overrideLogger] 输出
|
* CLI 环境 [LoginSolver]. 将验证码图片转为字符画并通过 `output` 输出, [input] 获取用户输入.
|
||||||
|
*
|
||||||
|
* 使用字符图片展示验证码, 使用 [input] 获取输入, 使用 [loggerSupplier] 输出
|
||||||
|
*
|
||||||
|
* @see createBlocking
|
||||||
*/
|
*/
|
||||||
@MiraiExperimentalApi
|
public class StandardCharImageLoginSolver @JvmOverloads constructor(
|
||||||
public class StandardCharImageLoginSolver(
|
input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
|
||||||
input: suspend () -> String,
|
|
||||||
/**
|
/**
|
||||||
* 为 `null` 时使用 [Bot.logger]
|
* 为 `null` 时使用 [Bot.logger]
|
||||||
*/
|
*/
|
||||||
private val overrideLogger: MiraiLogger? = null
|
private val loggerSupplier: (bot: Bot) -> MiraiLogger = { it.logger }
|
||||||
) : LoginSolver() {
|
) : LoginSolver() {
|
||||||
|
public constructor(
|
||||||
|
input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
|
||||||
|
overrideLogger: MiraiLogger?
|
||||||
|
) : this(input, { overrideLogger ?: it.logger })
|
||||||
|
|
||||||
private val input: suspend () -> String = suspend {
|
private val input: suspend () -> String = suspend {
|
||||||
withContext(Dispatchers.IO) { input() }
|
withContext(Dispatchers.IO) { input() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock {
|
override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock {
|
||||||
val logger = overrideLogger ?: bot.logger
|
val logger = loggerSupplier(bot)
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val tempFile: File = File.createTempFile("tmp", ".png").apply { deleteOnExit() }
|
val tempFile: File = File.createTempFile("tmp", ".png").apply { deleteOnExit() }
|
||||||
tempFile.createNewFile()
|
tempFile.createNewFile()
|
||||||
logger.info("需要图片验证码登录, 验证码为 4 字母")
|
logger.info { "[PicCaptcha] 需要图片验证码登录, 验证码为 4 字母" }
|
||||||
|
logger.info { "[PicCaptcha] Picture captcha required. Captcha consists of 4 letters." }
|
||||||
try {
|
try {
|
||||||
tempFile.writeChannel().apply { writeFully(data); close() }
|
tempFile.writeChannel().apply { writeFully(data); close() }
|
||||||
logger.info("将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}")
|
logger.info { "[PicCaptcha] 将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}" }
|
||||||
|
logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file ${tempFile.absolutePath}" }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.info("无法写出验证码文件(${e.message}), 请尝试查看以上字符图片")
|
logger.warning("[PicCaptcha] 无法写出验证码文件, 请尝试查看以上字符图片", e)
|
||||||
|
logger.warning("[PicCaptcha] Failed to export captcha image. Please see the char-image.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
tempFile.inputStream().use {
|
tempFile.inputStream().use { stream ->
|
||||||
try {
|
try {
|
||||||
val img = ImageIO.read(it)
|
val img = ImageIO.read(stream)
|
||||||
if (img == null) {
|
if (img == null) {
|
||||||
logger.info("无法创建字符图片. 请查看文件")
|
logger.warning { "[PicCaptcha] 无法创建字符图片. 请查看文件" }
|
||||||
|
logger.warning { "[PicCaptcha] Failed to create char-image. Please see the file." }
|
||||||
} else {
|
} else {
|
||||||
logger.info("\n" + img.createCharImg())
|
logger.info { "[PicCaptcha] \n" + img.createCharImg() }
|
||||||
}
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
logger.info("创建字符图片时出错($throwable)。请查看文件")
|
logger.warning("[PicCaptcha] 创建字符图片时出错. 请查看文件.", throwable)
|
||||||
|
logger.warning("[PicCaptcha] Failed to create char-image. Please see the file.", throwable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info("请输入 4 位字母验证码. 若要更换验证码, 请直接回车")
|
logger.info { "[PicCaptcha] 请输入 4 位字母验证码. 若要更换验证码, 请直接回车" }
|
||||||
|
logger.info { "[PicCaptcha] Please type 4-letter captcha. Press Enter directly to refresh." }
|
||||||
return input().takeUnless { it.isEmpty() || it.length != 4 }.also {
|
return input().takeUnless { it.isEmpty() || it.length != 4 }.also {
|
||||||
logger.info("正在提交[$it]中...")
|
logger.info { "[PicCaptcha] 正在提交 $it..." }
|
||||||
|
logger.info { "[PicCaptcha] Submitting $it..." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String = loginSolverLock.withLock {
|
override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String = loginSolverLock.withLock {
|
||||||
val logger = overrideLogger ?: bot.logger
|
val logger = loggerSupplier(bot)
|
||||||
logger.info("需要滑动验证码")
|
logger.info { "[SliderCaptcha] 需要滑动验证码, 请在任意浏览器中打开以下链接并完成验证码, 完成后请输入任意字符." }
|
||||||
logger.info("请在任意浏览器中打开以下链接并完成验证码. ")
|
logger.info { "[SliderCaptcha] Slider captcha required, please open the following link in any browser and solve the captcha. Type anything here after completion." }
|
||||||
logger.info("完成后请输入任意字符 ")
|
|
||||||
logger.info(url)
|
logger.info(url)
|
||||||
return input().also {
|
return input().also {
|
||||||
logger.info("正在提交中...")
|
logger.info { "[SliderCaptcha] 正在提交中..." }
|
||||||
|
logger.info { "[SliderCaptcha] Submitting..." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String = loginSolverLock.withLock {
|
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String = loginSolverLock.withLock {
|
||||||
val logger = overrideLogger ?: bot.logger
|
val logger = loggerSupplier(bot)
|
||||||
logger.info("需要进行账户安全认证")
|
logger.info { "[UnsafeLogin] 当前登录环境不安全,服务器要求账户认证。请在 QQ 浏览器打开 $url 并完成验证后输入任意字符。" }
|
||||||
logger.info("该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题")
|
logger.info { "[UnsafeLogin] Account verification required by the server. Please open $url in QQ browser and complete challenge, then type anything here to submit." }
|
||||||
logger.info("完成以下账号认证即可成功登录|理论本认证在mirai每个账户中最多出现1次")
|
|
||||||
logger.info("请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符")
|
|
||||||
logger.info("这步操作将在后续的版本中优化")
|
|
||||||
logger.info(url)
|
|
||||||
return input().also {
|
return input().also {
|
||||||
logger.info("正在提交中...")
|
logger.info { "[UnsafeLogin] 正在提交中..." }
|
||||||
|
logger.info { "[UnsafeLogin] Submitting..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
/**
|
||||||
|
* 创建 Java 阻塞版 [input] 的 [StandardCharImageLoginSolver]
|
||||||
|
*
|
||||||
|
* @param input 将在协程 IO 池执行, 可以有阻塞调用
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
public fun createBlocking(input: () -> String, output: MiraiLogger?): StandardCharImageLoginSolver {
|
||||||
|
return StandardCharImageLoginSolver({ withContext(Dispatchers.IO) { input() } }, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Java 阻塞版 [input] 的 [StandardCharImageLoginSolver]
|
||||||
|
*
|
||||||
|
* @param input 将在协程 IO 池执行, 可以有阻塞调用
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
public fun createBlocking(input: () -> String): StandardCharImageLoginSolver {
|
||||||
|
return StandardCharImageLoginSolver({ withContext(Dispatchers.IO) { input() } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,20 +70,24 @@ public object SwingSolver : LoginSolver() {
|
|||||||
// 不会触发各种 NoDefClassError
|
// 不会触发各种 NoDefClassError
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
internal object WindowHelperJvm {
|
internal object WindowHelperJvm {
|
||||||
internal val isDesktopSupported: Boolean = kotlin.run {
|
enum class PlatformKind {
|
||||||
if (System.getProperty("mirai.no-desktop") === null) {
|
ANDROID,
|
||||||
|
SWING,
|
||||||
|
CLI
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val platformKind: PlatformKind = kotlin.run {
|
||||||
|
if (kotlin.runCatching { Class.forName("android.util.Log") }.isSuccess) {
|
||||||
|
// Android platform
|
||||||
|
return@run PlatformKind.ANDROID
|
||||||
|
}
|
||||||
|
if (System.getProperty("mirai.no-desktop") != null) return@run PlatformKind.CLI
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
Class.forName("java.awt.Desktop")
|
Class.forName("java.awt.Desktop")
|
||||||
Class.forName("java.awt.Toolkit")
|
Class.forName("java.awt.Toolkit")
|
||||||
}.onFailure { return@run false } // Android OS
|
|
||||||
kotlin.runCatching {
|
|
||||||
Toolkit.getDefaultToolkit()
|
Toolkit.getDefaultToolkit()
|
||||||
}.onFailure { // AWT Error, #270
|
|
||||||
return@run false
|
if (Desktop.isDesktopSupported()) {
|
||||||
}
|
|
||||||
kotlin.runCatching {
|
|
||||||
Desktop.isDesktopSupported().also { stat ->
|
|
||||||
if (stat) {
|
|
||||||
MiraiLogger.TopLevel.info(
|
MiraiLogger.TopLevel.info(
|
||||||
"""
|
"""
|
||||||
Mirai 正在使用桌面环境. 如遇到验证码将会弹出对话框. 可添加 JVM 属性 `mirai.no-desktop` 以关闭.
|
Mirai 正在使用桌面环境. 如遇到验证码将会弹出对话框. 可添加 JVM 属性 `mirai.no-desktop` 以关闭.
|
||||||
@ -94,15 +98,12 @@ internal object WindowHelperJvm {
|
|||||||
Mirai is using desktop. Captcha will be thrown by window popup. You can add `mirai.no-desktop` to JVM properties (-Dmirai.no-desktop) to disable it.
|
Mirai is using desktop. Captcha will be thrown by window popup. You can add `mirai.no-desktop` to JVM properties (-Dmirai.no-desktop) to disable it.
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
}
|
return@run PlatformKind.SWING
|
||||||
|
} else {
|
||||||
|
return@run PlatformKind.CLI
|
||||||
}
|
}
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
// Should not happen
|
return@run PlatformKind.CLI
|
||||||
MiraiLogger.TopLevel.warning("Exception in checking desktop support.", it)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ internal abstract class AbstractBot<N : BotNetworkHandler> constructor(
|
|||||||
is BotOfflineEvent.MsfOffline,
|
is BotOfflineEvent.MsfOffline,
|
||||||
is BotOfflineEvent.Dropped,
|
is BotOfflineEvent.Dropped,
|
||||||
is BotOfflineEvent.RequireReconnect,
|
is BotOfflineEvent.RequireReconnect,
|
||||||
is BotOfflineEvent.PacketFactory10008
|
is BotOfflineEvent.PacketFactoryErrorCode
|
||||||
-> {
|
-> {
|
||||||
if (!_network.isActive) {
|
if (!_network.isActive) {
|
||||||
// normally closed
|
// normally closed
|
||||||
|
@ -276,16 +276,6 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
|||||||
if (bot.id != source.fromId) {
|
if (bot.id != source.fromId) {
|
||||||
group.checkBotPermission(MemberPermission.ADMINISTRATOR)
|
group.checkBotPermission(MemberPermission.ADMINISTRATOR)
|
||||||
}
|
}
|
||||||
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
|
|
||||||
MessageRecallEvent.GroupRecall(
|
|
||||||
bot,
|
|
||||||
source.fromId,
|
|
||||||
source.ids,
|
|
||||||
source.internalIds,
|
|
||||||
source.time,
|
|
||||||
null,
|
|
||||||
group
|
|
||||||
).broadcast()
|
|
||||||
|
|
||||||
network.run {
|
network.run {
|
||||||
PbMessageSvc.PbMsgWithDraw.createForGroupMessage(
|
PbMessageSvc.PbMsgWithDraw.createForGroupMessage(
|
||||||
|
@ -35,6 +35,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcP
|
|||||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGroup
|
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGroup
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
|
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
|
import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
|
||||||
|
import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache
|
||||||
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
|
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
|
||||||
import net.mamoe.mirai.internal.utils.estimateLength
|
import net.mamoe.mirai.internal.utils.estimateLength
|
||||||
import net.mamoe.mirai.internal.utils.toUHexString
|
import net.mamoe.mirai.internal.utils.toUHexString
|
||||||
@ -460,11 +461,13 @@ internal class GroupImpl(
|
|||||||
response.fileKey,
|
response.fileKey,
|
||||||
codec
|
codec
|
||||||
)
|
)
|
||||||
Voice("${md5.toUHexString("")}.amr", md5, content.size.toLong(), "")
|
Voice("${md5.toUHexString("")}.amr", md5, content.size.toLong(), codec, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun toString(): String = "Group($id)"
|
override fun toString(): String = "Group($id)"
|
||||||
|
|
||||||
|
val groupPkgMsgParsingCache = GroupPkgMsgParsingCache()
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import net.mamoe.mirai.contact.Group
|
|||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
|
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgOnlinePush
|
||||||
import net.mamoe.mirai.internal.utils.*
|
import net.mamoe.mirai.internal.utils.*
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||||
@ -234,24 +235,52 @@ internal fun MsgComm.Msg.toMessageChain(
|
|||||||
isTemp: Boolean = false
|
isTemp: Boolean = false
|
||||||
): MessageChain = toMessageChain(bot, bot.id, groupIdOrZero, onlineSource, isTemp)
|
): MessageChain = toMessageChain(bot, bot.id, groupIdOrZero, onlineSource, isTemp)
|
||||||
|
|
||||||
|
internal fun List<MsgOnlinePush.PbPushMsg>.toMessageChain(
|
||||||
|
bot: Bot,
|
||||||
|
groupIdOrZero: Long,
|
||||||
|
onlineSource: Boolean,
|
||||||
|
isTemp: Boolean = false
|
||||||
|
): MessageChain = toMessageChain(bot, bot.id, groupIdOrZero, onlineSource, isTemp)
|
||||||
|
|
||||||
|
@JvmName("toMessageChain1")
|
||||||
|
internal fun List<MsgOnlinePush.PbPushMsg>.toMessageChain(
|
||||||
|
bot: Bot?,
|
||||||
|
botId: Long,
|
||||||
|
groupIdOrZero: Long,
|
||||||
|
onlineSource: Boolean,
|
||||||
|
isTemp: Boolean = false
|
||||||
|
): MessageChain = map{it.msg}.toMessageChain(bot, botId, groupIdOrZero, onlineSource, isTemp)
|
||||||
|
|
||||||
internal fun MsgComm.Msg.toMessageChain(
|
internal fun MsgComm.Msg.toMessageChain(
|
||||||
bot: Bot?,
|
bot: Bot?,
|
||||||
botId: Long,
|
botId: Long,
|
||||||
groupIdOrZero: Long,
|
groupIdOrZero: Long,
|
||||||
onlineSource: Boolean,
|
onlineSource: Boolean,
|
||||||
isTemp: Boolean = false
|
isTemp: Boolean = false
|
||||||
): MessageChain {
|
): MessageChain = listOf(this).toMessageChain(bot, botId, groupIdOrZero, onlineSource, isTemp)
|
||||||
val elements = this.msgBody.richText.elems
|
|
||||||
|
|
||||||
val pptMsg = msgBody.richText.ptt?.run {
|
internal fun List<MsgComm.Msg>.toMessageChain(
|
||||||
|
bot: Bot?,
|
||||||
|
botId: Long,
|
||||||
|
groupIdOrZero: Long,
|
||||||
|
onlineSource: Boolean,
|
||||||
|
isTemp: Boolean = false
|
||||||
|
): MessageChain {
|
||||||
|
val elements = this.flatMap { it.msgBody.richText.elems }
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
val ppts = buildList<Message> {
|
||||||
|
this@toMessageChain.forEach { msg ->
|
||||||
|
msg.msgBody.richText.ptt?.run {
|
||||||
// when (fileType) {
|
// when (fileType) {
|
||||||
// 4 -> Voice(String(fileName), fileMd5, fileSize.toLong(),String(downPara))
|
// 4 -> Voice(String(fileName), fileMd5, fileSize.toLong(),String(downPara))
|
||||||
// else -> null
|
// else -> null
|
||||||
// }
|
// }
|
||||||
Voice(String(fileName), fileMd5, fileSize.toLong(), String(downPara))
|
add(Voice(String(fileName), fileMd5, fileSize.toLong(), format, String(downPara)))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return buildMessageChain(elements.size + 1 + if (pptMsg == null) 0 else 1) {
|
}
|
||||||
|
return buildMessageChain(elements.size + 1 + ppts.size) {
|
||||||
if (onlineSource) {
|
if (onlineSource) {
|
||||||
checkNotNull(bot) { "bot is null" }
|
checkNotNull(bot) { "bot is null" }
|
||||||
when {
|
when {
|
||||||
@ -263,7 +292,6 @@ internal fun MsgComm.Msg.toMessageChain(
|
|||||||
+OfflineMessageSourceImplByMsg(this@toMessageChain, botId)
|
+OfflineMessageSourceImplByMsg(this@toMessageChain, botId)
|
||||||
}
|
}
|
||||||
elements.joinToMessageChain(groupIdOrZero, botId, this)
|
elements.joinToMessageChain(groupIdOrZero, botId, this)
|
||||||
pptMsg?.let(::add)
|
|
||||||
}.cleanupRubbishMessageElements()
|
}.cleanupRubbishMessageElements()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import net.mamoe.mirai.message.data.Message
|
|||||||
import net.mamoe.mirai.message.data.MessageChain
|
import net.mamoe.mirai.message.data.MessageChain
|
||||||
import net.mamoe.mirai.message.data.MessageSource
|
import net.mamoe.mirai.message.data.MessageSource
|
||||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||||
|
import net.mamoe.mirai.utils.mapToIntArray
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
internal interface MessageSourceInternal {
|
internal interface MessageSourceInternal {
|
||||||
@ -65,44 +66,44 @@ internal suspend inline fun Message.ensureSequenceIdAvailable() {
|
|||||||
|
|
||||||
internal class MessageSourceFromFriendImpl(
|
internal class MessageSourceFromFriendImpl(
|
||||||
override val bot: Bot,
|
override val bot: Bot,
|
||||||
val msg: MsgComm.Msg
|
val msg: List<MsgComm.Msg>
|
||||||
) : OnlineMessageSource.Incoming.FromFriend(), MessageSourceInternal {
|
) : OnlineMessageSource.Incoming.FromFriend(), MessageSourceInternal {
|
||||||
override val sequenceIds: IntArray get() = intArrayOf(msg.msgHead.msgSeq)
|
override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||||
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
||||||
override val ids: IntArray get() = sequenceIds// msg.msgBody.richText.attr!!.random
|
override val ids: IntArray get() = sequenceIds// msg.msgBody.richText.attr!!.random
|
||||||
override val internalIds: IntArray get() = intArrayOf(msg.msgBody.richText.attr!!.random)
|
override val internalIds: IntArray get() = msg.mapToIntArray { it.msgBody.richText.attr!!.random }
|
||||||
override val time: Int get() = msg.msgHead.msgTime
|
override val time: Int get() = msg.first().msgHead.msgTime
|
||||||
override val originalMessage: MessageChain by lazy { msg.toMessageChain(bot, bot.id, 0, false) }
|
override val originalMessage: MessageChain by lazy { msg.toMessageChain(bot, bot.id, 0, false) }
|
||||||
override val sender: Friend get() = bot.getFriendOrFail(msg.msgHead.fromUin)
|
override val sender: Friend get() = bot.getFriendOrFail(msg.first().msgHead.fromUin)
|
||||||
|
|
||||||
private val jceData by lazy { msg.toJceDataFriendOrTemp(internalIds) }
|
private val jceData by lazy { msg.toJceDataFriendOrTemp(internalIds) }
|
||||||
|
|
||||||
override fun toJceData(): ImMsgBody.SourceMsg = jceData
|
override fun toJceData(): ImMsgBody.SourceMsg = jceData
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MsgComm.Msg.toJceDataFriendOrTemp(ids: IntArray): ImMsgBody.SourceMsg {
|
private fun List<MsgComm.Msg>.toJceDataFriendOrTemp(ids: IntArray): ImMsgBody.SourceMsg {
|
||||||
val elements = msgBody.richText.elems.toMutableList().also {
|
val elements = flatMap {it.msgBody.richText.elems}.toMutableList().also {
|
||||||
if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
|
if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
|
||||||
}
|
}
|
||||||
return ImMsgBody.SourceMsg(
|
return ImMsgBody.SourceMsg(
|
||||||
origSeqs = intArrayOf(this.msgHead.msgSeq),
|
origSeqs = mapToIntArray { it.msgHead.msgSeq },
|
||||||
senderUin = this.msgHead.fromUin,
|
senderUin = first().msgHead.fromUin,
|
||||||
toUin = this.msgHead.toUin,
|
toUin = first().msgHead.toUin,
|
||||||
flag = 1,
|
flag = 1,
|
||||||
elems = this.msgBody.richText.elems,
|
elems = flatMap{it.msgBody.richText.elems},
|
||||||
type = 0,
|
type = 0,
|
||||||
time = this.msgHead.msgTime,
|
time = this.first().msgHead.msgTime,
|
||||||
pbReserve = SourceMsg.ResvAttr(
|
pbReserve = SourceMsg.ResvAttr(
|
||||||
origUids = ids.map { it.toLong() and 0xFFFF_FFFF }
|
origUids = ids.map { it.toLong() and 0xFFFF_FFFF }
|
||||||
).toByteArray(SourceMsg.ResvAttr.serializer()),
|
).toByteArray(SourceMsg.ResvAttr.serializer()),
|
||||||
srcMsg = MsgComm.Msg(
|
srcMsg = MsgComm.Msg(
|
||||||
msgHead = MsgComm.MsgHead(
|
msgHead = MsgComm.MsgHead(
|
||||||
fromUin = this.msgHead.fromUin, // qq
|
fromUin = this.first().msgHead.fromUin, // qq
|
||||||
toUin = this.msgHead.toUin, // group
|
toUin = this.first().msgHead.toUin, // group
|
||||||
msgType = this.msgHead.msgType, // 82?
|
msgType = this.first().msgHead.msgType, // 82?
|
||||||
c2cCmd = this.msgHead.c2cCmd,
|
c2cCmd = this.first().msgHead.c2cCmd,
|
||||||
msgSeq = this.msgHead.msgSeq,
|
msgSeq = this.first().msgHead.msgSeq,
|
||||||
msgTime = this.msgHead.msgTime,
|
msgTime = this.first().msgHead.msgTime,
|
||||||
msgUid = ids.single().toLong() and 0xFFFF_FFFF, // ok
|
msgUid = ids.single().toLong() and 0xFFFF_FFFF, // ok
|
||||||
// groupInfo = MsgComm.GroupInfo(groupCode = this.msgHead.groupInfo.groupCode),
|
// groupInfo = MsgComm.GroupInfo(groupCode = this.msgHead.groupInfo.groupCode),
|
||||||
isSrcMsg = true
|
isSrcMsg = true
|
||||||
@ -118,16 +119,24 @@ private fun MsgComm.Msg.toJceDataFriendOrTemp(ids: IntArray): ImMsgBody.SourceMs
|
|||||||
|
|
||||||
internal class MessageSourceFromTempImpl(
|
internal class MessageSourceFromTempImpl(
|
||||||
override val bot: Bot,
|
override val bot: Bot,
|
||||||
private val msg: MsgComm.Msg
|
private val msg: List<MsgComm.Msg>
|
||||||
) : OnlineMessageSource.Incoming.FromTemp(), MessageSourceInternal {
|
) : OnlineMessageSource.Incoming.FromTemp(), MessageSourceInternal {
|
||||||
override val sequenceIds: IntArray get() = intArrayOf(msg.msgHead.msgSeq)
|
override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||||
override val internalIds: IntArray get() = intArrayOf(msg.msgBody.richText.attr!!.random)
|
override val internalIds: IntArray get() = msg.mapToIntArray{it.msgBody.richText.attr!!.random }
|
||||||
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
||||||
override val ids: IntArray get() = sequenceIds//
|
override val ids: IntArray get() = sequenceIds//
|
||||||
override val time: Int get() = msg.msgHead.msgTime
|
override val time: Int get() = msg.first().msgHead.msgTime
|
||||||
override val originalMessage: MessageChain by lazy { msg.toMessageChain(bot, 0, false) }
|
override val originalMessage: MessageChain by lazy {
|
||||||
|
msg.toMessageChain(
|
||||||
|
bot,
|
||||||
|
bot.id,
|
||||||
|
groupIdOrZero = 0,
|
||||||
|
onlineSource = false,
|
||||||
|
isTemp = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
override val sender: Member
|
override val sender: Member
|
||||||
get() = with(msg.msgHead) {
|
get() = with(msg.first().msgHead) {
|
||||||
bot.getGroupOrFail(c2cTmpMsgHead!!.groupUin).getOrFail(fromUin)
|
bot.getGroupOrFail(c2cTmpMsgHead!!.groupUin).getOrFail(fromUin)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,24 +146,24 @@ internal class MessageSourceFromTempImpl(
|
|||||||
|
|
||||||
internal data class MessageSourceFromGroupImpl(
|
internal data class MessageSourceFromGroupImpl(
|
||||||
override val bot: Bot,
|
override val bot: Bot,
|
||||||
private val msg: MsgComm.Msg
|
private val msg: List<MsgComm.Msg>
|
||||||
) : OnlineMessageSource.Incoming.FromGroup(), MessageSourceInternal {
|
) : OnlineMessageSource.Incoming.FromGroup(), MessageSourceInternal {
|
||||||
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
||||||
override val sequenceIds: IntArray get() = intArrayOf(msg.msgHead.msgSeq)
|
override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||||
override val internalIds: IntArray get() = intArrayOf(msg.msgBody.richText.attr!!.random)
|
override val internalIds: IntArray get() = msg.mapToIntArray{ it.msgBody.richText.attr!!.random }
|
||||||
override val ids: IntArray get() = sequenceIds
|
override val ids: IntArray get() = sequenceIds
|
||||||
override val time: Int get() = msg.msgHead.msgTime
|
override val time: Int get() = msg.first().msgHead.msgTime
|
||||||
override val originalMessage: MessageChain by lazy {
|
override val originalMessage: MessageChain by lazy {
|
||||||
msg.toMessageChain(bot, groupIdOrZero = group.id, onlineSource = false)
|
msg.toMessageChain(bot, bot.id, groupIdOrZero = group.id, onlineSource = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val sender: Member by lazy {
|
override val sender: Member by lazy {
|
||||||
(bot.getGroup(
|
(bot.getGroup(
|
||||||
msg.msgHead.groupInfo?.groupCode
|
msg.first().msgHead.groupInfo?.groupCode
|
||||||
?: error("cannot find groupCode for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
?: error("cannot find groupCode for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
||||||
) as GroupImpl).run {
|
) as GroupImpl).run {
|
||||||
get(msg.msgHead.fromUin)
|
get(msg.first().msgHead.fromUin)
|
||||||
?: msg.msgBody.richText.elems.firstOrNull { it.anonGroupMsg != null }?.run {
|
?: msg.first().msgBody.richText.elems.firstOrNull { it.anonGroupMsg != null }?.run {
|
||||||
newAnonymous(anonGroupMsg!!.anonNick.encodeToString(), anonGroupMsg.anonId.encodeToBase64())
|
newAnonymous(anonGroupMsg!!.anonNick.encodeToString(), anonGroupMsg.anonId.encodeToBase64())
|
||||||
}
|
}
|
||||||
?: error("cannot find member for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
?: error("cannot find member for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
||||||
@ -163,13 +172,13 @@ internal data class MessageSourceFromGroupImpl(
|
|||||||
|
|
||||||
override fun toJceData(): ImMsgBody.SourceMsg {
|
override fun toJceData(): ImMsgBody.SourceMsg {
|
||||||
return ImMsgBody.SourceMsg(
|
return ImMsgBody.SourceMsg(
|
||||||
origSeqs = intArrayOf(msg.msgHead.msgSeq),
|
origSeqs = intArrayOf(msg.first().msgHead.msgSeq),
|
||||||
senderUin = msg.msgHead.fromUin,
|
senderUin = msg.first().msgHead.fromUin,
|
||||||
toUin = 0,
|
toUin = 0,
|
||||||
flag = 1,
|
flag = 1,
|
||||||
elems = msg.msgBody.richText.elems,
|
elems = msg.flatMap { it.msgBody.richText.elems },
|
||||||
type = 0,
|
type = 0,
|
||||||
time = msg.msgHead.msgTime,
|
time = msg.first().msgHead.msgTime,
|
||||||
pbReserve = EMPTY_BYTE_ARRAY,
|
pbReserve = EMPTY_BYTE_ARRAY,
|
||||||
srcMsg = EMPTY_BYTE_ARRAY
|
srcMsg = EMPTY_BYTE_ARRAY
|
||||||
)
|
)
|
||||||
|
@ -25,41 +25,41 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
|
|
||||||
internal class OfflineMessageSourceImplByMsg(
|
internal class OfflineMessageSourceImplByMsg(
|
||||||
// from other sources' originalMessage
|
// from other sources' originalMessage
|
||||||
val delegate: MsgComm.Msg,
|
val delegate: List<MsgComm.Msg>,
|
||||||
override val botId: Long,
|
override val botId: Long,
|
||||||
) : OfflineMessageSource(), MessageSourceInternal {
|
) : OfflineMessageSource(), MessageSourceInternal {
|
||||||
override val kind: MessageSourceKind =
|
override val kind: MessageSourceKind =
|
||||||
if (delegate.msgHead.groupInfo != null) MessageSourceKind.GROUP else MessageSourceKind.FRIEND
|
if (delegate.first().msgHead.groupInfo != null) MessageSourceKind.GROUP else MessageSourceKind.FRIEND
|
||||||
override val ids: IntArray get() = sequenceIds
|
override val ids: IntArray get() = sequenceIds
|
||||||
override val internalIds: IntArray = intArrayOf(delegate.msgHead.msgUid.toInt())
|
override val internalIds: IntArray = delegate.mapToIntArray { it.msgHead.msgUid.toInt() }
|
||||||
override val time: Int
|
override val time: Int
|
||||||
get() = delegate.msgHead.msgTime
|
get() = delegate.first().msgHead.msgTime
|
||||||
override val fromId: Long
|
override val fromId: Long
|
||||||
get() = delegate.msgHead.fromUin
|
get() = delegate.first().msgHead.fromUin
|
||||||
override val targetId: Long
|
override val targetId: Long
|
||||||
get() = delegate.msgHead.groupInfo?.groupCode ?: delegate.msgHead.toUin
|
get() = delegate.first().msgHead.groupInfo?.groupCode ?: delegate.first().msgHead.toUin
|
||||||
override val originalMessage: MessageChain by lazy {
|
override val originalMessage: MessageChain by lazy {
|
||||||
delegate.toMessageChain(
|
delegate.toMessageChain(
|
||||||
null,
|
null,
|
||||||
botId,
|
botId,
|
||||||
groupIdOrZero = delegate.msgHead.groupInfo?.groupCode ?: 0,
|
groupIdOrZero = delegate.first().msgHead.groupInfo?.groupCode ?: 0,
|
||||||
onlineSource = false,
|
onlineSource = false,
|
||||||
isTemp = delegate.msgHead.c2cTmpMsgHead != null
|
isTemp = delegate.first().msgHead.c2cTmpMsgHead != null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override val sequenceIds: IntArray = intArrayOf(delegate.msgHead.msgSeq)
|
override val sequenceIds: IntArray = delegate.mapToIntArray { it.msgHead.msgSeq }
|
||||||
|
|
||||||
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
||||||
|
|
||||||
override fun toJceData(): ImMsgBody.SourceMsg {
|
override fun toJceData(): ImMsgBody.SourceMsg {
|
||||||
return ImMsgBody.SourceMsg(
|
return ImMsgBody.SourceMsg(
|
||||||
origSeqs = intArrayOf(delegate.msgHead.msgSeq),
|
origSeqs = delegate.mapToIntArray { it.msgHead.msgSeq },
|
||||||
senderUin = delegate.msgHead.fromUin,
|
senderUin = delegate.first().msgHead.fromUin,
|
||||||
toUin = 0,
|
toUin = 0,
|
||||||
flag = 1,
|
flag = 1,
|
||||||
elems = delegate.msgBody.richText.elems,
|
elems = delegate.flatMap { it.msgBody.richText.elems },
|
||||||
type = 0,
|
type = 0,
|
||||||
time = delegate.msgHead.msgTime,
|
time = delegate.first().msgHead.msgTime,
|
||||||
pbReserve = EMPTY_BYTE_ARRAY,
|
pbReserve = EMPTY_BYTE_ARRAY,
|
||||||
srcMsg = EMPTY_BYTE_ARRAY
|
srcMsg = EMPTY_BYTE_ARRAY
|
||||||
)
|
)
|
||||||
|
@ -30,7 +30,7 @@ import net.mamoe.mirai.internal.contact.*
|
|||||||
import net.mamoe.mirai.internal.network.protocol.data.jce.StTroopNum
|
import net.mamoe.mirai.internal.network.protocol.data.jce.StTroopNum
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgSvc
|
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgSvc
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.*
|
import net.mamoe.mirai.internal.network.protocol.packet.*
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.KnownPacketFactories.PacketFactoryIllegalState10008Exception
|
import net.mamoe.mirai.internal.network.protocol.packet.KnownPacketFactories.PacketFactoryIllegalStateException
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.GroupInfoImpl
|
import net.mamoe.mirai.internal.network.protocol.packet.chat.GroupInfoImpl
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbGetMsg
|
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbGetMsg
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
|
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
|
||||||
@ -152,17 +152,28 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
|
|||||||
logger.info { "Connected to server $host:$port" }
|
logger.info { "Connected to server $host:$port" }
|
||||||
startPacketReceiverJobOrKill(CancellationException("relogin", cause))
|
startPacketReceiverJobOrKill(CancellationException("relogin", cause))
|
||||||
|
|
||||||
|
fun LoginSolver?.notnull(): LoginSolver {
|
||||||
|
checkNotNull(this) {
|
||||||
|
"No LoginSolver found. Please provide by BotConfiguration.loginSolver. " +
|
||||||
|
"For example use `BotFactory.newBot(...) { loginSolver = yourLoginSolver}` in Kotlin, " +
|
||||||
|
"use `BotFactory.newBot(..., new BotConfiguration() {{ setLoginSolver(yourLoginSolver) }})` in Java."
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loginSolverNotNull() = bot.configuration.loginSolver.notnull()
|
||||||
|
|
||||||
var response: WtLogin.Login.LoginPacketResponse = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
|
var response: WtLogin.Login.LoginPacketResponse = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
|
||||||
mainloop@ while (true) {
|
mainloop@ while (true) {
|
||||||
when (response) {
|
when (response) {
|
||||||
is WtLogin.Login.LoginPacketResponse.UnsafeLogin -> {
|
is WtLogin.Login.LoginPacketResponse.UnsafeLogin -> {
|
||||||
bot.configuration.loginSolver.onSolveUnsafeDeviceLoginVerify(bot, response.url)
|
loginSolverNotNull().onSolveUnsafeDeviceLoginVerify(bot, response.url)
|
||||||
response = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
|
response = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
|
||||||
}
|
}
|
||||||
|
|
||||||
is WtLogin.Login.LoginPacketResponse.Captcha -> when (response) {
|
is WtLogin.Login.LoginPacketResponse.Captcha -> when (response) {
|
||||||
is WtLogin.Login.LoginPacketResponse.Captcha.Picture -> {
|
is WtLogin.Login.LoginPacketResponse.Captcha.Picture -> {
|
||||||
var result = bot.configuration.loginSolver.onSolvePicCaptcha(bot, response.data)
|
var result = loginSolverNotNull().onSolvePicCaptcha(bot, response.data)
|
||||||
if (result == null || result.length != 4) {
|
if (result == null || result.length != 4) {
|
||||||
//refresh captcha
|
//refresh captcha
|
||||||
result = "ABCD"
|
result = "ABCD"
|
||||||
@ -172,7 +183,7 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
|
|||||||
continue@mainloop
|
continue@mainloop
|
||||||
}
|
}
|
||||||
is WtLogin.Login.LoginPacketResponse.Captcha.Slider -> {
|
is WtLogin.Login.LoginPacketResponse.Captcha.Slider -> {
|
||||||
val ticket = bot.configuration.loginSolver.onSolveSliderCaptcha(bot, response.url).orEmpty()
|
val ticket = loginSolverNotNull().onSolveSliderCaptcha(bot, response.url).orEmpty()
|
||||||
response = WtLogin.Login.SubCommand2.SubmitSliderCaptcha(bot.client, ticket).sendAndExpect()
|
response = WtLogin.Login.SubCommand2.SubmitSliderCaptcha(bot.client, ticket).sendAndExpect()
|
||||||
continue@mainloop
|
continue@mainloop
|
||||||
}
|
}
|
||||||
@ -443,9 +454,9 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
|
|||||||
input.use {
|
input.use {
|
||||||
try {
|
try {
|
||||||
parsePacket(it)
|
parsePacket(it)
|
||||||
} catch (e: PacketFactoryIllegalState10008Exception) {
|
} catch (e: PacketFactoryIllegalStateException) {
|
||||||
logger.warning { "Network force offline: ${e.message}" }
|
logger.warning { "Network force offline: ${e.message}" }
|
||||||
bot.launch { BotOfflineEvent.PacketFactory10008(bot, e).broadcast() }
|
bot.launch { BotOfflineEvent.PacketFactoryErrorCode(e.code, bot, e).broadcast() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,7 +173,8 @@ internal object KnownPacketFactories {
|
|||||||
?: IncomingFactories.firstOrNull { it.receivingCommandName == commandName }
|
?: IncomingFactories.firstOrNull { it.receivingCommandName == commandName }
|
||||||
}
|
}
|
||||||
|
|
||||||
class PacketFactoryIllegalState10008Exception @JvmOverloads constructor(
|
class PacketFactoryIllegalStateException @JvmOverloads constructor(
|
||||||
|
val code: Int,
|
||||||
override val message: String? = null,
|
override val message: String? = null,
|
||||||
override val cause: Throwable? = null
|
override val cause: Throwable? = null
|
||||||
) : RuntimeException()
|
) : RuntimeException()
|
||||||
@ -310,8 +311,9 @@ internal object KnownPacketFactories {
|
|||||||
|
|
||||||
val returnCode = readInt()
|
val returnCode = readInt()
|
||||||
check(returnCode == 0) {
|
check(returnCode == 0) {
|
||||||
if (returnCode == -10008) { // https://github.com/mamoe/mirai/issues/470
|
if (returnCode <= -10000) {
|
||||||
throw PacketFactoryIllegalState10008Exception("returnCode = $returnCode")
|
// https://github.com/mamoe/mirai/issues/470
|
||||||
|
throw PacketFactoryIllegalStateException(returnCode, "returnCode = $returnCode")
|
||||||
} else "returnCode = $returnCode"
|
} else "returnCode = $returnCode"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
|
|||||||
import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
|
import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
|
||||||
import net.mamoe.mirai.message.data.MessageChain
|
import net.mamoe.mirai.message.data.MessageChain
|
||||||
import net.mamoe.mirai.message.data.PttMessage
|
import net.mamoe.mirai.message.data.PttMessage
|
||||||
|
import net.mamoe.mirai.message.data.Voice
|
||||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||||
import kotlin.contracts.InvocationKind
|
import kotlin.contracts.InvocationKind
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
@ -145,7 +146,14 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
|
|||||||
boolValid = true,
|
boolValid = true,
|
||||||
fileSize = fileSize.toInt(),
|
fileSize = fileSize.toInt(),
|
||||||
fileType = 4,
|
fileType = 4,
|
||||||
pbReserve = byteArrayOf(0)
|
pbReserve = byteArrayOf(0),
|
||||||
|
format = let {
|
||||||
|
if (it is Voice) {
|
||||||
|
it.codec
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -61,19 +61,23 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("Onlin
|
|||||||
pbPushMsg.msg.msgHead.msgSeq
|
pbPushMsg.msg.msgHead.msgSeq
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val group =
|
||||||
|
bot.getGroup(pbPushMsg.msg.msgHead.groupInfo!!.groupCode) as GroupImpl? ?: return null // 机器人还正在进群
|
||||||
|
val msgs = group.groupPkgMsgParsingCache.put(pbPushMsg)
|
||||||
|
if (msgs.isEmpty()) return null
|
||||||
|
|
||||||
var extraInfo: ImMsgBody.ExtraInfo? = null
|
var extraInfo: ImMsgBody.ExtraInfo? = null
|
||||||
var anonymous: ImMsgBody.AnonymousGroupMsg? = null
|
var anonymous: ImMsgBody.AnonymousGroupMsg? = null
|
||||||
|
|
||||||
for (elem in pbPushMsg.msg.msgBody.richText.elems) {
|
for (msg in msgs) {
|
||||||
|
for (elem in msg.msg.msgBody.richText.elems) {
|
||||||
when {
|
when {
|
||||||
elem.extraInfo != null -> extraInfo = elem.extraInfo
|
elem.extraInfo != null -> extraInfo = elem.extraInfo
|
||||||
elem.anonGroupMsg != null -> anonymous = elem.anonGroupMsg
|
elem.anonGroupMsg != null -> anonymous = elem.anonGroupMsg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val group =
|
|
||||||
bot.getGroup(pbPushMsg.msg.msgHead.groupInfo!!.groupCode) as GroupImpl? ?: return null // 机器人还正在进群
|
|
||||||
val sender = if (anonymous != null) {
|
val sender = if (anonymous != null) {
|
||||||
group.newAnonymous(anonymous.anonNick.encodeToString(), anonymous.anonId.encodeToBase64())
|
group.newAnonymous(anonymous.anonNick.encodeToString(), anonymous.anonId.encodeToBase64())
|
||||||
} else {
|
} else {
|
||||||
@ -107,7 +111,7 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("Onlin
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
sender = sender,
|
sender = sender,
|
||||||
message = pbPushMsg.msg.toMessageChain(bot, groupIdOrZero = group.id, onlineSource = true),
|
message = msgs.toMessageChain(bot, groupIdOrZero = group.id, onlineSource = true),
|
||||||
permission = when {
|
permission = when {
|
||||||
flags and 16 != 0 -> MemberPermission.ADMINISTRATOR
|
flags and 16 != 0 -> MemberPermission.ADMINISTRATOR
|
||||||
flags and 8 != 0 -> MemberPermission.OWNER
|
flags and 8 != 0 -> MemberPermission.OWNER
|
||||||
|
@ -44,6 +44,7 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.TroopTips0x857
|
|||||||
import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory
|
import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.buildResponseUniPacket
|
import net.mamoe.mirai.internal.network.protocol.packet.buildResponseUniPacket
|
||||||
|
import net.mamoe.mirai.internal.utils.*
|
||||||
import net.mamoe.mirai.internal.utils._miraiContentToString
|
import net.mamoe.mirai.internal.utils._miraiContentToString
|
||||||
import net.mamoe.mirai.internal.utils.encodeToString
|
import net.mamoe.mirai.internal.utils.encodeToString
|
||||||
import net.mamoe.mirai.internal.utils.io.ProtoBuf
|
import net.mamoe.mirai.internal.utils.io.ProtoBuf
|
||||||
@ -53,6 +54,7 @@ import net.mamoe.mirai.internal.utils.read
|
|||||||
import net.mamoe.mirai.internal.utils.toUHexString
|
import net.mamoe.mirai.internal.utils.toUHexString
|
||||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||||
import net.mamoe.mirai.utils.debug
|
import net.mamoe.mirai.utils.debug
|
||||||
|
import net.mamoe.mirai.utils.mapToIntArray
|
||||||
|
|
||||||
|
|
||||||
//0C 01 B1 89 BE 09 5E 3D 72 A6 00 01 73 68 FC 06 00 00 00 3C
|
//0C 01 B1 89 BE 09 5E 3D 72 A6 00 01 73 68 FC 06 00 00 00 3C
|
||||||
@ -243,7 +245,7 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
|
|||||||
val grayTip = readProtoBuf(TroopTips0x857.NotifyMsgBody.serializer()).optGeneralGrayTip
|
val grayTip = readProtoBuf(TroopTips0x857.NotifyMsgBody.serializer()).optGeneralGrayTip
|
||||||
when (grayTip?.templId) {
|
when (grayTip?.templId) {
|
||||||
//戳一戳
|
//戳一戳
|
||||||
10043L, 1134L, 1135L, 1136L -> {
|
10043L, 1133L, 1132L, 1134L, 1135L, 1136L -> {
|
||||||
//预置数据,服务器将不会提供己方已知消息
|
//预置数据,服务器将不会提供己方已知消息
|
||||||
var action = ""
|
var action = ""
|
||||||
var from: Member = group.botAsMember
|
var from: Member = group.botAsMember
|
||||||
@ -357,22 +359,21 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
|
|||||||
val operator =
|
val operator =
|
||||||
if (recallReminder.uin == bot.id) group.botAsMember
|
if (recallReminder.uin == bot.id) group.botAsMember
|
||||||
else group[recallReminder.uin] ?: return@lambda732 emptySequence()
|
else group[recallReminder.uin] ?: return@lambda732 emptySequence()
|
||||||
|
val firstPkg = recallReminder.recalledMsgList.firstOrNull() ?: return@lambda732 emptySequence()
|
||||||
|
|
||||||
return@lambda732 recallReminder.recalledMsgList.asSequence().mapNotNull { pkg ->
|
return@lambda732 when {
|
||||||
when {
|
firstPkg.authorUin == bot.id && operator.id == bot.id -> emptySequence()
|
||||||
pkg.authorUin == bot.id && operator.id == bot.id -> null
|
else -> sequenceOf(
|
||||||
else -> {
|
|
||||||
MessageRecallEvent.GroupRecall(
|
MessageRecallEvent.GroupRecall(
|
||||||
bot,
|
bot,
|
||||||
pkg.authorUin,
|
firstPkg.authorUin,
|
||||||
intArrayOf(pkg.seq),
|
recallReminder.recalledMsgList.mapToIntArray { it.seq },
|
||||||
intArrayOf(pkg.msgRandom),
|
recallReminder.recalledMsgList.mapToIntArray { it.msgRandom },
|
||||||
pkg.time,
|
firstPkg.time,
|
||||||
operator,
|
operator,
|
||||||
group
|
group
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -422,6 +423,8 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
|
|||||||
@ProtoNumber(7) val pkgNum: Int, // 1
|
@ProtoNumber(7) val pkgNum: Int, // 1
|
||||||
@ProtoNumber(8) val pkgIndex: Int, // 0
|
@ProtoNumber(8) val pkgIndex: Int, // 0
|
||||||
@ProtoNumber(9) val devSeq: Int, // 0
|
@ProtoNumber(9) val devSeq: Int, // 0
|
||||||
|
@ProtoNumber(10) val unknown1: Int = 0, // ? 补 id
|
||||||
|
@ProtoNumber(11) val unknown2: Int = 0, // ?
|
||||||
@ProtoNumber(12) val flag: Int // 1
|
@ProtoNumber(12) val flag: Int // 1
|
||||||
) : ProtoBuf
|
) : ProtoBuf
|
||||||
|
|
||||||
@ -441,7 +444,7 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
|
|||||||
messageIds = intArrayOf(it.srcId),
|
messageIds = intArrayOf(it.srcId),
|
||||||
messageInternalIds = intArrayOf(it.srcInternalId),
|
messageInternalIds = intArrayOf(it.srcInternalId),
|
||||||
messageTime = it.time,
|
messageTime = it.time,
|
||||||
operator = it.fromUin
|
operatorId = it.fromUin
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -509,7 +512,7 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
|
|||||||
val body = vProtobuf.loadAs(Submsgtype0x122.Submsgtype0x122.MsgBody.serializer())
|
val body = vProtobuf.loadAs(Submsgtype0x122.Submsgtype0x122.MsgBody.serializer())
|
||||||
when (body.templId) {
|
when (body.templId) {
|
||||||
//戳一戳
|
//戳一戳
|
||||||
1134L, 1135L, 1136L, 10043L -> {
|
1132L, 1133L, 1134L, 1135L, 1136L, 10043L -> {
|
||||||
//预置数据,服务器将不会提供己方已知消息
|
//预置数据,服务器将不会提供己方已知消息
|
||||||
var from: Friend = bot.asFriend
|
var from: Friend = bot.asFriend
|
||||||
var action = ""
|
var action = ""
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019-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.internal.utils
|
||||||
|
|
||||||
|
import kotlinx.atomicfu.locks.withLock
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgOnlinePush
|
||||||
|
import net.mamoe.mirai.utils.currentTimeMillis
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
|
||||||
|
internal class GroupPkgMsgParsingCache {
|
||||||
|
class PkgMsg(
|
||||||
|
val size: Int,
|
||||||
|
val divSeq: Int,
|
||||||
|
val data: MutableMap<Int, MsgOnlinePush.PbPushMsg>,
|
||||||
|
) {
|
||||||
|
val createTime = currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val deque = ArrayList<PkgMsg>(16)
|
||||||
|
private val accessLock = ReentrantLock()
|
||||||
|
private fun clearInvalid() {
|
||||||
|
deque.removeIf {
|
||||||
|
currentTimeMillis() - it.createTime > 10000L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(msg: MsgOnlinePush.PbPushMsg): List<MsgOnlinePush.PbPushMsg> {
|
||||||
|
val head = msg.msg.contentHead ?: return listOf(msg)
|
||||||
|
val size = head.pkgNum
|
||||||
|
if (size < 2) return listOf(msg)
|
||||||
|
accessLock.withLock {
|
||||||
|
clearInvalid()
|
||||||
|
val seq = head.divSeq
|
||||||
|
val index = head.pkgIndex
|
||||||
|
val pkgMsg = deque.find {
|
||||||
|
it.divSeq == seq
|
||||||
|
} ?: PkgMsg(size, seq, mutableMapOf()).also { deque.add(it) }
|
||||||
|
pkgMsg.data[index] = msg
|
||||||
|
if (pkgMsg.data.size == pkgMsg.size) {
|
||||||
|
deque.removeIf { it.divSeq == seq }
|
||||||
|
return pkgMsg.data.entries.asSequence()
|
||||||
|
.sortedBy { it.key }
|
||||||
|
.map { it.value }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user