mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-18 09:32:23 +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 ->
|
||||
target.compilations.all { kotlinOptions.jvmTarget = "1.8" }
|
||||
target.compilations.all {
|
||||
kotlinOptions.jvmTarget = "1.8"
|
||||
kotlinOptions.languageVersion = "1.4"
|
||||
}
|
||||
target.testRuns["test"].executionTask.configure { useJUnitPlatform() }
|
||||
}
|
||||
|
||||
|
@ -119,6 +119,9 @@ inline fun Project.configurePublishing(
|
||||
setPublications("mavenJava")
|
||||
setConfigurations("archives")
|
||||
|
||||
publish = true
|
||||
override = true
|
||||
|
||||
pkg.apply {
|
||||
repo = bintrayRepo
|
||||
name = bintrayPkgName
|
||||
|
@ -26,10 +26,14 @@ mirai 已经可以用于各项工作,但我们并不保证完全的稳定。
|
||||
阅读 [项目整体架构](mirai.md#%E9%A1%B9%E7%9B%AE%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84).
|
||||
|
||||
**警告:** mirai 开发者自愿花费其休息时间,无偿维护 mirai 系列项目,**但没有义务提供任何方面的帮助**。
|
||||
使用者应秉持对开发者无私贡献的尊重,而不应该期望得到帮助。
|
||||
使用者应秉持对开发者无私贡献的尊重,而不应该期望得到帮助。
|
||||
|
||||
### 启动 Mirai 时遇到 java.security.NoSuchProviderException: JCE cannot authenticate the provider BC
|
||||
请更换 OpenJDK。该问题是由于BouncyCastle在重打包后丢失Jar签名引起的。
|
||||
~~多发于Oracle JDK~~
|
||||
|
||||
## 开发相关
|
||||
|
||||
> 欢迎帮助维护这个问题列表: 提交 PR 或者 issue.
|
||||
|
||||
... 待补充
|
||||
... 待补充
|
||||
|
@ -10,7 +10,7 @@
|
||||
publishing {
|
||||
repositories {
|
||||
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 {
|
||||
username = upload.Bintray.getUser(project)
|
||||
|
@ -73,7 +73,8 @@ public sealed class BotOfflineEvent : BotEvent, AbstractEvent() {
|
||||
* 因 returnCode = -10008 等原因掉线
|
||||
*/
|
||||
@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 cause: Throwable
|
||||
) : BotOfflineEvent(), Packet, BotPassiveEvent, CauseAware
|
||||
|
@ -262,6 +262,11 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
||||
*/
|
||||
public abstract val authorId: Long
|
||||
|
||||
/**
|
||||
* 消息原发送人, 为 `null` 表示原消息发送人已经不是 bot 的好友或已经被移出群。
|
||||
*/
|
||||
public abstract val author: UserOrBot?
|
||||
|
||||
/**
|
||||
* 消息 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 messageIds: IntArray,
|
||||
public override val messageInternalIds: IntArray,
|
||||
@ -290,11 +295,25 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
||||
/**
|
||||
* 撤回操作人, 好友的 [User.id]
|
||||
*/
|
||||
public val operator: Long
|
||||
public val operatorId: Long
|
||||
) : MessageRecallEvent(), Packet {
|
||||
public override val authorId: Long
|
||||
get() = bot.id
|
||||
@Deprecated("Binary compatibility.", level = DeprecationLevel.HIDDEN)
|
||||
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 {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
@ -305,7 +324,7 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
||||
if (!messageIds.contentEquals(other.messageIds)) return false
|
||||
if (!messageInternalIds.contentEquals(other.messageInternalIds)) return false
|
||||
if (messageTime != other.messageTime) return false
|
||||
if (operator != other.operator) return false
|
||||
if (operatorId != other.operatorId) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@ -315,7 +334,7 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
||||
result = 31 * result + messageIds.contentHashCode()
|
||||
result = 31 * result + messageInternalIds.contentHashCode()
|
||||
result = 31 * result + messageTime
|
||||
result = 31 * result + operator.hashCode()
|
||||
result = 31 * result + operatorId.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ -335,6 +354,8 @@ public sealed class MessageRecallEvent : BotEvent, AbstractEvent() {
|
||||
public override val operator: Member?,
|
||||
public override val group: Group
|
||||
) : MessageRecallEvent(), GroupOperableEvent, Packet {
|
||||
override val author: NormalMember? get() = group[authorId]
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
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
|
||||
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
|
||||
// no need
|
||||
|
||||
|
@ -14,9 +14,9 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.mamoe.mirai.event.*
|
||||
import net.mamoe.mirai.event.events.BotEvent
|
||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.reflect.KClass
|
||||
@ -26,7 +26,7 @@ internal fun <L : Listener<E>, E : Event> KClass<out E>.subscribeInternal(listen
|
||||
with(GlobalEventListeners[listener.priority]) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val node = ListenerRegistry(listener as Listener<Event>, this@subscribeInternal)
|
||||
addLast(node)
|
||||
add(node)
|
||||
listener.invokeOnCompletion {
|
||||
this.remove(node)
|
||||
}
|
||||
@ -98,18 +98,18 @@ internal class ListenerRegistry(
|
||||
|
||||
|
||||
internal object GlobalEventListeners {
|
||||
private val ALL_LEVEL_REGISTRIES: Map<EventPriority, LockFreeLinkedList<ListenerRegistry>>
|
||||
private val ALL_LEVEL_REGISTRIES: Map<EventPriority, ConcurrentLinkedQueue<ListenerRegistry>>
|
||||
|
||||
init {
|
||||
val map =
|
||||
EnumMap<Listener.EventPriority, LockFreeLinkedList<ListenerRegistry>>(Listener.EventPriority::class.java)
|
||||
EnumMap<Listener.EventPriority, ConcurrentLinkedQueue<ListenerRegistry>>(Listener.EventPriority::class.java)
|
||||
EventPriority.values().forEach {
|
||||
map[it] = LockFreeLinkedList()
|
||||
map[it] = ConcurrentLinkedQueue()
|
||||
}
|
||||
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]!!
|
||||
}
|
||||
|
||||
@ -121,54 +121,56 @@ internal suspend inline fun AbstractEvent.broadcastInternal() {
|
||||
callAndRemoveIfRequired(this@broadcastInternal)
|
||||
}
|
||||
|
||||
internal inline fun <E, T : Iterable<E>> T.forEach0(block: T.(E) -> Unit) {
|
||||
forEach { block(it) }
|
||||
}
|
||||
|
||||
@Suppress("DuplicatedCode")
|
||||
internal suspend inline fun <E : AbstractEvent> callAndRemoveIfRequired(
|
||||
event: E
|
||||
) {
|
||||
for (p in Listener.EventPriority.prioritiesExcludedMonitor) {
|
||||
GlobalEventListeners[p].forEachNode { registeredRegistryNode ->
|
||||
GlobalEventListeners[p].forEach0 { registeredRegistry ->
|
||||
if (event.isIntercepted) {
|
||||
return
|
||||
}
|
||||
val listenerRegistry = registeredRegistryNode.nodeValue
|
||||
if (!listenerRegistry.type.isInstance(event)) return@forEachNode
|
||||
val listener = listenerRegistry.listener
|
||||
if (!registeredRegistry.type.isInstance(event)) return@forEach0
|
||||
val listener = registeredRegistry.listener
|
||||
when (listener.concurrencyKind) {
|
||||
Listener.ConcurrencyKind.LOCKED -> {
|
||||
(listener as Handler).lock!!.withLock {
|
||||
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
||||
removeNode(registeredRegistryNode)
|
||||
remove(registeredRegistry)
|
||||
}
|
||||
}
|
||||
}
|
||||
Listener.ConcurrencyKind.CONCURRENT -> {
|
||||
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
||||
removeNode(registeredRegistryNode)
|
||||
remove(registeredRegistry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
coroutineScope {
|
||||
GlobalEventListeners[EventPriority.MONITOR].forEachNode { registeredRegistryNode ->
|
||||
GlobalEventListeners[EventPriority.MONITOR].forEach0 { registeredRegistry ->
|
||||
if (event.isIntercepted) {
|
||||
return@coroutineScope
|
||||
}
|
||||
val listenerRegistry = registeredRegistryNode.nodeValue
|
||||
if (!listenerRegistry.type.isInstance(event)) return@forEachNode
|
||||
val listener = listenerRegistry.listener
|
||||
if (!registeredRegistry.type.isInstance(event)) return@forEach0
|
||||
val listener = registeredRegistry.listener
|
||||
launch {
|
||||
when (listener.concurrencyKind) {
|
||||
Listener.ConcurrencyKind.LOCKED -> {
|
||||
(listener as Handler).lock!!.withLock {
|
||||
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
||||
removeNode(registeredRegistryNode)
|
||||
remove(registeredRegistry)
|
||||
}
|
||||
}
|
||||
}
|
||||
Listener.ConcurrencyKind.CONCURRENT -> {
|
||||
if (listener.onEvent(event) == ListeningStatus.STOPPED) {
|
||||
removeNode(registeredRegistryNode)
|
||||
remove(registeredRegistry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,11 +56,12 @@ import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
* @see nextMessage 挂起协程并等待下一条消息
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@BuilderInference
|
||||
public suspend inline fun <reified T : MessageEvent> T.whileSelectMessages(
|
||||
timeoutMillis: Long = -1,
|
||||
filterContext: Boolean = true,
|
||||
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)
|
||||
|
||||
/**
|
||||
@ -68,11 +69,12 @@ public suspend inline fun <reified T : MessageEvent> T.whileSelectMessages(
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
@JvmName("selectMessages1")
|
||||
@BuilderInference
|
||||
public suspend inline fun <reified T : MessageEvent> T.selectMessagesUnit(
|
||||
timeoutMillis: Long = -1,
|
||||
filterContext: Boolean = true,
|
||||
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)
|
||||
|
||||
|
||||
@ -97,12 +99,12 @@ public suspend inline fun <reified T : MessageEvent> T.selectMessagesUnit(
|
||||
* @see nextMessage 挂起协程并等待下一条消息
|
||||
*/
|
||||
@Suppress("unused") // false positive
|
||||
// @BuilderInference // https://youtrack.jetbrains.com/issue/KT-37716
|
||||
@BuilderInference
|
||||
public suspend inline fun <reified T : MessageEvent, R> T.selectMessages(
|
||||
timeoutMillis: Long = -1,
|
||||
filterContext: Boolean = true,
|
||||
priority: Listener.EventPriority = EventPriority.MONITOR,
|
||||
// @BuilderInference
|
||||
@BuilderInference
|
||||
crossinline selectBuilder: @MessageDsl MessageSelectBuilder<T, R>.() -> Unit
|
||||
): R =
|
||||
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] 实例
|
||||
*/
|
||||
@JvmSynthetic
|
||||
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] 实例
|
||||
|
@ -39,6 +39,7 @@ public class Voice @MiraiInternalApi constructor(
|
||||
public override val fileName: String,
|
||||
public override val md5: ByteArray,
|
||||
public override val fileSize: Long,
|
||||
public val codec: Int = 0,
|
||||
private val _url: String
|
||||
) : PttMessage() {
|
||||
|
||||
|
@ -52,7 +52,7 @@ public class RetryLaterException @MiraiInternalApi constructor() :
|
||||
* 无标准输入或 Kotlin 不支持此输入.
|
||||
*/
|
||||
public class NoStandardInputForCaptchaException @MiraiInternalApi constructor(
|
||||
public override val cause: Throwable?
|
||||
public override val cause: Throwable? = null
|
||||
) : 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 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
|
||||
@ -115,6 +124,7 @@ public open class BotConfiguration { // open for Java
|
||||
Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
prettyPrint = true
|
||||
}
|
||||
}.getOrElse { Json {} }
|
||||
|
||||
@ -133,6 +143,7 @@ public open class BotConfiguration { // open for Java
|
||||
*
|
||||
* @see deviceInfo
|
||||
*/
|
||||
@ConfigurationDsl
|
||||
public fun loadDeviceInfoJson(json: String) {
|
||||
deviceInfo = {
|
||||
this.json.decodeFromString(DeviceInfo.serializer(), json)
|
||||
|
@ -20,6 +20,8 @@ import kotlinx.coroutines.withContext
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.LoginFailedException
|
||||
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.BufferedImage
|
||||
import java.io.File
|
||||
@ -29,6 +31,9 @@ import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* 验证码, 设备锁解决器
|
||||
*
|
||||
* @see Default
|
||||
* @see BotConfiguration.loginSolver
|
||||
*/
|
||||
public abstract class LoginSolver {
|
||||
/**
|
||||
@ -61,109 +66,134 @@ public abstract class LoginSolver {
|
||||
public abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
|
||||
|
||||
public companion object {
|
||||
public val Default: LoginSolver = kotlin.run {
|
||||
if (WindowHelperJvm.isDesktopSupported) {
|
||||
SwingSolver
|
||||
} else {
|
||||
DefaultLoginSolver({ readLine() ?: throw NoStandardInputForCaptchaException(null) })
|
||||
}
|
||||
/**
|
||||
* 当前平台默认的 [LoginSolver]。
|
||||
*
|
||||
* 检测策略:
|
||||
* 1. 检测 `android.util.Log`, 如果存在, 返回 `null`.
|
||||
* 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 [StandardCharImageLoginSolver]
|
||||
* 3. 检测 JVM 桌面环境, 若支持, 返回 [SwingSolver]
|
||||
* 4. 返回 [StandardCharImageLoginSolver]
|
||||
*
|
||||
* @return [SwingSolver] 或 [StandardCharImageLoginSolver] 或 `null`
|
||||
*/
|
||||
@JvmField
|
||||
public val Default: LoginSolver? = when (WindowHelperJvm.platformKind) {
|
||||
WindowHelperJvm.PlatformKind.ANDROID -> null
|
||||
WindowHelperJvm.PlatformKind.SWING -> SwingSolver
|
||||
WindowHelperJvm.PlatformKind.CLI -> StandardCharImageLoginSolver()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 自动选择 [SwingSolver] 或 [StandardCharImageLoginSolver]
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
public class DefaultLoginSolver(
|
||||
public val input: suspend () -> String,
|
||||
overrideLogger: MiraiLogger? = null
|
||||
) : LoginSolver() {
|
||||
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? {
|
||||
return delegate.onSolveSliderCaptcha(bot, url)
|
||||
}
|
||||
|
||||
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
|
||||
return delegate.onSolveUnsafeDeviceLoginVerify(bot, url)
|
||||
@Suppress("unused")
|
||||
@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")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用字符图片展示验证码, 使用 [input] 获取输入, 使用 [overrideLogger] 输出
|
||||
* CLI 环境 [LoginSolver]. 将验证码图片转为字符画并通过 `output` 输出, [input] 获取用户输入.
|
||||
*
|
||||
* 使用字符图片展示验证码, 使用 [input] 获取输入, 使用 [loggerSupplier] 输出
|
||||
*
|
||||
* @see createBlocking
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
public class StandardCharImageLoginSolver(
|
||||
input: suspend () -> String,
|
||||
public class StandardCharImageLoginSolver @JvmOverloads constructor(
|
||||
input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
|
||||
/**
|
||||
* 为 `null` 时使用 [Bot.logger]
|
||||
*/
|
||||
private val overrideLogger: MiraiLogger? = null
|
||||
private val loggerSupplier: (bot: Bot) -> MiraiLogger = { it.logger }
|
||||
) : LoginSolver() {
|
||||
public constructor(
|
||||
input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
|
||||
overrideLogger: MiraiLogger?
|
||||
) : this(input, { overrideLogger ?: it.logger })
|
||||
|
||||
private val input: suspend () -> String = suspend {
|
||||
withContext(Dispatchers.IO) { input() }
|
||||
}
|
||||
|
||||
override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock {
|
||||
val logger = overrideLogger ?: bot.logger
|
||||
val logger = loggerSupplier(bot)
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
withContext(Dispatchers.IO) {
|
||||
val tempFile: File = File.createTempFile("tmp", ".png").apply { deleteOnExit() }
|
||||
tempFile.createNewFile()
|
||||
logger.info("需要图片验证码登录, 验证码为 4 字母")
|
||||
logger.info { "[PicCaptcha] 需要图片验证码登录, 验证码为 4 字母" }
|
||||
logger.info { "[PicCaptcha] Picture captcha required. Captcha consists of 4 letters." }
|
||||
try {
|
||||
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) {
|
||||
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 {
|
||||
val img = ImageIO.read(it)
|
||||
val img = ImageIO.read(stream)
|
||||
if (img == null) {
|
||||
logger.info("无法创建字符图片. 请查看文件")
|
||||
logger.warning { "[PicCaptcha] 无法创建字符图片. 请查看文件" }
|
||||
logger.warning { "[PicCaptcha] Failed to create char-image. Please see the file." }
|
||||
} else {
|
||||
logger.info("\n" + img.createCharImg())
|
||||
logger.info { "[PicCaptcha] \n" + img.createCharImg() }
|
||||
}
|
||||
} 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 {
|
||||
logger.info("正在提交[$it]中...")
|
||||
logger.info { "[PicCaptcha] 正在提交 $it..." }
|
||||
logger.info { "[PicCaptcha] Submitting $it..." }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String = loginSolverLock.withLock {
|
||||
val logger = overrideLogger ?: bot.logger
|
||||
logger.info("需要滑动验证码")
|
||||
logger.info("请在任意浏览器中打开以下链接并完成验证码. ")
|
||||
logger.info("完成后请输入任意字符 ")
|
||||
val logger = loggerSupplier(bot)
|
||||
logger.info { "[SliderCaptcha] 需要滑动验证码, 请在任意浏览器中打开以下链接并完成验证码, 完成后请输入任意字符." }
|
||||
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(url)
|
||||
return input().also {
|
||||
logger.info("正在提交中...")
|
||||
logger.info { "[SliderCaptcha] 正在提交中..." }
|
||||
logger.info { "[SliderCaptcha] Submitting..." }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String = loginSolverLock.withLock {
|
||||
val logger = overrideLogger ?: bot.logger
|
||||
logger.info("需要进行账户安全认证")
|
||||
logger.info("该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题")
|
||||
logger.info("完成以下账号认证即可成功登录|理论本认证在mirai每个账户中最多出现1次")
|
||||
logger.info("请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符")
|
||||
logger.info("这步操作将在后续的版本中优化")
|
||||
logger.info(url)
|
||||
val logger = loggerSupplier(bot)
|
||||
logger.info { "[UnsafeLogin] 当前登录环境不安全,服务器要求账户认证。请在 QQ 浏览器打开 $url 并完成验证后输入任意字符。" }
|
||||
logger.info { "[UnsafeLogin] Account verification required by the server. Please open $url in QQ browser and complete challenge, then type anything here to submit." }
|
||||
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,39 +70,40 @@ public object SwingSolver : LoginSolver() {
|
||||
// 不会触发各种 NoDefClassError
|
||||
@Suppress("DEPRECATION")
|
||||
internal object WindowHelperJvm {
|
||||
internal val isDesktopSupported: Boolean = kotlin.run {
|
||||
if (System.getProperty("mirai.no-desktop") === null) {
|
||||
kotlin.runCatching {
|
||||
Class.forName("java.awt.Desktop")
|
||||
Class.forName("java.awt.Toolkit")
|
||||
}.onFailure { return@run false } // Android OS
|
||||
kotlin.runCatching {
|
||||
Toolkit.getDefaultToolkit()
|
||||
}.onFailure { // AWT Error, #270
|
||||
return@run false
|
||||
enum class PlatformKind {
|
||||
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 {
|
||||
Class.forName("java.awt.Desktop")
|
||||
Class.forName("java.awt.Toolkit")
|
||||
Toolkit.getDefaultToolkit()
|
||||
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
MiraiLogger.TopLevel.info(
|
||||
"""
|
||||
Mirai 正在使用桌面环境. 如遇到验证码将会弹出对话框. 可添加 JVM 属性 `mirai.no-desktop` 以关闭.
|
||||
""".trimIndent()
|
||||
)
|
||||
MiraiLogger.TopLevel.info(
|
||||
"""
|
||||
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()
|
||||
)
|
||||
return@run PlatformKind.SWING
|
||||
} else {
|
||||
return@run PlatformKind.CLI
|
||||
}
|
||||
kotlin.runCatching {
|
||||
Desktop.isDesktopSupported().also { stat ->
|
||||
if (stat) {
|
||||
MiraiLogger.TopLevel.info(
|
||||
"""
|
||||
Mirai 正在使用桌面环境. 如遇到验证码将会弹出对话框. 可添加 JVM 属性 `mirai.no-desktop` 以关闭.
|
||||
""".trimIndent()
|
||||
)
|
||||
MiraiLogger.TopLevel.info(
|
||||
"""
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
// Should not happen
|
||||
MiraiLogger.TopLevel.warning("Exception in checking desktop support.", it)
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}.getOrElse {
|
||||
return@run PlatformKind.CLI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ internal abstract class AbstractBot<N : BotNetworkHandler> constructor(
|
||||
is BotOfflineEvent.MsfOffline,
|
||||
is BotOfflineEvent.Dropped,
|
||||
is BotOfflineEvent.RequireReconnect,
|
||||
is BotOfflineEvent.PacketFactory10008
|
||||
is BotOfflineEvent.PacketFactoryErrorCode
|
||||
-> {
|
||||
if (!_network.isActive) {
|
||||
// normally closed
|
||||
|
@ -276,16 +276,6 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
||||
if (bot.id != source.fromId) {
|
||||
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 {
|
||||
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.voice.PttStore
|
||||
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.estimateLength
|
||||
import net.mamoe.mirai.internal.utils.toUHexString
|
||||
@ -460,11 +461,13 @@ internal class GroupImpl(
|
||||
response.fileKey,
|
||||
codec
|
||||
)
|
||||
Voice("${md5.toUHexString("")}.amr", md5, content.size.toLong(), "")
|
||||
Voice("${md5.toUHexString("")}.amr", md5, content.size.toLong(), codec, "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
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.ImMsgBody
|
||||
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.io.serialization.loadAs
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
@ -234,24 +235,52 @@ internal fun MsgComm.Msg.toMessageChain(
|
||||
isTemp: Boolean = false
|
||||
): 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(
|
||||
bot: Bot?,
|
||||
botId: Long,
|
||||
groupIdOrZero: Long,
|
||||
onlineSource: Boolean,
|
||||
isTemp: Boolean = false
|
||||
): MessageChain {
|
||||
val elements = this.msgBody.richText.elems
|
||||
): MessageChain = listOf(this).toMessageChain(bot, botId, groupIdOrZero, onlineSource, isTemp)
|
||||
|
||||
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) {
|
||||
// 4 -> Voice(String(fileName), fileMd5, fileSize.toLong(),String(downPara))
|
||||
// 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) {
|
||||
checkNotNull(bot) { "bot is null" }
|
||||
when {
|
||||
@ -263,7 +292,6 @@ internal fun MsgComm.Msg.toMessageChain(
|
||||
+OfflineMessageSourceImplByMsg(this@toMessageChain, botId)
|
||||
}
|
||||
elements.joinToMessageChain(groupIdOrZero, botId, this)
|
||||
pptMsg?.let(::add)
|
||||
}.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.MessageSource
|
||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||
import net.mamoe.mirai.utils.mapToIntArray
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
internal interface MessageSourceInternal {
|
||||
@ -65,44 +66,44 @@ internal suspend inline fun Message.ensureSequenceIdAvailable() {
|
||||
|
||||
internal class MessageSourceFromFriendImpl(
|
||||
override val bot: Bot,
|
||||
val msg: MsgComm.Msg
|
||||
val msg: List<MsgComm.Msg>
|
||||
) : 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 val ids: IntArray get() = sequenceIds// msg.msgBody.richText.attr!!.random
|
||||
override val internalIds: IntArray get() = intArrayOf(msg.msgBody.richText.attr!!.random)
|
||||
override val time: Int get() = msg.msgHead.msgTime
|
||||
override val internalIds: IntArray get() = msg.mapToIntArray { it.msgBody.richText.attr!!.random }
|
||||
override val time: Int get() = msg.first().msgHead.msgTime
|
||||
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) }
|
||||
|
||||
override fun toJceData(): ImMsgBody.SourceMsg = jceData
|
||||
}
|
||||
|
||||
private fun MsgComm.Msg.toJceDataFriendOrTemp(ids: IntArray): ImMsgBody.SourceMsg {
|
||||
val elements = msgBody.richText.elems.toMutableList().also {
|
||||
private fun List<MsgComm.Msg>.toJceDataFriendOrTemp(ids: IntArray): ImMsgBody.SourceMsg {
|
||||
val elements = flatMap {it.msgBody.richText.elems}.toMutableList().also {
|
||||
if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
|
||||
}
|
||||
return ImMsgBody.SourceMsg(
|
||||
origSeqs = intArrayOf(this.msgHead.msgSeq),
|
||||
senderUin = this.msgHead.fromUin,
|
||||
toUin = this.msgHead.toUin,
|
||||
origSeqs = mapToIntArray { it.msgHead.msgSeq },
|
||||
senderUin = first().msgHead.fromUin,
|
||||
toUin = first().msgHead.toUin,
|
||||
flag = 1,
|
||||
elems = this.msgBody.richText.elems,
|
||||
elems = flatMap{it.msgBody.richText.elems},
|
||||
type = 0,
|
||||
time = this.msgHead.msgTime,
|
||||
time = this.first().msgHead.msgTime,
|
||||
pbReserve = SourceMsg.ResvAttr(
|
||||
origUids = ids.map { it.toLong() and 0xFFFF_FFFF }
|
||||
).toByteArray(SourceMsg.ResvAttr.serializer()),
|
||||
srcMsg = MsgComm.Msg(
|
||||
msgHead = MsgComm.MsgHead(
|
||||
fromUin = this.msgHead.fromUin, // qq
|
||||
toUin = this.msgHead.toUin, // group
|
||||
msgType = this.msgHead.msgType, // 82?
|
||||
c2cCmd = this.msgHead.c2cCmd,
|
||||
msgSeq = this.msgHead.msgSeq,
|
||||
msgTime = this.msgHead.msgTime,
|
||||
fromUin = this.first().msgHead.fromUin, // qq
|
||||
toUin = this.first().msgHead.toUin, // group
|
||||
msgType = this.first().msgHead.msgType, // 82?
|
||||
c2cCmd = this.first().msgHead.c2cCmd,
|
||||
msgSeq = this.first().msgHead.msgSeq,
|
||||
msgTime = this.first().msgHead.msgTime,
|
||||
msgUid = ids.single().toLong() and 0xFFFF_FFFF, // ok
|
||||
// groupInfo = MsgComm.GroupInfo(groupCode = this.msgHead.groupInfo.groupCode),
|
||||
isSrcMsg = true
|
||||
@ -118,16 +119,24 @@ private fun MsgComm.Msg.toJceDataFriendOrTemp(ids: IntArray): ImMsgBody.SourceMs
|
||||
|
||||
internal class MessageSourceFromTempImpl(
|
||||
override val bot: Bot,
|
||||
private val msg: MsgComm.Msg
|
||||
private val msg: List<MsgComm.Msg>
|
||||
) : OnlineMessageSource.Incoming.FromTemp(), MessageSourceInternal {
|
||||
override val sequenceIds: IntArray get() = intArrayOf(msg.msgHead.msgSeq)
|
||||
override val internalIds: IntArray get() = intArrayOf(msg.msgBody.richText.attr!!.random)
|
||||
override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||
override val internalIds: IntArray get() = msg.mapToIntArray{it.msgBody.richText.attr!!.random }
|
||||
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
||||
override val ids: IntArray get() = sequenceIds//
|
||||
override val time: Int get() = msg.msgHead.msgTime
|
||||
override val originalMessage: MessageChain by lazy { msg.toMessageChain(bot, 0, false) }
|
||||
override val time: Int get() = msg.first().msgHead.msgTime
|
||||
override val originalMessage: MessageChain by lazy {
|
||||
msg.toMessageChain(
|
||||
bot,
|
||||
bot.id,
|
||||
groupIdOrZero = 0,
|
||||
onlineSource = false,
|
||||
isTemp = false,
|
||||
)
|
||||
}
|
||||
override val sender: Member
|
||||
get() = with(msg.msgHead) {
|
||||
get() = with(msg.first().msgHead) {
|
||||
bot.getGroupOrFail(c2cTmpMsgHead!!.groupUin).getOrFail(fromUin)
|
||||
}
|
||||
|
||||
@ -137,24 +146,24 @@ internal class MessageSourceFromTempImpl(
|
||||
|
||||
internal data class MessageSourceFromGroupImpl(
|
||||
override val bot: Bot,
|
||||
private val msg: MsgComm.Msg
|
||||
private val msg: List<MsgComm.Msg>
|
||||
) : OnlineMessageSource.Incoming.FromGroup(), MessageSourceInternal {
|
||||
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
||||
override val sequenceIds: IntArray get() = intArrayOf(msg.msgHead.msgSeq)
|
||||
override val internalIds: IntArray get() = intArrayOf(msg.msgBody.richText.attr!!.random)
|
||||
override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||
override val internalIds: IntArray get() = msg.mapToIntArray{ it.msgBody.richText.attr!!.random }
|
||||
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, groupIdOrZero = group.id, onlineSource = false)
|
||||
msg.toMessageChain(bot, bot.id, groupIdOrZero = group.id, onlineSource = false)
|
||||
}
|
||||
|
||||
override val sender: Member by lazy {
|
||||
(bot.getGroup(
|
||||
msg.msgHead.groupInfo?.groupCode
|
||||
msg.first().msgHead.groupInfo?.groupCode
|
||||
?: error("cannot find groupCode for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
||||
) as GroupImpl).run {
|
||||
get(msg.msgHead.fromUin)
|
||||
?: msg.msgBody.richText.elems.firstOrNull { it.anonGroupMsg != null }?.run {
|
||||
get(msg.first().msgHead.fromUin)
|
||||
?: msg.first().msgBody.richText.elems.firstOrNull { it.anonGroupMsg != null }?.run {
|
||||
newAnonymous(anonGroupMsg!!.anonNick.encodeToString(), anonGroupMsg.anonId.encodeToBase64())
|
||||
}
|
||||
?: error("cannot find member for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
||||
@ -163,13 +172,13 @@ internal data class MessageSourceFromGroupImpl(
|
||||
|
||||
override fun toJceData(): ImMsgBody.SourceMsg {
|
||||
return ImMsgBody.SourceMsg(
|
||||
origSeqs = intArrayOf(msg.msgHead.msgSeq),
|
||||
senderUin = msg.msgHead.fromUin,
|
||||
origSeqs = intArrayOf(msg.first().msgHead.msgSeq),
|
||||
senderUin = msg.first().msgHead.fromUin,
|
||||
toUin = 0,
|
||||
flag = 1,
|
||||
elems = msg.msgBody.richText.elems,
|
||||
elems = msg.flatMap { it.msgBody.richText.elems },
|
||||
type = 0,
|
||||
time = msg.msgHead.msgTime,
|
||||
time = msg.first().msgHead.msgTime,
|
||||
pbReserve = EMPTY_BYTE_ARRAY,
|
||||
srcMsg = EMPTY_BYTE_ARRAY
|
||||
)
|
||||
|
@ -25,41 +25,41 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
internal class OfflineMessageSourceImplByMsg(
|
||||
// from other sources' originalMessage
|
||||
val delegate: MsgComm.Msg,
|
||||
val delegate: List<MsgComm.Msg>,
|
||||
override val botId: Long,
|
||||
) : OfflineMessageSource(), MessageSourceInternal {
|
||||
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 internalIds: IntArray = intArrayOf(delegate.msgHead.msgUid.toInt())
|
||||
override val internalIds: IntArray = delegate.mapToIntArray { it.msgHead.msgUid.toInt() }
|
||||
override val time: Int
|
||||
get() = delegate.msgHead.msgTime
|
||||
get() = delegate.first().msgHead.msgTime
|
||||
override val fromId: Long
|
||||
get() = delegate.msgHead.fromUin
|
||||
get() = delegate.first().msgHead.fromUin
|
||||
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 {
|
||||
delegate.toMessageChain(
|
||||
null,
|
||||
botId,
|
||||
groupIdOrZero = delegate.msgHead.groupInfo?.groupCode ?: 0,
|
||||
groupIdOrZero = delegate.first().msgHead.groupInfo?.groupCode ?: 0,
|
||||
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 fun toJceData(): ImMsgBody.SourceMsg {
|
||||
return ImMsgBody.SourceMsg(
|
||||
origSeqs = intArrayOf(delegate.msgHead.msgSeq),
|
||||
senderUin = delegate.msgHead.fromUin,
|
||||
origSeqs = delegate.mapToIntArray { it.msgHead.msgSeq },
|
||||
senderUin = delegate.first().msgHead.fromUin,
|
||||
toUin = 0,
|
||||
flag = 1,
|
||||
elems = delegate.msgBody.richText.elems,
|
||||
elems = delegate.flatMap { it.msgBody.richText.elems },
|
||||
type = 0,
|
||||
time = delegate.msgHead.msgTime,
|
||||
time = delegate.first().msgHead.msgTime,
|
||||
pbReserve = 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.proto.MsgSvc
|
||||
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.receive.MessageSvcPbGetMsg
|
||||
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" }
|
||||
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()
|
||||
mainloop@ while (true) {
|
||||
when (response) {
|
||||
is WtLogin.Login.LoginPacketResponse.UnsafeLogin -> {
|
||||
bot.configuration.loginSolver.onSolveUnsafeDeviceLoginVerify(bot, response.url)
|
||||
loginSolverNotNull().onSolveUnsafeDeviceLoginVerify(bot, response.url)
|
||||
response = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
|
||||
}
|
||||
|
||||
is WtLogin.Login.LoginPacketResponse.Captcha -> when (response) {
|
||||
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) {
|
||||
//refresh captcha
|
||||
result = "ABCD"
|
||||
@ -172,7 +183,7 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
|
||||
continue@mainloop
|
||||
}
|
||||
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()
|
||||
continue@mainloop
|
||||
}
|
||||
@ -443,9 +454,9 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
|
||||
input.use {
|
||||
try {
|
||||
parsePacket(it)
|
||||
} catch (e: PacketFactoryIllegalState10008Exception) {
|
||||
} catch (e: PacketFactoryIllegalStateException) {
|
||||
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 }
|
||||
}
|
||||
|
||||
class PacketFactoryIllegalState10008Exception @JvmOverloads constructor(
|
||||
class PacketFactoryIllegalStateException @JvmOverloads constructor(
|
||||
val code: Int,
|
||||
override val message: String? = null,
|
||||
override val cause: Throwable? = null
|
||||
) : RuntimeException()
|
||||
@ -310,8 +311,9 @@ internal object KnownPacketFactories {
|
||||
|
||||
val returnCode = readInt()
|
||||
check(returnCode == 0) {
|
||||
if (returnCode == -10008) { // https://github.com/mamoe/mirai/issues/470
|
||||
throw PacketFactoryIllegalState10008Exception("returnCode = $returnCode")
|
||||
if (returnCode <= -10000) {
|
||||
// https://github.com/mamoe/mirai/issues/470
|
||||
throw PacketFactoryIllegalStateException(returnCode, "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.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.PttMessage
|
||||
import net.mamoe.mirai.message.data.Voice
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
@ -145,7 +146,14 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
|
||||
boolValid = true,
|
||||
fileSize = fileSize.toInt(),
|
||||
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
|
||||
)
|
||||
}
|
||||
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 anonymous: ImMsgBody.AnonymousGroupMsg? = null
|
||||
|
||||
for (elem in pbPushMsg.msg.msgBody.richText.elems) {
|
||||
when {
|
||||
elem.extraInfo != null -> extraInfo = elem.extraInfo
|
||||
elem.anonGroupMsg != null -> anonymous = elem.anonGroupMsg
|
||||
for (msg in msgs) {
|
||||
for (elem in msg.msg.msgBody.richText.elems) {
|
||||
when {
|
||||
elem.extraInfo != null -> extraInfo = elem.extraInfo
|
||||
elem.anonGroupMsg != null -> anonymous = elem.anonGroupMsg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val group =
|
||||
bot.getGroup(pbPushMsg.msg.msgHead.groupInfo!!.groupCode) as GroupImpl? ?: return null // 机器人还正在进群
|
||||
val sender = if (anonymous != null) {
|
||||
group.newAnonymous(anonymous.anonNick.encodeToString(), anonymous.anonId.encodeToBase64())
|
||||
} else {
|
||||
@ -107,7 +111,7 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("Onlin
|
||||
}
|
||||
},
|
||||
sender = sender,
|
||||
message = pbPushMsg.msg.toMessageChain(bot, groupIdOrZero = group.id, onlineSource = true),
|
||||
message = msgs.toMessageChain(bot, groupIdOrZero = group.id, onlineSource = true),
|
||||
permission = when {
|
||||
flags and 16 != 0 -> MemberPermission.ADMINISTRATOR
|
||||
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.OutgoingPacket
|
||||
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.encodeToString
|
||||
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.utils.currentTimeSeconds
|
||||
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
|
||||
@ -243,7 +245,7 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
|
||||
val grayTip = readProtoBuf(TroopTips0x857.NotifyMsgBody.serializer()).optGeneralGrayTip
|
||||
when (grayTip?.templId) {
|
||||
//戳一戳
|
||||
10043L, 1134L, 1135L, 1136L -> {
|
||||
10043L, 1133L, 1132L, 1134L, 1135L, 1136L -> {
|
||||
//预置数据,服务器将不会提供己方已知消息
|
||||
var action = ""
|
||||
var from: Member = group.botAsMember
|
||||
@ -357,22 +359,21 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
|
||||
val operator =
|
||||
if (recallReminder.uin == bot.id) group.botAsMember
|
||||
else group[recallReminder.uin] ?: return@lambda732 emptySequence()
|
||||
val firstPkg = recallReminder.recalledMsgList.firstOrNull() ?: return@lambda732 emptySequence()
|
||||
|
||||
return@lambda732 recallReminder.recalledMsgList.asSequence().mapNotNull { pkg ->
|
||||
when {
|
||||
pkg.authorUin == bot.id && operator.id == bot.id -> null
|
||||
else -> {
|
||||
MessageRecallEvent.GroupRecall(
|
||||
bot,
|
||||
pkg.authorUin,
|
||||
intArrayOf(pkg.seq),
|
||||
intArrayOf(pkg.msgRandom),
|
||||
pkg.time,
|
||||
operator,
|
||||
group
|
||||
)
|
||||
}
|
||||
}
|
||||
return@lambda732 when {
|
||||
firstPkg.authorUin == bot.id && operator.id == bot.id -> emptySequence()
|
||||
else -> sequenceOf(
|
||||
MessageRecallEvent.GroupRecall(
|
||||
bot,
|
||||
firstPkg.authorUin,
|
||||
recallReminder.recalledMsgList.mapToIntArray { it.seq },
|
||||
recallReminder.recalledMsgList.mapToIntArray { it.msgRandom },
|
||||
firstPkg.time,
|
||||
operator,
|
||||
group
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -422,6 +423,8 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
|
||||
@ProtoNumber(7) val pkgNum: Int, // 1
|
||||
@ProtoNumber(8) val pkgIndex: 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
|
||||
) : ProtoBuf
|
||||
|
||||
@ -441,7 +444,7 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
|
||||
messageIds = intArrayOf(it.srcId),
|
||||
messageInternalIds = intArrayOf(it.srcInternalId),
|
||||
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())
|
||||
when (body.templId) {
|
||||
//戳一戳
|
||||
1134L, 1135L, 1136L, 10043L -> {
|
||||
1132L, 1133L, 1134L, 1135L, 1136L, 10043L -> {
|
||||
//预置数据,服务器将不会提供己方已知消息
|
||||
var from: Friend = bot.asFriend
|
||||
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