1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-25 21:23:55 +08:00

[core] Provide isFirstLogin and getReLoginCause in BotAuthInfo ()

* [core] provide `isFirstLogin` and `getReLoginCause` in `BotAuthInfo`

* [core] provide `reAuthCause` as sealed class

* [core] add test

* Update mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt

我觉得这个描述不太好,因为这里是 `authorize` 过程而不是 `login` 过程

Co-authored-by: Him188 <Him188@mamoe.net>

* [core] optimize docs and rename

* import

* revert linebreak

---------

Co-authored-by: Him188 <Him188@mamoe.net>
This commit is contained in:
StageGuard 2023-06-20 21:22:49 +08:00 committed by GitHub
parent 8ff64d4a7f
commit 545e5bb310
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 384 additions and 5 deletions
mirai-core-api
compatibility-validation
android/api
jvm/api
src/commonMain/kotlin/auth
mirai-core/src
commonMain/kotlin
commonTest/kotlin/network

View File

@ -172,10 +172,48 @@ public final class net/mamoe/mirai/_MiraiInstance {
public static final fun set (Lnet/mamoe/mirai/IMirai;)V
}
public abstract class net/mamoe/mirai/auth/AuthReason {
public abstract fun getBot ()Lnet/mamoe/mirai/Bot;
public abstract fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$FastLoginError : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$ForceOffline : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$FreshLogin : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$MsfOffline : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$NetworkError : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$Unknown : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public final fun getCause ()Ljava/lang/Throwable;
public fun getMessage ()Ljava/lang/String;
}
public abstract interface class net/mamoe/mirai/auth/BotAuthInfo {
public abstract fun getConfiguration ()Lnet/mamoe/mirai/utils/BotConfiguration;
public abstract fun getDeviceInfo ()Lnet/mamoe/mirai/utils/DeviceInfo;
public abstract fun getId ()J
public abstract fun getReason ()Lnet/mamoe/mirai/auth/AuthReason;
public abstract fun isFirstLogin ()Z
}
public abstract interface class net/mamoe/mirai/auth/BotAuthResult {

View File

@ -172,10 +172,48 @@ public final class net/mamoe/mirai/_MiraiInstance {
public static final fun set (Lnet/mamoe/mirai/IMirai;)V
}
public abstract class net/mamoe/mirai/auth/AuthReason {
public abstract fun getBot ()Lnet/mamoe/mirai/Bot;
public abstract fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$FastLoginError : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$ForceOffline : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$FreshLogin : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$MsfOffline : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$NetworkError : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public fun getMessage ()Ljava/lang/String;
}
public final class net/mamoe/mirai/auth/AuthReason$Unknown : net/mamoe/mirai/auth/AuthReason {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public final fun getCause ()Ljava/lang/Throwable;
public fun getMessage ()Ljava/lang/String;
}
public abstract interface class net/mamoe/mirai/auth/BotAuthInfo {
public abstract fun getConfiguration ()Lnet/mamoe/mirai/utils/BotConfiguration;
public abstract fun getDeviceInfo ()Lnet/mamoe/mirai/utils/DeviceInfo;
public abstract fun getId ()J
public abstract fun getReason ()Lnet/mamoe/mirai/auth/AuthReason;
public abstract fun isFirstLogin ()Z
}
public abstract interface class net/mamoe/mirai/auth/BotAuthResult {

View File

@ -9,8 +9,10 @@
package net.mamoe.mirai.auth
import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotFactory
import net.mamoe.mirai.Mirai
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.RetryLaterException
import net.mamoe.mirai.utils.*
@ -102,6 +104,114 @@ public interface BotAuthInfo {
public val id: Long
public val deviceInfo: DeviceInfo
public val configuration: BotConfiguration
/**
* 是否是首次登录
*
* 首次登录指的是首次调用 [Bot.login] 进行登录直到登录成功的过程
*
* 若在首次登录过程中多次进入[认证流程][BotAuthorization.authorize]则这流程些均被视为首次登录
*
* @see Bot.login
* @see BotAuthorization.authorize
*/
public val isFirstLogin: Boolean
/**
* 导致进入[认证流程][BotAuthorization.authorize]的原因
*/
public val reason: AuthReason
}
/**
* 导致进行[认证流程][BotAuthorization.authorize]的原因
*/
public sealed class AuthReason {
public abstract val bot: Bot
public abstract val message: String?
/**
* Bot 全新[登录][Bot.login]
*
* 全新登录指登录前本地没有任何当前 Bot 的登录缓存信息而进行的登录
*
* 全新登录时将会进入[认证流程][BotAuthorization.authorize]
*
* @see Bot.login
* @see FastLoginError
*/
public class FreshLogin @MiraiInternalApi constructor(
override val bot: Bot,
override val message: String?
) : AuthReason()
/**
* Bot 被挤下线
*
* Bot 账号在其他客户端使用相同或相似协议登录时Bot 会下线
* 被挤下线后当前的登录会话将失效
*
* [BotConfiguration.autoReconnectOnForceOffline] `true`
* Bot 会尝试重新登录并会以此原因进入[认证流程][BotAuthorization.authorize]
*
* @see BotConfiguration.autoReconnectOnForceOffline
* @see BotOfflineEvent.Force
*/
public class ForceOffline @MiraiInternalApi constructor(
override val bot: Bot,
override val message: String?
) : AuthReason()
/**
* Bot 被服务器断开
*
* 因其他原因导致 Bot 被服务器断开这些原因包括账号被封禁被其他客户端手动下线等
* 被服务器断开下线后当前的登录会话将失效
*
* Bot 会尝试重新登录并会以此原因进入[认证流程][BotAuthorization.authorize]
*
* @see BotOfflineEvent.MsfOffline
*/
public class MsfOffline @MiraiInternalApi constructor(
override val bot: Bot,
override val message: String?
) : AuthReason()
/**
* 由网络原因引起的掉线
*
* 一般情况下Bot 被服务器断开后会尝试重新登录
*
* 由网络问题引起的掉线不一定会使当前的登录会话失效
* 仅登录会话失效时 Bot 会以此原因进入[认证流程][BotAuthorization.authorize]
*/
public class NetworkError @MiraiInternalApi constructor(
override val bot: Bot,
override val message: String?
) : AuthReason()
/**
* 快速登录失败
*
* Bot 账号首次 [登录][Bot.login] 成功后会保存登录缓存信息用于下次登录
*
* 下次登录时Bot 会首先使用登录缓存信息尝试快速登录
* 若快速登录失败则会以此原因进入[认证流程][BotAuthorization.authorize]
*
* @see BotAuthorization.authorize
* @see Bot.login
*/
public class FastLoginError @MiraiInternalApi constructor(
override val bot: Bot,
override val message: String?
) : AuthReason()
public class Unknown @MiraiInternalApi constructor(
override val bot: Bot,
public val cause: Throwable?
) : AuthReason() {
override val message: String? = cause?.message
}
}
@NotStableForInheritance

View File

@ -12,6 +12,7 @@ package net.mamoe.mirai.internal
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import net.mamoe.mirai.Bot
import net.mamoe.mirai.auth.AuthReason
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.BotOnlineEvent
@ -141,14 +142,18 @@ internal open class QQAndroidBot constructor(
cause is ForceOfflineException -> {
eventDispatcher.broadcastAsync(BotOfflineEvent.Force(bot, cause.title, cause.message))
}
cause is StatSvc.ReqMSFOffline.MsfOfflineToken -> {
eventDispatcher.broadcastAsync(BotOfflineEvent.MsfOffline(bot, cause))
}
cause is NetworkException && cause.recoverable -> {
eventDispatcher.broadcastAsync(BotOfflineEvent.Dropped(bot, cause))
}
cause is BotClosedByEvent -> {
}
else -> {
// any other unexpected exceptions considered as an error
@ -165,6 +170,17 @@ internal open class QQAndroidBot constructor(
}
}
},
StateChangedObserver("ReLoginCauseCatcher", State.OK, State.CLOSED) { new ->
get(SsoProcessor).authReason = when (val cause = new.getCause()) {
is ForceOfflineException -> AuthReason.ForceOffline(bot, cause.message)
is StatSvc.ReqMSFOffline.MsfOfflineToken -> AuthReason.MsfOffline(bot, cause.message)
is NetworkException -> AuthReason.NetworkError(bot, cause.message)
else -> AuthReason.Unknown(bot, cause)
}
},
StateChangedObserver("FirstLoginObserver", State.OK) {
get(SsoProcessor).isFirstLogin = false
}
).safe(logger.subLogger("StateObserver")) + LoggingStateObserver.createLoggingIfEnabled()
}

