Merge remote-tracking branch 'origin/dev' into anonymous

# Conflicts:
#	mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt
This commit is contained in:
Karlatemp 2020-12-20 15:53:06 +08:00
commit de3fd9d698
No known key found for this signature in database
GPG Key ID: 21FBDDF664FF06F8
26 changed files with 433 additions and 231 deletions

View File

@ -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() }
} }

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
} }
} }
} }

View File

@ -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(

View File

@ -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] 实例

View File

@ -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() {

View File

@ -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")
/** /**

View File

@ -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)

View File

@ -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() } })
} }
} }
} }

View File

@ -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
} }
} }
} }

View File

@ -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

View File

@ -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(

View File

@ -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()
} }

View File

@ -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()
} }

View File

@ -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
) )

View File

@ -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
) )

View File

@ -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() }
} }
} }
} }

View File

@ -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"
} }

View File

@ -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
}
}
) )
} }
) )

View File

@ -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

View File

@ -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 = ""

View File

@ -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()
}
}
}