Support server changing, close #52

This commit is contained in:
Him188 2020-04-12 19:17:27 +08:00
parent e6ef03f60d
commit da801d7b6d
11 changed files with 289 additions and 72 deletions

View File

@ -46,7 +46,6 @@ import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.LongMsg
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.*
import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
import net.mamoe.mirai.qqandroid.utils.*
import net.mamoe.mirai.qqandroid.utils.MiraiPlatformUtils
import net.mamoe.mirai.qqandroid.utils.encodeToString
import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
@ -204,6 +203,12 @@ internal abstract class QQAndroidBotBase constructor(
})
}
override suspend fun relogin(cause: Throwable?) {
client.useNextServers { host, port ->
network.relogin(host, port, cause)
}
}
@LowLevelAPI
override fun _lowLevelNewQQ(friendInfo: FriendInfo): QQ {
return QQImpl(

View File

@ -37,6 +37,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.*
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.ConfigPushSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.Heartbeat
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin
@ -107,7 +108,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}
@OptIn(MiraiExperimentalAPI::class)
override suspend fun relogin(cause: Throwable?) {
override suspend fun relogin(host: String, port: Int, cause: Throwable?) {
heartbeatJob?.cancel(CancellationException("relogin", cause))
heartbeatJob?.join()
if (::channel.isInitialized) {
@ -121,10 +122,11 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}
channel = PlatformSocket()
// TODO: 2020/2/14 连接多个服务器, #52
withTimeoutOrNull(3000) {
channel.connect("114.221.144.22", 8080)
channel.connect(host, port)
} ?: error("timeout connecting server")
logger.info("Connected to server 114.221.144.22:8080")
logger.info("Connected to server $host:$port")
startPacketReceiverJobOrKill(CancellationException("relogin", cause))
var response: WtLogin.Login.LoginPacketResponse = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
@ -301,6 +303,30 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
launch { reloadGroupList() }
}
this@QQAndroidBotNetworkHandler.launch {
logger.info { "Awaiting ConfigPushSvc.PushReq" }
val resp =
subscribingGetOrNull<ConfigPushSvc.PushReq.PushReqResponse, ConfigPushSvc.PushReq.PushReqResponse>(
10_000) { it }
when (resp) {
null -> logger.info { "Missing ConfigPushSvc.PushReq." }
is ConfigPushSvc.PushReq.PushReqResponse.Success -> {
logger.info { "ConfigPushSvc.PushReq: success" }
}
is ConfigPushSvc.PushReq.PushReqResponse.ChangeServer -> {
logger.info { "Server requires reconnect." }
logger.info { "ChangeServer.unknown = ${resp.unknown}" }
logger.info { "Server list: ${resp.serverList.joinToString()}" }
resp.serverList.forEach {
bot.client.serverList.add(it.host to it.port)
}
BotOfflineEvent.RequireReconnect(bot).broadcast()
}
}
}
withTimeoutOrNull(30000) {
launch { subscribingGet<MessageSvc.PbGetMsg.GetMsgSuccess, Unit> { Unit } }
MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendAndExpect<Packet>()

View File

@ -15,8 +15,10 @@ import kotlinx.atomicfu.AtomicInt
import kotlinx.atomicfu.atomic
import kotlinx.io.core.*
import net.mamoe.mirai.data.OnlineStatus
import net.mamoe.mirai.network.NoServerAvailableException
import net.mamoe.mirai.qqandroid.BotAccount
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.FileStoragePushFSSvcList
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketLogger
import net.mamoe.mirai.qqandroid.network.protocol.packet.Tlv
@ -42,6 +44,17 @@ private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray
*/
internal fun getRandomByteArray(length: Int): ByteArray = ByteArray(length) { Random.nextInt(0..255).toByte() }
internal object DefaultServerList : Set<Pair<String, Int>> by setOf(
"42.81.169.46" to 8080,
"42.81.172.81" to 80,
"114.221.148.59" to 14000,
"42.81.172.147" to 443,
"125.94.60.146" to 80,
"114.221.144.215" to 80,
"msfwifi.3g.qq.com" to 8080,
"42.81.172.22" to 80
)
/*
APP ID:
GetStViaSMSVerifyLogin = 16
@ -64,6 +77,8 @@ internal open class QQAndroidClient(
val device: DeviceInfo = SystemDeviceInfo(context),
bot: QQAndroidBot
) {
internal val serverList: MutableList<Pair<String, Int>> = DefaultServerList.toMutableList()
val keys: Map<String, ByteArray> by lazy {
mapOf(
"16 zero" to ByteArray(16),
@ -102,11 +117,30 @@ internal open class QQAndroidClient(
val randomKey: ByteArray = getRandomByteArray(16)
var miscBitMap: Int = 184024956 // 也可能是 150470524 ?
var mainSigMap: Int = 16724722
private var mainSigMap: Int = 16724722
var subSigMap: Int = 0x10400 //=66,560
private val _ssoSequenceId: AtomicInt = atomic(85600)
lateinit var fileStoragePushFSSvcList: FileStoragePushFSSvcList
internal suspend inline fun useNextServers(crossinline block: suspend (host: String, port: Int) -> Unit) {
@Suppress("UNREACHABLE_CODE", "ThrowableNotThrown") // false positive
retryCatching(bot.client.serverList.size) {
val pair = bot.client.serverList.random()
kotlin.runCatching {
block(pair.first, pair.second)
return
}.getOrElse {
bot.client.serverList.remove(pair)
bot.logger.warning(it)
throw it
}
}.getOrElse {
bot.client.serverList.addAll(DefaultServerList)
throw NoServerAvailableException(it)
}
}
@MiraiInternalAPI("Do not use directly. Get from the lambda param of buildSsoPacket")
internal fun nextSsoSequenceId() = _ssoSequenceId.addAndGet(2)

View File

@ -12,7 +12,6 @@ package net.mamoe.mirai.qqandroid.network.protocol.data.jce
import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.qqandroid.utils.io.JceStruct
import net.mamoe.mirai.qqandroid.utils.io.ProtoBuf
import net.mamoe.mirai.qqandroid.utils.io.serialization.jce.JceId
@Serializable
@ -85,7 +84,7 @@ internal class _340(
@JceId(14) val netType: Byte? = 0,
@JceId(15) val heThreshold: Int? = 0,
@JceId(16) val policyId: String? = ""
) : ProtoBuf
) : JceStruct
@Serializable
internal class _339(
@ -102,8 +101,8 @@ internal class _339(
@Serializable
internal class FileStoragePushFSSvcList(
@JceId(0) val vUpLoadList: List<FileStorageServerListInfo> = listOf(),
@JceId(1) val vPicDownLoadList: List<FileStorageServerListInfo> = listOf(),
@JceId(0) val vUpLoadList: List<FileStorageServerListInfo>? = listOf(),
@JceId(1) val vPicDownLoadList: List<FileStorageServerListInfo>? = listOf(),
@JceId(2) val vGPicDownLoadList: List<FileStorageServerListInfo>? = null,
@JceId(3) val vQzoneProxyServiceList: List<FileStorageServerListInfo>? = null,
@JceId(4) val vUrlEncodeServiceList: List<FileStorageServerListInfo>? = null,

View File

@ -43,6 +43,8 @@ internal sealed class PacketFactory<TPacket : Packet?> {
* 筛选从服务器接收到的包时的 commandName
*/
abstract val receivingCommandName: String
open val canBeCached: Boolean get() = true
}
/**
@ -216,7 +218,7 @@ internal object KnownPacketFactories {
}?.let {
it as IncomingPacket<T>
if (it.packetFactory is IncomingPacketFactory<T> && bot.network.pendingEnabled) {
if (it.packetFactory is IncomingPacketFactory<T> && it.packetFactory.canBeCached && bot.network.pendingEnabled) {
bot.network.pendingIncomingPackets?.addLast(it.also {
it.consumer = consumer
it.flag2 = flag2

View File

@ -83,10 +83,14 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
@Suppress("PropertyName")
internal lateinit var _network: N
protected abstract suspend fun relogin(cause: Throwable?)
@Suppress("unused")
private val offlineListener: Listener<BotOfflineEvent> = this.subscribeAlways { event ->
when (event) {
is BotOfflineEvent.Dropped -> {
is BotOfflineEvent.Dropped,
is BotOfflineEvent.RequireReconnect
-> {
if (!_network.isActive) {
return@subscribeAlways
}
@ -96,9 +100,9 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
if (tryCount != 0) {
delay(configuration.reconnectPeriodMillis)
}
network.relogin(event.cause)
relogin((event as? BotOfflineEvent.Dropped)?.cause)
logger.info("Reconnected successfully")
BotReloginEvent(bot, event.cause).broadcast()
BotReloginEvent(bot, (event as? BotOfflineEvent.Dropped)?.cause).broadcast()
return@subscribeAlways
}?.let {
logger.info("Cannot reconnect")
@ -134,7 +138,7 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
while (true) {
_network = createNetworkHandler(this.coroutineContext)
try {
_network.relogin()
relogin(null)
return
} catch (e: LoginFailedException) {
throw e

View File

@ -71,6 +71,12 @@ sealed class BotOfflineEvent : BotEvent {
* 被服务器断开或因网络问题而掉线
*/
data class Dropped(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent(), Packet, BotPassiveEvent
/**
* 服务器主动要求更换另一个服务器
*/
@SinceMirai("0.38.0")
data class RequireReconnect(override val bot: Bot) : BotOfflineEvent(), Packet, BotPassiveEvent
}
/**

View File

@ -93,7 +93,7 @@ inline fun <reified E : Event, R : Any> CoroutineScope.subscribingGetAsync(
internal suspend inline fun <reified E : Event, R> subscribingGetOrNullImpl(
coroutineScope: CoroutineScope,
noinline mapper: suspend E.(E) -> R?
): R {
): R? {
var result: Result<R?> = Result.success(null) // stub
var listener: Listener<E>? = null
@Suppress("DuplicatedCode") // for better performance
@ -114,5 +114,5 @@ internal suspend inline fun <reified E : Event, R> subscribingGetOrNullImpl(
}
listener.join()
return result.getOrThrow()!!
return result.getOrThrow()
}

View File

@ -67,7 +67,7 @@ abstract class BotNetworkHandler : CoroutineScope {
*/
@Suppress("SpellCheckingInspection")
@MiraiInternalAPI
abstract suspend fun relogin(cause: Throwable? = null)
abstract suspend fun relogin(host: String, port: Int, cause: Throwable? = null)
/**
* 初始化获取好友列表等值.

View File

@ -29,6 +29,11 @@ sealed class LoginFailedException : RuntimeException {
*/
class WrongPasswordException(message: String?) : LoginFailedException(message)
/**
* 无可用服务器
*/
class NoServerAvailableException(override val cause: Throwable?) : LoginFailedException("no server available")
/**
* 需要短信验证时抛出. mirai 目前还不支持短信验证.
*/