View File

@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.network.components
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import net.mamoe.mirai.auth.AuthReason
import net.mamoe.mirai.auth.BotAuthInfo
import net.mamoe.mirai.auth.BotAuthorization
import net.mamoe.mirai.internal.network.Packet
@ -58,6 +59,9 @@ internal interface SsoProcessor {
val firstLoginSucceed: Boolean get() = firstLoginResult?.success ?: false
val registerResp: StatSvc.Register.Response?
var isFirstLogin: Boolean
var authReason: AuthReason
/**
* Do login. Throws [LoginFailedException] if failed
*/
@ -147,6 +151,11 @@ internal open class SsoProcessorImpl(
override val ssoSession: SsoSession get() = client
private val components get() = ssoContext.bot.components
override var isFirstLogin: Boolean = true
override var authReason: AuthReason by lateinitMutableProperty {
AuthReason.FreshLogin(ssoContext.bot, null)
}
private val botAuthInfo = object : BotAuthInfo {
override val id: Long
get() = ssoContext.bot.id
@ -154,6 +163,10 @@ internal open class SsoProcessorImpl(
get() = ssoContext.device
override val configuration: BotConfiguration
get() = ssoContext.bot.configuration
override val isFirstLogin: Boolean
get() = this@SsoProcessorImpl.isFirstLogin
override val reason: AuthReason
get() = this@SsoProcessorImpl.authReason
}
protected open suspend fun doSlowLogin(
@ -215,6 +228,11 @@ internal open class SsoProcessorImpl(
kotlin.runCatching {
doFastLogin(handler)
}.onFailure { e ->
// first fast-login exception should also be considered as re-auth cause.
if (isFirstLogin) {
authReason = AuthReason.FastLoginError(ssoContext.bot, e.message)
}
initAndStartAuthControl()
authControl!!.exceptionCollector.collect(e)
@ -224,6 +242,8 @@ internal open class SsoProcessorImpl(
loginSuccess()
return
} else if (isFirstLogin) {
authReason = AuthReason.FreshLogin(ssoContext.bot, null)
}
}

View File

@ -26,7 +26,7 @@ internal abstract class AbstractBotAuthTest : AbstractCommonNHTestWithSelector()
overrideComponents[SsoProcessor] = SsoProcessorImpl(overrideComponents[SsoProcessorContext])
}
protected fun setAuthorization(authorize: (session: BotAuthSession, info: BotAuthInfo) -> BotAuthResult) {
protected fun setAuthorization(authorize: suspend (session: BotAuthSession, info: BotAuthInfo) -> BotAuthResult) {
// Run a real SsoProcessor, just without sending packets
bot.account.authorization = object : BotAuthorization {
override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult {

View File

@ -0,0 +1,149 @@
/*
* Copyright 2019-2023 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/dev/LICENSE
*/
package net.mamoe.mirai.internal.network.auth
import kotlinx.coroutines.test.runTest
import net.mamoe.mirai.auth.AuthReason
import net.mamoe.mirai.auth.BotAuthResult
import net.mamoe.mirai.internal.MockAccount
import net.mamoe.mirai.internal.contact.uin
import net.mamoe.mirai.internal.network.components.AccountSecretsManager
import net.mamoe.mirai.internal.network.framework.createWLoginSigInfo
import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushForceOffline
import net.mamoe.mirai.internal.network.protocol.data.jce.SvcRespRegister
import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacket
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPushForceOffline
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertTrue
internal class AuthorizationReasonTest : AbstractBotAuthTest() {
@Test
fun `first login without fast login`() = runTest {
var isFirstLogin: Boolean = false
var authReason: AuthReason? = AuthReason.Unknown(bot, null)
setAuthorization { auth, info ->
isFirstLogin = info.isFirstLogin
authReason = info.reason
auth.authByPassword("")
return@setAuthorization object : BotAuthResult {}
}
usePacketReplierThroughout {
expect(WtLogin.Login) reply {
bot.components[AccountSecretsManager].getSecrets(MockAccount)
?.wLoginSigInfo = createWLoginSigInfo(bot.id)
WtLogin.Login.LoginPacketResponse.Success(bot)
}
expect(StatSvc.Register) reply { StatSvc.Register.Response(SvcRespRegister()) }
expect(StatSvc.Register) reply { StatSvc.Register.Response(SvcRespRegister()) }
}
bot.components[AccountSecretsManager].getSecrets(MockAccount)?.wLoginSigInfoField = null
bot.login()
assertTrue(isFirstLogin)
assertIs<AuthReason.FreshLogin>(authReason)
}
@Test
fun `first login with fast login success`() = runTest {
var authorizationCalled = false
setAuthorization { auth, _ ->
authorizationCalled = true
auth.authByPassword("")
return@setAuthorization object : BotAuthResult {}
}
usePacketReplierThroughout {
expect(WtLogin.ExchangeEmp) reply { WtLogin.Login.LoginPacketResponse.Success(bot) }
expect(StatSvc.Register) reply { StatSvc.Register.Response(SvcRespRegister()) }
expect(StatSvc.Register) reply { StatSvc.Register.Response(SvcRespRegister()) }
}
bot.login()
assertFalse(authorizationCalled)
}
@Test
fun `first login with fast login fail`() = runTest {
var isFirstLogin: Boolean = false
var authReason: AuthReason? = null
setAuthorization { auth, info ->
isFirstLogin = info.isFirstLogin
authReason = info.reason
auth.authByPassword("")
return@setAuthorization object : BotAuthResult {}
}
usePacketReplierThroughout {
expect(WtLogin.ExchangeEmp) reply { WtLogin.Login.LoginPacketResponse.Error(bot, 1, "", "", ", ") }
expect(WtLogin.Login) reply { WtLogin.Login.LoginPacketResponse.Success(bot) }
expect(StatSvc.Register) reply { StatSvc.Register.Response(SvcRespRegister()) }
expect(StatSvc.Register) reply { StatSvc.Register.Response(SvcRespRegister()) }
}
bot.login()
assertTrue(isFirstLogin)
assertIs<AuthReason.FastLoginError>(authReason)
}
@Test
fun `force offline`() = runTest {
var isFirstLogin: Boolean = true
var authReason: AuthReason? = null
setAuthorization { auth, info ->
isFirstLogin = info.isFirstLogin
authReason = info.reason
auth.authByPassword("")
return@setAuthorization object : BotAuthResult {}
}
usePacketReplierThroughout {
expect(WtLogin.ExchangeEmp) reply { WtLogin.Login.LoginPacketResponse.Success(bot) }
expect(StatSvc.Register) reply { StatSvc.Register.Response(SvcRespRegister()) }
expect(WtLogin.Login) reply {
bot.components[AccountSecretsManager].getSecrets(MockAccount)
?.wLoginSigInfo = createWLoginSigInfo(bot.id)
WtLogin.Login.LoginPacketResponse.Success(bot)
}
expect(StatSvc.Register) reply { StatSvc.Register.Response(SvcRespRegister()) }
expect(StatSvc.Register) reply { StatSvc.Register.Response(SvcRespRegister()) }
}
bot.configuration.autoReconnectOnForceOffline = true
bot.login()
network.currentInstance().collectReceived(
IncomingPacket(
MessageSvcPushForceOffline.commandName,
RequestPushForceOffline(bot.uin, tips = "force offline manually in test")
)
)
eventDispatcher.joinBroadcast() // why test finished before code reaches end??
assertFalse(isFirstLogin)
assertIs<AuthReason.ForceOffline>(authReason)
}
}

View File

@ -11,10 +11,7 @@ package net.mamoe.mirai.internal.network.component
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.yield
import net.mamoe.mirai.auth.BotAuthInfo
import net.mamoe.mirai.auth.BotAuthResult
import net.mamoe.mirai.auth.BotAuthSession
import net.mamoe.mirai.auth.BotAuthorization
import net.mamoe.mirai.auth.*
import net.mamoe.mirai.internal.network.auth.AuthControl
import net.mamoe.mirai.internal.network.components.SsoProcessorContext
import net.mamoe.mirai.internal.network.components.SsoProcessorImpl
@ -23,6 +20,7 @@ import net.mamoe.mirai.network.CustomLoginFailedException
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.DeviceInfo
import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY
import kotlin.properties.Delegates
import kotlin.reflect.KClass
import kotlin.test.Test
import kotlin.test.assertFailsWith
@ -36,6 +34,11 @@ internal class BotAuthControlTest : AbstractCommonNHTest() {
get() = bot.components[SsoProcessorContext].device
override val configuration: BotConfiguration
get() = bot.configuration
override val isFirstLogin: Boolean
get() = false
override val reason: AuthReason by Delegates.notNull()
}
private suspend fun AuthControl.assertRequire(exceptedType: KClass<*>) {

View File

@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.network.framework.components
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import net.mamoe.mirai.auth.AuthReason
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.components.*
@ -21,6 +22,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.utils.DeviceInfo
import net.mamoe.mirai.utils.debug
import net.mamoe.mirai.utils.lateinitMutableProperty
import kotlin.properties.Delegates
import kotlin.random.Random
internal open class TestSsoProcessor(private val bot: QQAndroidBot) : SsoProcessor {
@ -47,6 +49,9 @@ internal open class TestSsoProcessor(private val bot: QQAndroidBot) : SsoProcess
}
override var registerResp: StatSvc.Register.Response? = null
override var authReason: AuthReason by Delegates.notNull()
override var isFirstLogin: Boolean = true
override suspend fun login(handler: NetworkHandler) {
bot.network.logger.debug { "SsoProcessor.login" }
}