Highway big data channel (#917)

- Fix retryWithServers exception suppress
- ConfigPush BDH session
- ConfigPush SSO change server
- Upload group PTT silk, #577
- Support upload private image through highway, close #194;
- Add fallback strategies for uploading group voice #577 and private image #916
- Upload all resources through highway BDH
- Support concurrent uploading
- Add BotConfiguration.highwayUploadCoroutineCount

close #916, close #577, close #194
This commit is contained in:
Him188 2021-01-26 19:08:01 +08:00 committed by GitHub
parent 8ad0b91974
commit d8b1505181
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1230 additions and 979 deletions

View File

@ -5329,6 +5329,7 @@ public class net/mamoe/mirai/utils/BotConfiguration {
public final fun getFirstReconnectDelayMillis ()J
public final fun getHeartbeatPeriodMillis ()J
public final fun getHeartbeatTimeoutMillis ()J
public final fun getHighwayUploadCoroutineCount ()I
public final fun getJson ()Lkotlinx/serialization/json/Json;
public final fun getLoginSolver ()Lnet/mamoe/mirai/utils/LoginSolver;
public final fun getNetworkLoggerSupplier ()Lkotlin/jvm/functions/Function1;
@ -5366,6 +5367,7 @@ public class net/mamoe/mirai/utils/BotConfiguration {
public final fun setFirstReconnectDelayMillis (J)V
public final fun setHeartbeatPeriodMillis (J)V
public final fun setHeartbeatTimeoutMillis (J)V
public final fun setHighwayUploadCoroutineCount (I)V
public final fun setJson (Lkotlinx/serialization/json/Json;)V
public final fun setLoginSolver (Lnet/mamoe/mirai/utils/LoginSolver;)V
public final fun setNetworkLoggerSupplier (Lkotlin/jvm/functions/Function1;)V

View File

@ -12,7 +12,7 @@
import org.gradle.api.attributes.Attribute
object Versions {
const val project = "2.2.0-dev-1"
const val project = "2.2.0-dev-bdh-2"
const val kotlinCompiler = "1.4.21"
const val kotlinStdlib = "1.4.21"

View File

@ -120,6 +120,16 @@ public open class BotConfiguration { // open for Java
/** 使用协议类型 */
public var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE
/**
* Highway 通道上传图片, 语音, 文件等资源时的协程数量.
*
* 每个协程的速度约为 200KB/s. 协程数量越多越快, 同时也更要求性能.
* 默认 [CPU 核心数][Runtime.availableProcessors].
*
* @since 2.2
*/
public var highwayUploadCoroutineCount: Int = Runtime.getRuntime().availableProcessors()
/**
* 设备信息覆盖. 在没有手动指定时将会通过日志警告, 并使用随机设备信息.
* @see fileBasedDeviceInfo 使用指定文件存储设备信息

View File

@ -12,10 +12,11 @@
package net.mamoe.mirai.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@Suppress("unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "DeprecatedCallableAddReplaceWith")
@Deprecated(
@ -31,3 +32,13 @@ public suspend inline fun <R> runBIO(
public suspend inline fun <R> runBIO(
noinline block: () -> R
): R = runInterruptible(context = Dispatchers.IO, block = block)
public inline fun CoroutineScope.launchWithPermit(
semaphore: Semaphore,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
crossinline block: suspend () -> Unit
): Job {
return launch(coroutineContext) {
semaphore.withPermit { block() }
}
}

View File

@ -20,6 +20,24 @@ public inline fun <reified T> Any?.safeCast(): T? = this as? T
public inline fun <reified T> Any?.castOrNull(): T? = this as? T
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE")
@kotlin.internal.InlineOnly
@kotlin.internal.LowPriorityInOverloadResolution
public inline fun <R, T : R> Result<T>.recoverCatchingSuppressed(transform: (exception: Throwable) -> R): Result<R> {
return when (val exception = exceptionOrNull()) {
null -> this
else -> {
try {
Result.success(transform(exception))
} catch (e: Throwable) {
e.addSuppressed(exception)
Result.failure(e)
}
}
}
}
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE")
@kotlin.internal.InlineOnly
@kotlin.internal.LowPriorityInOverloadResolution

View File

@ -68,6 +68,7 @@ internal abstract class AbstractBot<N : BotNetworkHandler> constructor(
}
// region network
internal val serverList: MutableList<Pair<String, Int>> = DefaultServerList.toMutableList()
val network: N get() = _network

View File

@ -25,6 +25,7 @@ import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.internal.contact.*
import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.network.highway.Highway
import net.mamoe.mirai.internal.network.highway.ResourceKind
import net.mamoe.mirai.internal.network.protocol.data.jce.SvcDevLoginInfo
import net.mamoe.mirai.internal.network.protocol.data.proto.LongMsg
import net.mamoe.mirai.internal.network.protocol.packet.chat.*
@ -745,19 +746,20 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
)
).toByteArray(LongMsg.ReqBody.serializer())
Highway.uploadResource(
bot,
response.proto.uint32UpIp.zip(response.proto.uint32UpPort),
response.proto.msgSig,
body.toExternalResource(null),
when (isLong) {
true -> "group long message"
false -> "group forward message"
body.toExternalResource().use { resource ->
Highway.uploadResourceBdh(
bot = bot,
resource = resource,
kind = when (isLong) {
true -> ResourceKind.GROUP_LONG_MESSAGE
false -> ResourceKind.GROUP_FORWARD_MESSAGE
},
27
commandId = 27,
initialTicket = response.proto.msgSig
)
}
}
}
return resId
}

View File

@ -26,6 +26,7 @@ import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.packet.chat.*
import net.mamoe.mirai.internal.network.useNextServers
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.*
@ -63,6 +64,8 @@ internal class QQAndroidBot constructor(
return client
}
override val bot: QQAndroidBot get() = this
internal var firstLoginSucceed: Boolean = false
inline val json get() = configuration.json

View File

@ -22,15 +22,16 @@ import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.ImageUploadEvent
import net.mamoe.mirai.internal.message.OfflineFriendImage
import net.mamoe.mirai.internal.message.getImageType
import net.mamoe.mirai.internal.network.highway.ChannelKind
import net.mamoe.mirai.internal.network.highway.Highway
import net.mamoe.mirai.internal.network.highway.ResourceKind.PRIVATE_IMAGE
import net.mamoe.mirai.internal.network.highway.postImage
import net.mamoe.mirai.internal.network.highway.sizeToString
import net.mamoe.mirai.internal.network.highway.tryServers
import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x352
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.utils.*
import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt
import kotlin.time.measureTime
internal val User.info: UserInfo? get() = this.castOrNull<AbstractUser>()?.info
@ -53,7 +54,7 @@ internal abstract class AbstractUser(
if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
}
val response = bot.network.run {
val resp = bot.network.run {
LongConn.OffPicUp(
bot.client, Cmd0x352.TryUpImgReq(
srcUin = bot.id.toInt(),
@ -73,56 +74,64 @@ internal abstract class AbstractUser(
is Member -> "temp"
else -> "unknown"
}
return when (response) {
return when (resp) {
is LongConn.OffPicUp.Response.FileExists -> OfflineFriendImage(
imageId = generateImageIdFromResourceId(
resourceId = response.resourceId,
format = getImageType(response.imageInfo.fileType).takeIf { it != ExternalResource.DEFAULT_FORMAT_NAME }
resourceId = resp.resourceId,
format = getImageType(resp.imageInfo.fileType).takeIf { it != ExternalResource.DEFAULT_FORMAT_NAME }
?: resource.formatName
) ?: response.resourceId
) ?: resp.resourceId
).also {
ImageUploadEvent.Succeed(this, resource, it).broadcast()
}
is LongConn.OffPicUp.Response.RequireUpload -> {
bot.network.logger.verbose {
"[Http] Uploading $kind image, size=${resource.size.sizeToString()}"
}
val time = measureTime {
kotlin.runCatching {
Highway.uploadResourceBdh(
bot = bot,
resource = resource,
kind = PRIVATE_IMAGE,
commandId = 1,
initialTicket = resp.uKey
)
}.recoverCatchingSuppressed {
tryServers(
bot = bot,
servers = resp.serverIp.zip(resp.serverPort),
resourceSize = resource.size,
resourceKind = PRIVATE_IMAGE,
channelKind = ChannelKind.HTTP
) { ip, port ->
Mirai.Http.postImage(
"0x6ff0070",
bot.id,
null,
serverIp = ip, serverPort = port,
htcmd = "0x6ff0070",
uin = bot.id,
groupcode = null,
imageInput = resource,
uKeyHex = response.uKey.toUHexString("")
uKeyHex = resp.uKey.toUHexString("")
)
}
bot.network.logger.verbose {
"[Http] Uploading $kind image: succeed at ${(resource.size.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s"
}
/*
HighwayHelper.uploadImageToServers(
bot,
response.serverIp.zip(response.serverPort),
response.uKey,
image,
kind = "friend",
commandId = 1
)*/
// 为什么不能 ??
}.recoverCatchingSuppressed {
Mirai.Http.postImage(
serverIp = "htdata2.qq.com",
htcmd = "0x6ff0070",
uin = bot.id,
groupcode = null,
imageInput = resource,
uKeyHex = resp.uKey.toUHexString("")
)
}.getOrThrow()
OfflineFriendImage(
generateImageIdFromResourceId(response.resourceId, resource.formatName) ?: response.resourceId
generateImageIdFromResourceId(resp.resourceId, resource.formatName) ?: resp.resourceId
).also {
ImageUploadEvent.Succeed(this, resource, it).broadcast()
}
}
is LongConn.OffPicUp.Response.Failed -> {
ImageUploadEvent.Failed(this, resource, -1, response.message).broadcast()
error(response.message)
ImageUploadEvent.Failed(this, resource, -1, resp.message).broadcast()
error(resp.message)
}
}
}

View File

@ -13,6 +13,7 @@
package net.mamoe.mirai.internal.contact
import net.mamoe.mirai.LowLevelApi
import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo
@ -21,12 +22,16 @@ import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.internal.network.highway.Highway
import net.mamoe.mirai.internal.network.highway.*
import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_IMAGE
import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_VOICE
import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x388
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.*
@ -152,16 +157,16 @@ internal class GroupImpl(
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
}
is ImgStore.GroupPicUp.Response.RequireUpload -> {
Highway.uploadResource(
bot,
response.uploadIpList.zip(response.uploadPortList),
response.uKey,
resource,
kind = "group image",
commandId = 2
// val servers = response.uploadIpList.zip(response.uploadPortList)
Highway.uploadResourceBdh(
bot = bot,
resource = resource,
kind = GROUP_IMAGE,
commandId = 2,
initialTicket = response.uKey
)
val resourceId = resource.calculateResourceId()
return OfflineGroupImage(imageId = resourceId)
return OfflineGroupImage(imageId = resource.calculateResourceId())
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
}
}
@ -169,20 +174,36 @@ internal class GroupImpl(
}
override suspend fun uploadVoice(resource: ExternalResource): Voice {
if (resource.size > 1048576) {
throw OverFileSizeMaxException()
}
return bot.network.run {
val response: PttStore.GroupPttUp.Response.RequireUpload =
PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect()
Highway.uploadPttToServers(
bot,
response.uploadIpList.zip(response.uploadPortList),
resource,
response.uKey,
response.fileKey,
kotlin.runCatching {
val (_) = Highway.uploadResourceBdh(
bot = bot,
resource = resource,
kind = GROUP_VOICE,
commandId = 29,
extendInfo = PttStore.GroupPttUp.createTryUpPttPack(bot.id, id, resource)
.toByteArray(Cmd0x388.ReqBody.serializer()),
)
}.recoverCatchingSuppressed {
when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect()) {
is PttStore.GroupPttUp.Response.RequireUpload -> {
tryServers(
bot,
resp.uploadIpList.zip(resp.uploadPortList),
resource.size,
GROUP_VOICE,
ChannelKind.HTTP
) { ip, port ->
Mirai.Http.postPtt(ip, port, resource, resp.uKey, resp.fileKey)
}
}
}
}.getOrThrow()
// val body = resp?.loadAs(Cmd0x388.RspBody.serializer())
// ?.msgTryupPttRsp
// ?.singleOrNull()?.fileKey ?: error("Group voice highway transfer succeed but failed to find fileKey")
Voice(
"${resource.md5.toUHexString("")}.amr",
resource.md5,

View File

@ -213,21 +213,27 @@ private suspend fun MessageChain.convertToLongMessageIfNeeded(
step: GroupMessageSendingStep,
groupImpl: GroupImpl,
): MessageChain {
return when (step) {
GroupMessageSendingStep.FIRST -> {
// 只需要在第一次发送的时候验证长度
// 后续重试直接跳过
verityLength(this, groupImpl)
this
}
GroupMessageSendingStep.LONG_MESSAGE -> {
suspend fun sendLongImpl(): MessageChain {
val resId = groupImpl.uploadGroupLongMessageHighway(this)
this + RichMessage.longMessage(
return this + RichMessage.longMessage(
brief = takeContent(27),
resId = resId,
timeSeconds = currentTimeSeconds()
) // LongMessageInternal replaces all contents and preserves metadata
}
return when (step) {
GroupMessageSendingStep.FIRST -> {
// 只需要在第一次发送的时候验证长度
// 后续重试直接跳过
if (contains(ForceAsLongMessage)) {
sendLongImpl()
}
verityLength(this, groupImpl)
this
}
GroupMessageSendingStep.LONG_MESSAGE -> {
sendLongImpl()
}
GroupMessageSendingStep.FRAGMENTED -> this
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2019-2021 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.message
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.safeCast
/**
* 内部 flag, 放入 chain 强制作为 long 发送
*/
internal object ForceAsLongMessage : MessageMetadata, ConstrainSingle, InternalFlagOnlyMessage,
AbstractMessageKey<ForceAsLongMessage>({ it.safeCast() }) {
override val key: MessageKey<ForceAsLongMessage> get() = this
override fun toString(): String = "ForceLongMessage"
}
/**
* Ignore on transformation
*/
internal interface InternalFlagOnlyMessage : SingleMessage

View File

@ -243,6 +243,9 @@ internal fun MessageChain.toRichTextElems(
is RichMessage // already transformed above
-> {
}
is InternalFlagOnlyMessage -> {
// ignore
}
else -> error("unsupported message type: ${currentMessage::class.simpleName}")
}

View File

@ -447,19 +447,26 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
this@QQAndroidBotNetworkHandler.launch(CoroutineName("Awaiting ConfigPushSvc.PushReq")) {
logger.info { "Awaiting ConfigPushSvc.PushReq." }
when (val resp: ConfigPushSvc.PushReq.PushReqResponse? = nextEventOrNull(10_000)) {
null -> logger.info { "Missing ConfigPushSvc.PushReq." }
null -> {
kotlin.runCatching { bot.client.bdhSession.completeExceptionally(TimeoutCancellationException("Timeout waiting for ConfigPushSvc.PushReq")) }
logger.warning { "Missing ConfigPushSvc.PushReq. File uploading may be affected." }
}
is ConfigPushSvc.PushReq.PushReqResponse.Success -> {
logger.info { "ConfigPushSvc.PushReq: Success." }
}
is ConfigPushSvc.PushReq.PushReqResponse.ChangeServer -> {
bot.logger.info { "Server requires reconnect." }
logger.debug { "ChangeServer.unknown = ${resp.unknown}." }
bot.logger.info { "Server list: ${resp.serverList.joinToString()}." }
if (resp.serverList.isNotEmpty()) {
bot.serverList.clear()
resp.serverList.forEach {
bot.client.serverList.add(it.host to it.port)
bot.serverList.add(it.host to it.port)
}
BotOfflineEvent.RequireReconnect(bot).broadcast()
}
bot.launch { BotOfflineEvent.RequireReconnect(bot).broadcast() }
return@launch
}
}
}

View File

@ -14,21 +14,21 @@ package net.mamoe.mirai.internal.network
import kotlinx.atomicfu.AtomicBoolean
import kotlinx.atomicfu.AtomicInt
import kotlinx.atomicfu.atomic
import kotlinx.io.core.*
import kotlinx.coroutines.CompletableDeferred
import kotlinx.io.core.String
import kotlinx.io.core.toByteArray
import net.mamoe.mirai.data.OnlineStatus
import net.mamoe.mirai.internal.BotAccount
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.protocol.SyncingCacheList
import net.mamoe.mirai.internal.network.protocol.data.jce.FileStoragePushFSSvcListFuckKotlin
import net.mamoe.mirai.internal.network.protocol.data.jce.FileStoragePushFSSvcList
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.internal.network.protocol.packet.PacketLogger
import net.mamoe.mirai.internal.network.protocol.packet.Tlv
import net.mamoe.mirai.internal.utils.*
import net.mamoe.mirai.internal.utils.MiraiProtocolInternal
import net.mamoe.mirai.internal.utils.NetworkType
import net.mamoe.mirai.internal.utils.crypto.ECDH
import net.mamoe.mirai.internal.utils.crypto.TEA
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.NoServerAvailableException
import net.mamoe.mirai.utils.*
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.random.Random
internal val DeviceInfo.guid: ByteArray get() = generateGuid(androidId, macAddress)
@ -82,36 +82,8 @@ internal open class QQAndroidClient(
get() = protocol.id
internal var strangerSeq: Int = 0
internal val serverList: MutableList<Pair<String, Int>> = DefaultServerList.toMutableList()
val keys: Map<String, ByteArray> by lazy {
mapOf(
"16 zero" to ByteArray(16),
"D2 key" to wLoginSigInfo.d2Key,
"wtSessionTicketKey" to wLoginSigInfo.wtSessionTicketKey,
"userStKey" to wLoginSigInfo.userStKey,
"tgtgtKey" to tgtgtKey,
"tgtKey" to wLoginSigInfo.tgtKey,
"deviceToken" to wLoginSigInfo.deviceToken,
"shareKeyCalculatedByConstPubKey" to ecdh.keyPair.initialShareKey
//"t108" to wLoginSigInfo.t1,
//"t10c" to t10c,
//"t163" to t163
)
}
internal inline fun <R> tryDecryptOrNull(data: ByteArray, size: Int = data.size, mapper: (ByteArray) -> R): R? {
keys.forEach { (key, value) ->
kotlin.runCatching {
return mapper(TEA.decrypt(data, value, size).also { PacketLogger.verbose { "成功使用 $key 解密" } })
}
}
return null
}
override fun toString(): String { // extremely slow
return "QQAndroidClient(account=$account, ecdh=$ecdh, device=$device, tgtgtKey=${tgtgtKey.toUHexString()}, randomKey=${randomKey.toUHexString()}, miscBitMap=$miscBitMap, mainSigMap=$mainSigMap, subSigMap=$subSigMap, openAppId=$openAppId, apkVersionName=${apkVersionName.toUHexString()}, loginState=$loginState, appClientVersion=$appClientVersion, networkType=$networkType, apkSignatureMd5=${apkSignatureMd5.toUHexString()}, protocolVersion=$protocolVersion, apkId=${apkId.toUHexString()}, t150=${t150?.value?.toUHexString()}, rollbackSig=${rollbackSig?.toUHexString()}, ipFromT149=${ipFromT149?.toUHexString()}, timeDifference=$timeDifference, uin=$uin, t530=${t530?.toUHexString()}, t528=${t528?.toUHexString()}, ksid='$ksid', pwdFlag=$pwdFlag, loginExtraData=$loginExtraData, wFastLoginInfo=$wFastLoginInfo, reserveUinInfo=$reserveUinInfo, wLoginSigInfo=$wLoginSigInfo, tlv113=${tlv113?.toUHexString()}, qrPushSig=${qrPushSig.toUHexString()}, mainDisplayName='$mainDisplayName')"
}
val keys: Map<String, ByteArray> by lazy { allKeys() }
var onlineStatus: OnlineStatus = OnlineStatus.ONLINE
@ -126,33 +98,7 @@ internal open class QQAndroidClient(
private val _ssoSequenceId: AtomicInt = atomic(85600)
lateinit var fileStoragePushFSSvcList: FileStoragePushFSSvcListFuckKotlin
internal suspend inline fun useNextServers(crossinline block: suspend (host: String, port: Int) -> Unit) {
if (bot.client.serverList.isEmpty()) {
bot.client.serverList.addAll(DefaultServerList)
}
retryCatchingExceptions(bot.client.serverList.size, except = LoginFailedException::class) l@{
val pair = bot.client.serverList[0]
runCatchingExceptions {
block(pair.first, pair.second)
return@l
}.getOrElse {
bot.client.serverList.remove(pair)
if (it !is LoginFailedException) {
// 不要重复打印.
bot.logger.warning(it)
}
throw it
}
}.getOrElse {
if (it is LoginFailedException) {
throw it
}
bot.client.serverList.addAll(DefaultServerList)
throw NoServerAvailableException(it)
}
}
var fileStoragePushFSSvcList: FileStoragePushFSSvcList? = null
@MiraiInternalApi("Do not use directly. Get from the lambda param of buildSsoPacket")
internal fun nextSsoSequenceId() = _ssoSequenceId.addAndGet(2)
@ -318,7 +264,6 @@ internal open class QQAndroidClient(
lateinit var wFastLoginInfo: WFastLoginInfo
var reserveUinInfo: ReserveUinInfo? = null
lateinit var wLoginSigInfo: WLoginSigInfo
var tlv113: ByteArray? = null
/**
* from tlvMap119
@ -331,193 +276,17 @@ internal open class QQAndroidClient(
var transportSequenceId = 1
lateinit var t104: ByteArray
}
@Suppress("RemoveRedundantQualifierName") // bug
internal fun generateTgtgtKey(guid: ByteArray): ByteArray =
(getRandomByteArray(16) + guid).md5()
internal class ReserveUinInfo(
val imgType: ByteArray,
val imgFormat: ByteArray,
val imgUrl: ByteArray
) {
override fun toString(): String {
return "ReserveUinInfo(imgType=${imgType.toUHexString()}, imgFormat=${imgFormat.toUHexString()}, imgUrl=${imgUrl.toUHexString()})"
}
}
internal class WFastLoginInfo(
val outA1: ByteReadPacket,
var adUrl: String = "",
var iconUrl: String = "",
var profileUrl: String = "",
var userJson: String = ""
) {
override fun toString(): String {
return "WFastLoginInfo(outA1=$outA1, adUrl='$adUrl', iconUrl='$iconUrl', profileUrl='$profileUrl', userJson='$userJson')"
}
}
internal class WLoginSimpleInfo(
val uin: Long, // uin
val face: Int, // ubyte actually
val age: Int, // ubyte
val gender: Int, // ubyte
val nick: String, // ubyte lv string
val imgType: ByteArray,
val imgFormat: ByteArray,
val imgUrl: ByteArray,
val mainDisplayName: ByteArray
) {
override fun toString(): String {
return "WLoginSimpleInfo(uin=$uin, face=$face, age=$age, gender=$gender, nick='$nick', imgType=${imgType.toUHexString()}, imgFormat=${imgFormat.toUHexString()}, imgUrl=${imgUrl.toUHexString()}, mainDisplayName=${mainDisplayName.toUHexString()})"
}
}
internal class LoginExtraData(
val uin: Long,
val ip: ByteArray,
val time: Int,
val version: Int
) {
override fun toString(): String {
return "LoginExtraData(uin=$uin, ip=${ip.toUHexString()}, time=$time, version=$version)"
}
}
internal class WLoginSigInfo(
val uin: Long,
val encryptA1: ByteArray?, // sigInfo[0]
/**
* WARNING, please check [QQAndroidClient.tlv16a]
* from ConfigPush.PushReq
*/
val noPicSig: ByteArray?, // sigInfo[1]
val G: ByteArray, // sigInfo[2]
val dpwd: ByteArray,
val randSeed: ByteArray,
val simpleInfo: WLoginSimpleInfo,
val appPri: Long,
val a2ExpiryTime: Long,
val loginBitmap: Long,
val tgt: ByteArray,
val a2CreationTime: Long,
val tgtKey: ByteArray,
val userStSig: UserStSig,
/**
* TransEmpPacket 加密使用
*/
val userStKey: ByteArray,
val userStWebSig: UserStWebSig,
val userA5: UserA5,
val userA8: UserA8,
val lsKey: LSKey,
val sKey: SKey,
val userSig64: UserSig64,
val openId: ByteArray,
val openKey: OpenKey,
val vKey: VKey,
val accessToken: AccessToken,
val d2: D2,
val d2Key: ByteArray,
val sid: Sid,
val aqSig: AqSig,
val psKeyMap: PSKeyMap,
val pt4TokenMap: Pt4TokenMap,
val superKey: ByteArray,
val payToken: ByteArray,
val pf: ByteArray,
val pfKey: ByteArray,
val da2: ByteArray,
// val pt4Token: ByteArray,
val wtSessionTicket: WtSessionTicket,
val wtSessionTicketKey: ByteArray,
val deviceToken: ByteArray
) {
override fun toString(): String {
return "WLoginSigInfo(uin=$uin, encryptA1=${encryptA1?.toUHexString()}, noPicSig=${noPicSig?.toUHexString()}, G=${G.toUHexString()}, dpwd=${dpwd.toUHexString()}, randSeed=${randSeed.toUHexString()}, simpleInfo=$simpleInfo, appPri=$appPri, a2ExpiryTime=$a2ExpiryTime, loginBitmap=$loginBitmap, tgt=${tgt.toUHexString()}, a2CreationTime=$a2CreationTime, tgtKey=${tgtKey.toUHexString()}, userStSig=$userStSig, userStKey=${userStKey.toUHexString()}, userStWebSig=$userStWebSig, userA5=$userA5, userA8=$userA8, lsKey=$lsKey, sKey=$sKey, userSig64=$userSig64, openId=${openId.toUHexString()}, openKey=$openKey, vKey=$vKey, accessToken=$accessToken, d2=$d2, d2Key=${d2Key.toUHexString()}, sid=$sid, aqSig=$aqSig, psKey=$psKeyMap, superKey=${superKey.toUHexString()}, payToken=${payToken.toUHexString()}, pf=${pf.toUHexString()}, pfKey=${pfKey.toUHexString()}, da2=${da2.toUHexString()}, wtSessionTicket=$wtSessionTicket, wtSessionTicketKey=${wtSessionTicketKey.toUHexString()}, deviceToken=${deviceToken.toUHexString()})"
}
@JvmField
val bdhSession: CompletableDeferred<BdhSession> = CompletableDeferred()
}
internal class UserStSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class LSKey(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class UserStWebSig(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class UserA8(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class UserA5(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class SKey(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class UserSig64(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class OpenKey(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class VKey(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class AccessToken(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class D2(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
internal class Sid(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class AqSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class Pt4Token(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal typealias PSKeyMap = MutableMap<String, PSKey>
internal typealias Pt4TokenMap = MutableMap<String, Pt4Token>
internal inline fun Input.readUShortLVString(): String = kotlinx.io.core.String(this.readUShortLVByteArray())
internal inline fun Input.readUShortLVByteArray(): ByteArray = this.readBytes(this.readUShort().toInt())
internal fun parsePSKeyMapAndPt4TokenMap(
data: ByteArray,
creationTime: Long,
expireTime: Long,
outPSKeyMap: PSKeyMap,
outPt4TokenMap: Pt4TokenMap
) =
data.read {
repeat(readShort().toInt()) {
val domain = readUShortLVString()
val psKey = readUShortLVByteArray()
val pt4token = readUShortLVByteArray()
when {
psKey.isNotEmpty() -> outPSKeyMap[domain] = PSKey(psKey, creationTime, expireTime)
pt4token.isNotEmpty() -> outPt4TokenMap[domain] = Pt4Token(pt4token, creationTime, expireTime)
}
}
}
internal class PSKey(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class WtSessionTicket(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal open class KeyWithExpiry(
data: ByteArray,
creationTime: Long,
val expireTime: Long
) : KeyWithCreationTime(data, creationTime) {
override fun toString(): String {
return "KeyWithExpiry(data=${data.toUHexString()}, creationTime=$creationTime)"
}
}
internal open class KeyWithCreationTime(
val data: ByteArray,
val creationTime: Long
) {
override fun toString(): String {
return "KeyWithCreationTime(data=${data.toUHexString()}, creationTime=$creationTime)"
}
}
internal class BdhSession(
val sigSession: ByteArray,
val sessionKey: ByteArray,
var ssoAddresses: MutableSet<Pair<Int, Int>> = CopyOnWriteArraySet(),
var otherAddresses: MutableSet<Pair<Int, Int>> = CopyOnWriteArraySet(),
)

View File

@ -9,10 +9,16 @@
package net.mamoe.mirai.internal.network.highway
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.io.core.Closeable
import net.mamoe.mirai.utils.runBIO
import net.mamoe.mirai.utils.toLongUnsigned
import net.mamoe.mirai.utils.withUse
import java.io.InputStream
import java.util.concurrent.atomic.AtomicLong
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
internal class ChunkedFlowSession<T>(
private val input: InputStream,
@ -23,14 +29,18 @@ internal class ChunkedFlowSession<T>(
input.close()
}
private var offset = 0L
private var offset = AtomicLong(0L)
internal suspend inline fun useAll(crossinline block: suspend (T) -> Unit) = withUse {
internal suspend inline fun useAll(crossinline block: suspend (T) -> Unit) {
contract { callsInPlace(block, InvocationKind.UNKNOWN) }
withUse {
while (true) {
val size = runBIO { input.read(buffer) }
if (size == -1) return
block(mapper(buffer, size, offset))
offset += size
block(mapper(buffer, size, offset.getAndAdd(size.toLongUnsigned())))
}
}
}
internal suspend fun asFlow(): Flow<T> = flow { useAll { emit(it) } } // 'single thread' producer
}

View File

@ -9,146 +9,143 @@
package net.mamoe.mirai.internal.network.highway
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.utils.io.*
import io.ktor.utils.io.jvm.javaio.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.io.core.*
import net.mamoe.mirai.Mirai
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.receiveOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.produceIn
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.buildPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.writeFully
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.data.proto.CSDataHighwayHead
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
import net.mamoe.mirai.internal.utils.PlatformSocket
import net.mamoe.mirai.internal.utils.SocketException
import net.mamoe.mirai.internal.utils.crypto.TEA
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.internal.utils.toIpV4AddressString
import net.mamoe.mirai.internal.utils.retryWithServers
import net.mamoe.mirai.internal.utils.sizeToString
import net.mamoe.mirai.utils.*
import java.io.InputStream
import java.util.concurrent.atomic.AtomicReference
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.math.roundToInt
import kotlin.time.measureTime
/**
* 在发送完成后将会 [InputStream.close]
*/
internal fun ExternalResource.consumeAsWriteChannelContent(contentType: ContentType?): OutgoingContent.WriteChannelContent {
return object : OutgoingContent.WriteChannelContent() {
override val contentType: ContentType? = contentType
override val contentLength: Long = size
override suspend fun writeTo(channel: ByteWriteChannel) {
inputStream().withUse { copyTo(channel) }
}
}
}
@Suppress("SpellCheckingInspection")
internal suspend fun HttpClient.postImage(
htcmd: String,
uin: Long,
groupcode: Long?,
imageInput: ExternalResource,
uKeyHex: String
): Boolean = post<HttpStatusCode> {
url {
protocol = URLProtocol.HTTP
host = "htdata2.qq.com"
path("cgi-bin/httpconn")
parameters["htcmd"] = htcmd
parameters["uin"] = uin.toString()
if (groupcode != null) parameters["groupcode"] = groupcode.toString()
parameters["term"] = "pc"
parameters["ver"] = "5603"
parameters["filesize"] = imageInput.size.toString()
parameters["range"] = 0.toString()
parameters["ukey"] = uKeyHex
userAgent("QQClient")
}
body = imageInput.consumeAsWriteChannelContent(ContentType.Image.Any)
} == HttpStatusCode.OK
internal object Highway {
suspend fun uploadResource(
bot: QQAndroidBot,
servers: List<Pair<Int, Int>>,
uKey: ByteArray,
resource: ExternalResource,
kind: String,
commandId: Int
) = servers.retryWithServers(
(resource.size * 1000 / 1024 / 10).coerceAtLeast(5000),
onFail = {
throw IllegalStateException("cannot upload $kind, failed on all servers.", it)
}
) { ip, port ->
bot.network.logger.verbose {
"[Highway] Uploading $kind to ${ip}:$port, size=${resource.size.sizeToString()}"
}
val time = measureTime {
uploadResourceImpl(
client = bot.client,
serverIp = ip,
serverPort = port,
resource = resource,
fileMd5 = resource.md5,
ticket = uKey,
commandId = commandId
@Suppress("ArrayInDataClass")
data class BdhUploadResponse(
var extendInfo: ByteArray? = null,
)
}
bot.network.logger.verbose {
"[Highway] Uploading $kind: succeed at ${(resource.size.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s"
}
}
private suspend fun uploadResourceImpl(
client: QQAndroidClient,
serverIp: String,
serverPort: Int,
ticket: ByteArray,
suspend fun uploadResourceBdh(
bot: QQAndroidBot,
resource: ExternalResource,
fileMd5: ByteArray,
commandId: Int // group=2, friend=1
) {
require(fileMd5.size == 16) { "bad md5. Required size=16, got ${fileMd5.size}" }
// require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" }
// require(commandId == 2 || commandId == 1) { "bad commandId. Must be 1 or 2" }
kind: ResourceKind,
commandId: Int, // group image=2, friend image=1, groupPtt=29
extendInfo: ByteArray = EMPTY_BYTE_ARRAY,
encrypt: Boolean = false,
initialTicket: ByteArray? = null,
): BdhUploadResponse {
val bdhSession = bot.client.bdhSession.await() // no need to care about timeout. proceed by bot init
val socket = PlatformSocket()
while (client.bot.network.areYouOk() && client.bot.isActive) {
try {
socket.connect(serverIp, serverPort)
break
} catch (e: SocketException) {
delay(3000)
}
}
socket.use {
createImageDataPacketSequence(
client = client,
appId = client.subAppId.toInt(),
return tryServers(bot, bdhSession.ssoAddresses, resource.size, kind, ChannelKind.HIGHWAY) { ip, port ->
val md5 = resource.md5
require(md5.size == 16) { "bad md5. Required size=16, got ${md5.size}" }
val resp = BdhUploadResponse()
highwayPacketSession(
client = bot.client,
appId = bot.client.subAppId.toInt(),
command = "PicUp.DataUp",
commandId = commandId,
ticket = ticket,
initialTicket = initialTicket ?: bdhSession.sigSession,
data = resource,
fileMd5 = fileMd5
).useAll {
fileMd5 = md5,
extendInfo = if (encrypt) TEA.encrypt(extendInfo, bdhSession.sessionKey) else extendInfo
).sendConcurrently(
createConnection = { PlatformSocket.connect(ip, port) },
coroutines = bot.configuration.highwayUploadCoroutineCount
) { head ->
if (head.rspExtendinfo.isNotEmpty()) {
resp.extendInfo = head.rspExtendinfo
}
}
resp
}
}
}
internal enum class ResourceKind(
private val display: String
) {
PRIVATE_IMAGE("private image"),
GROUP_IMAGE("group image"),
PRIVATE_VOICE("private voice"),
GROUP_VOICE("group voice"),
GROUP_LONG_MESSAGE("group long message"),
GROUP_FORWARD_MESSAGE("group forward message"),
;
override fun toString(): String = display
}
internal enum class ChannelKind(
private val display: String
) {
HIGHWAY("Highway"),
HTTP("Http")
;
override fun toString(): String = display
}
internal suspend inline fun <reified R> tryServers(
bot: QQAndroidBot,
servers: Collection<Pair<Int, Int>>,
resourceSize: Long,
resourceKind: ResourceKind,
channelKind: ChannelKind,
crossinline implOnEachServer: suspend (ip: String, port: Int) -> R
) = servers.retryWithServers(
(resourceSize * 1000 / 1024 / 10).coerceAtLeast(5000),
onFail = { throw IllegalStateException("cannot upload $resourceKind, failed on all servers.", it) }
) { ip, port ->
bot.network.logger.verbose {
"[${channelKind}] Uploading $resourceKind to ${ip}:$port, size=${resourceSize.sizeToString()}"
}
var resp: R? = null
val time = measureTime {
runCatching {
resp = implOnEachServer(ip, port)
}.onFailure {
bot.network.logger.verbose {
"[${channelKind}] Uploading $resourceKind to ${ip}:$port, size=${resourceSize.sizeToString()} failed: $it"
}
throw it
}
}
bot.network.logger.verbose {
"[${channelKind}] Uploading $resourceKind: succeed at ${(resourceSize.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s"
}
resp as R
}
internal suspend fun ChunkedFlowSession<ByteReadPacket>.sendSequentially(
socket: PlatformSocket,
respCallback: (resp: CSDataHighwayHead.RspDataHighwayHead) -> Unit = {}
) {
contract { callsInPlace(respCallback, InvocationKind.UNKNOWN) }
useAll {
socket.send(it)
//0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00
@ -158,58 +155,78 @@ internal object Highway {
discardExact(4)
val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength)
check(proto.errorCode == 0) { "highway transfer failed, error ${proto.errorCode}" }
respCallback(proto)
}
}
}
private fun <T> Flow<T>.produceIn0(coroutineScope: CoroutineScope): ReceiveChannel<T> {
return kotlin.runCatching {
@OptIn(FlowPreview::class)
produceIn(coroutineScope) // this is experimental api
}.getOrElse {
// fallback strategy in case binary changes.
val channel = Channel<T>()
coroutineScope.launch(CoroutineName("Flow collector")) {
collect {
channel.send(it)
}
channel.close()
}
channel
}
}
internal suspend fun ChunkedFlowSession<ByteReadPacket>.sendConcurrently(
createConnection: suspend () -> PlatformSocket,
coroutines: Int = 5,
respCallback: (resp: CSDataHighwayHead.RspDataHighwayHead) -> Unit = {}
) = coroutineScope {
val channel = asFlow().produceIn0(this)
// 'single thread' producer emits chunks to channel
repeat(coroutines) {
launch(CoroutineName("Worker $it")) {
val socket = createConnection()
while (isActive) {
val next = channel.tryReceive() ?: break // concurrent-safe receive
val result = next.withUse {
socket.sendReceiveHighway(next)
}
respCallback(result)
}
}
}
}
suspend fun uploadPttToServers(
bot: QQAndroidBot,
servers: List<Pair<Int, Int>>,
resource: ExternalResource,
uKey: ByteArray,
fileKey: ByteArray,
) {
servers.retryWithServers(10 * 1000, {
throw IllegalStateException("cannot upload ptt, failed on all servers.", it)
}, { s: String, i: Int ->
bot.network.logger.verbose {
"[Highway] Uploading ptt to ${s}:$i, size=${resource.size.sizeToString()}"
}
val time = measureTime {
uploadPttToServer(s, i, resource, uKey, fileKey)
}
bot.network.logger.verbose {
"[Highway] Uploading ptt: succeed at ${(resource.size.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s"
private suspend fun <E : Any> ReceiveChannel<E>.tryReceive(): E? {
return kotlin.runCatching {
@OptIn(ExperimentalCoroutinesApi::class)
receiveOrNull() // this is experimental api
}.recoverCatching {
// in case binary changes
receive()
}.getOrNull()
}
})
private suspend fun PlatformSocket.sendReceiveHighway(
it: ByteReadPacket,
): CSDataHighwayHead.RspDataHighwayHead {
send(it)
//0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00
}
private suspend fun uploadPttToServer(
serverIp: String,
serverPort: Int,
resource: ExternalResource,
uKey: ByteArray,
fileKey: ByteArray,
) {
Mirai.Http.post<String> {
url("http://$serverIp:$serverPort")
parameter("ver", 4679)
parameter("ukey", uKey.toUHexString(""))
parameter("filekey", fileKey.toUHexString(""))
parameter("filesize", resource.size)
parameter("bmd5", resource.md5.toUHexString(""))
parameter("mType", "pttDu")
parameter("voice_encodec", resource.voiceCodec)
body = resource.consumeAsWriteChannelContent(null)
}
read().withUse {
discardExact(1)
val headLength = readInt()
discardExact(4)
val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength)
check(proto.errorCode == 0) { "highway transfer failed, error ${proto.errorCode}" }
return proto
}
}
internal fun createImageDataPacketSequence(
internal fun highwayPacketSession(
// RequestDataTrans
client: QQAndroidClient,
command: String,
@ -217,14 +234,17 @@ internal fun createImageDataPacketSequence(
dataFlag: Int = 4096,
commandId: Int,
localId: Int = 2052,
ticket: ByteArray,
initialTicket: ByteArray,
data: ExternalResource,
fileMd5: ByteArray,
sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE
sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE,
extendInfo: ByteArray = EMPTY_BYTE_ARRAY,
): ChunkedFlowSession<ByteReadPacket> {
ByteArrayPool.checkBufferSize(sizePerPacket)
// require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" }
val ticket = AtomicReference(initialTicket)
return ChunkedFlowSession(data.inputStream(), ByteArray(sizePerPacket)) { buffer, size, offset ->
val head = CSDataHighwayHead.ReqDataHighwayHead(
msgBasehead = CSDataHighwayHead.DataHighwayHead(
@ -235,6 +255,7 @@ internal fun createImageDataPacketSequence(
2 -> client.nextHighwayDataTransSequenceIdForGroup()
1 -> client.nextHighwayDataTransSequenceIdForFriend()
27 -> client.nextHighwayDataTransSequenceIdForApplyUp()
29 -> client.nextHighwayDataTransSequenceIdForGroup()
else -> error("illegal commandId: $commandId")
},
retryTimes = 0,
@ -248,13 +269,13 @@ internal fun createImageDataPacketSequence(
datalength = size,
dataoffset = offset,
filesize = data.size,
serviceticket = ticket,
serviceticket = ticket.get(),
md5 = buffer.md5(0, size),
fileMd5 = fileMd5,
flag = 0,
rtcode = 0
),
reqExtendinfo = EMPTY_BYTE_ARRAY,
reqExtendinfo = extendInfo,
msgLoginSigHead = null
).toByteArray(CSDataHighwayHead.ReqDataHighwayHead.serializer())
@ -268,35 +289,3 @@ internal fun createImageDataPacketSequence(
}
}
}
internal suspend inline fun List<Pair<Int, Int>>.retryWithServers(
timeoutMillis: Long,
onFail: (exception: Throwable?) -> Unit,
crossinline block: suspend (ip: String, port: Int) -> Unit
) {
require(this.isNotEmpty()) { "receiver of retryWithServers must not be empty" }
var exception: Throwable? = null
for (pair in this) {
return kotlin.runCatching {
withTimeoutOrNull(timeoutMillis) {
block(pair.first.toIpV4AddressString(), pair.second)
}
}.recover {
if (exception != null) {
exception!!.addSuppressed(it)
}
exception = it
null
}.getOrNull() ?: continue
}
onFail(exception)
}
internal fun Int.sizeToString() = this.toLong().sizeToString()
internal fun Long.sizeToString(): String {
return if (this < 1024) {
"$this B"
} else ((this * 100.0 / 1024).roundToInt() / 100.0).toString() + " KiB"
}

View File

@ -0,0 +1,92 @@
/*
* Copyright 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.network.highway
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.utils.io.*
import io.ktor.utils.io.jvm.javaio.*
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
import net.mamoe.mirai.utils.ExternalResource
import net.mamoe.mirai.utils.toUHexString
import net.mamoe.mirai.utils.withUse
import java.io.InputStream
/**
* 在发送完成后将会 [InputStream.close]
*/
internal fun ExternalResource.consumeAsWriteChannelContent(contentType: ContentType?): OutgoingContent.WriteChannelContent {
return object : OutgoingContent.WriteChannelContent() {
override val contentType: ContentType? = contentType
override val contentLength: Long = size
override suspend fun writeTo(channel: ByteWriteChannel) {
inputStream().withUse { copyTo(channel) }
}
}
}
internal val FALLBACK_HTTP_SERVER = "htdata2.qq.com" to 0
@Suppress("SpellCheckingInspection")
internal suspend fun HttpClient.postImage(
serverIp: String,
serverPort: Int = DEFAULT_PORT,
htcmd: String,
uin: Long,
groupcode: Long?,
imageInput: ExternalResource,
uKeyHex: String
): Boolean = post<HttpStatusCode> {
url {
protocol = URLProtocol.HTTP
host = serverIp // "htdata2.qq.com"
port = serverPort
path("cgi-bin/httpconn")
parameters["htcmd"] = htcmd
parameters["uin"] = uin.toString()
if (groupcode != null) parameters["groupcode"] = groupcode.toString()
parameters["term"] = "pc"
parameters["ver"] = "5603"
parameters["filesize"] = imageInput.size.toString()
parameters["range"] = 0.toString()
parameters["ukey"] = uKeyHex
userAgent("QQClient")
}
body = imageInput.consumeAsWriteChannelContent(ContentType.Image.Any)
} == HttpStatusCode.OK
internal suspend fun HttpClient.postPtt(
serverIp: String,
serverPort: Int,
resource: ExternalResource,
uKey: ByteArray,
fileKey: ByteArray,
) {
post<String> {
url("http://$serverIp:$serverPort")
parameter("ver", 4679)
parameter("ukey", uKey.toUHexString(""))
parameter("filekey", fileKey.toUHexString(""))
parameter("filesize", resource.size)
parameter("bmd5", resource.md5.toUHexString(""))
parameter("mType", "pttDu")
parameter("voice_encodec", resource.voiceCodec)
body = resource.consumeAsWriteChannelContent(null)
}
}

View File

@ -0,0 +1,264 @@
/*
* Copyright 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.network
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.Input
import kotlinx.io.core.readBytes
import kotlinx.io.core.readUShort
import net.mamoe.mirai.internal.network.getRandomByteArray
import net.mamoe.mirai.internal.network.protocol.packet.PacketLogger
import net.mamoe.mirai.internal.utils.crypto.TEA
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.NoServerAvailableException
import net.mamoe.mirai.utils.*
internal class ReserveUinInfo(
val imgType: ByteArray,
val imgFormat: ByteArray,
val imgUrl: ByteArray
) {
override fun toString(): String {
return "ReserveUinInfo(imgType=${imgType.toUHexString()}, imgFormat=${imgFormat.toUHexString()}, imgUrl=${imgUrl.toUHexString()})"
}
}
internal class WFastLoginInfo(
val outA1: ByteReadPacket,
var adUrl: String = "",
var iconUrl: String = "",
var profileUrl: String = "",
var userJson: String = ""
) {
override fun toString(): String {
return "WFastLoginInfo(outA1=$outA1, adUrl='$adUrl', iconUrl='$iconUrl', profileUrl='$profileUrl', userJson='$userJson')"
}
}
internal class WLoginSimpleInfo(
val uin: Long, // uin
val face: Int, // ubyte actually
val age: Int, // ubyte
val gender: Int, // ubyte
val nick: String, // ubyte lv string
val imgType: ByteArray,
val imgFormat: ByteArray,
val imgUrl: ByteArray,
val mainDisplayName: ByteArray
) {
override fun toString(): String {
return "WLoginSimpleInfo(uin=$uin, face=$face, age=$age, gender=$gender, nick='$nick', imgType=${imgType.toUHexString()}, imgFormat=${imgFormat.toUHexString()}, imgUrl=${imgUrl.toUHexString()}, mainDisplayName=${mainDisplayName.toUHexString()})"
}
}
internal class LoginExtraData(
val uin: Long,
val ip: ByteArray,
val time: Int,
val version: Int
) {
override fun toString(): String {
return "LoginExtraData(uin=$uin, ip=${ip.toUHexString()}, time=$time, version=$version)"
}
}
internal class WLoginSigInfo(
val uin: Long,
val encryptA1: ByteArray?, // sigInfo[0]
/**
* WARNING, please check [QQAndroidClient.tlv16a]
*/
val noPicSig: ByteArray?, // sigInfo[1]
val G: ByteArray, // sigInfo[2]
val dpwd: ByteArray,
val randSeed: ByteArray,
val simpleInfo: WLoginSimpleInfo,
val appPri: Long,
val a2ExpiryTime: Long,
val loginBitmap: Long,
val tgt: ByteArray,
val a2CreationTime: Long,
val tgtKey: ByteArray,
val userStSig: UserStSig,
/**
* TransEmpPacket 加密使用
*/
val userStKey: ByteArray,
val userStWebSig: UserStWebSig,
val userA5: UserA5,
val userA8: UserA8,
val lsKey: LSKey,
val sKey: SKey,
val userSig64: UserSig64,
val openId: ByteArray,
val openKey: OpenKey,
val vKey: VKey,
val accessToken: AccessToken,
val d2: D2,
val d2Key: ByteArray,
val sid: Sid,
val aqSig: AqSig,
val psKeyMap: PSKeyMap,
val pt4TokenMap: Pt4TokenMap,
val superKey: ByteArray,
val payToken: ByteArray,
val pf: ByteArray,
val pfKey: ByteArray,
val da2: ByteArray,
// val pt4Token: ByteArray,
val wtSessionTicket: WtSessionTicket,
val wtSessionTicketKey: ByteArray,
val deviceToken: ByteArray
) {
override fun toString(): String {
return "WLoginSigInfo(uin=$uin, encryptA1=${encryptA1?.toUHexString()}, noPicSig=${noPicSig?.toUHexString()}, G=${G.toUHexString()}, dpwd=${dpwd.toUHexString()}, randSeed=${randSeed.toUHexString()}, simpleInfo=$simpleInfo, appPri=$appPri, a2ExpiryTime=$a2ExpiryTime, loginBitmap=$loginBitmap, tgt=${tgt.toUHexString()}, a2CreationTime=$a2CreationTime, tgtKey=${tgtKey.toUHexString()}, userStSig=$userStSig, userStKey=${userStKey.toUHexString()}, userStWebSig=$userStWebSig, userA5=$userA5, userA8=$userA8, lsKey=$lsKey, sKey=$sKey, userSig64=$userSig64, openId=${openId.toUHexString()}, openKey=$openKey, vKey=$vKey, accessToken=$accessToken, d2=$d2, d2Key=${d2Key.toUHexString()}, sid=$sid, aqSig=$aqSig, psKey=$psKeyMap, superKey=${superKey.toUHexString()}, payToken=${payToken.toUHexString()}, pf=${pf.toUHexString()}, pfKey=${pfKey.toUHexString()}, da2=${da2.toUHexString()}, wtSessionTicket=$wtSessionTicket, wtSessionTicketKey=${wtSessionTicketKey.toUHexString()}, deviceToken=${deviceToken.toUHexString()})"
}
}
internal class UserStSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class LSKey(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class UserStWebSig(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class UserA8(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class UserA5(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class SKey(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class UserSig64(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class OpenKey(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class VKey(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class AccessToken(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class D2(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
internal class Sid(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class AqSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class Pt4Token(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal typealias PSKeyMap = MutableMap<String, PSKey>
internal typealias Pt4TokenMap = MutableMap<String, Pt4Token>
internal fun Input.readUShortLVString(): String = kotlinx.io.core.String(this.readUShortLVByteArray())
internal fun Input.readUShortLVByteArray(): ByteArray = this.readBytes(this.readUShort().toInt())
internal fun parsePSKeyMapAndPt4TokenMap(
data: ByteArray,
creationTime: Long,
expireTime: Long,
outPSKeyMap: PSKeyMap,
outPt4TokenMap: Pt4TokenMap
) =
data.read {
repeat(readShort().toInt()) {
val domain = readUShortLVString()
val psKey = readUShortLVByteArray()
val pt4token = readUShortLVByteArray()
when {
psKey.isNotEmpty() -> outPSKeyMap[domain] = PSKey(psKey, creationTime, expireTime)
pt4token.isNotEmpty() -> outPt4TokenMap[domain] = Pt4Token(pt4token, creationTime, expireTime)
}
}
}
internal class PSKey(data: ByteArray, creationTime: Long, expireTime: Long) :
KeyWithExpiry(data, creationTime, expireTime)
internal class WtSessionTicket(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal open class KeyWithExpiry(
data: ByteArray,
creationTime: Long,
val expireTime: Long
) : KeyWithCreationTime(data, creationTime) {
override fun toString(): String {
return "KeyWithExpiry(data=${data.toUHexString()}, creationTime=$creationTime)"
}
}
internal open class KeyWithCreationTime(
val data: ByteArray,
val creationTime: Long
) {
override fun toString(): String {
return "KeyWithCreationTime(data=${data.toUHexString()}, creationTime=$creationTime)"
}
}
internal suspend inline fun QQAndroidClient.useNextServers(crossinline block: suspend (host: String, port: Int) -> Unit) {
if (bot.serverList.isEmpty()) {
bot.serverList.addAll(DefaultServerList)
}
retryCatchingExceptions(bot.serverList.size, except = LoginFailedException::class) l@{
val pair = bot.serverList[0]
runCatchingExceptions {
block(pair.first, pair.second)
return@l
}.getOrElse {
bot.serverList.remove(pair)
if (it !is LoginFailedException) {
// 不要重复打印.
bot.logger.warning(it)
}
throw it
}
}.getOrElse {
if (it is LoginFailedException) {
throw it
}
bot.serverList.addAll(DefaultServerList)
throw NoServerAvailableException(it)
}
}
@Suppress("RemoveRedundantQualifierName") // bug
internal fun generateTgtgtKey(guid: ByteArray): ByteArray =
(getRandomByteArray(16) + guid).md5()
internal inline fun <R> QQAndroidClient.tryDecryptOrNull(
data: ByteArray,
size: Int = data.size,
mapper: (ByteArray) -> R
): R? {
keys.forEach { (key, value) ->
kotlin.runCatching {
return mapper(TEA.decrypt(data, value, size).also { PacketLogger.verbose { "成功使用 $key 解密" } })
}
}
return null
}
internal fun QQAndroidClient.allKeys() = mapOf(
"16 zero" to ByteArray(16),
"D2 key" to wLoginSigInfo.d2Key,
"wtSessionTicketKey" to wLoginSigInfo.wtSessionTicketKey,
"userStKey" to wLoginSigInfo.userStKey,
"tgtgtKey" to tgtgtKey,
"tgtKey" to wLoginSigInfo.tgtKey,
"deviceToken" to wLoginSigInfo.deviceToken,
"shareKeyCalculatedByConstPubKey" to ecdh.keyPair.initialShareKey
//"t108" to wLoginSigInfo.t1,
//"t10c" to t10c,
//"t163" to t163
)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 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.
@ -100,19 +100,23 @@ internal class _339(
@TarsId(9) @JvmField val field1306: String = ""
) : JceStruct
/**
* v8.5.5
*/
@Serializable
internal class FileStoragePushFSSvcListFuckKotlin(
@TarsId(0) @JvmField val vUpLoadList: List<FileStorageServerListInfo>? = listOf(),
@TarsId(1) @JvmField val vPicDownLoadList: List<FileStorageServerListInfo>? = listOf(),
@TarsId(2) @JvmField val vGPicDownLoadList: List<FileStorageServerListInfo>? = null,
@TarsId(3) @JvmField val vQzoneProxyServiceList: List<FileStorageServerListInfo>? = null,
@TarsId(4) @JvmField val vUrlEncodeServiceList: List<FileStorageServerListInfo>? = null,
internal class FileStoragePushFSSvcList(
@TarsId(0) @JvmField val vUpLoadList: List<FileStorageServerListInfo> = emptyList(),
@TarsId(1) @JvmField val vPicDownLoadList: List<FileStorageServerListInfo> = emptyList(),
@TarsId(2) @JvmField val vGPicDownLoadList: List<FileStorageServerListInfo> = emptyList(),
@TarsId(3) @JvmField val vQzoneProxyServiceList: List<FileStorageServerListInfo> = emptyList(),
@TarsId(4) @JvmField val vUrlEncodeServiceList: List<FileStorageServerListInfo> = emptyList(),
@TarsId(5) @JvmField val bigDataChannel: BigDataChannel? = null,
@TarsId(6) @JvmField val vVipEmotionList: List<FileStorageServerListInfo>? = null,
@TarsId(7) @JvmField val vC2CPicDownList: List<FileStorageServerListInfo>? = null,
@TarsId(6) @JvmField val vVipEmotionList: List<FileStorageServerListInfo> = emptyList(),
@TarsId(7) @JvmField val vC2CPicDownList: List<FileStorageServerListInfo> = emptyList(),
@TarsId(8) @JvmField val fmtIPInfo: FmtIPInfo? = null,
@TarsId(9) @JvmField val domainIpChannel: DomainIpChannel? = null,
@TarsId(10) @JvmField val pttlist: ByteArray? = null
@TarsId(10) @JvmField val pttlist: ByteArray? = null,
) : JceStruct
@Serializable

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 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.
@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused", "SpellCheckingInspection")
package net.mamoe.mirai.internal.network.protocol.data.proto
import kotlinx.serialization.Serializable
@ -16,94 +18,99 @@ import kotlinx.serialization.protobuf.ProtoType
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.internal.utils.io.ProtoBuf
/**
* v8.5.5
*/
@Serializable
internal class BdhExtinfo : ProtoBuf {
@Serializable
internal class CommFileExtReq(
@ProtoNumber(1) @JvmField val actionType: Int = 0,
@ProtoNumber(2) @JvmField val uuid: ByteArray = EMPTY_BYTE_ARRAY
@JvmField @ProtoNumber(1) val actionType: Int = 0,
@JvmField @ProtoNumber(2) val uuid: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
@Serializable
internal class CommFileExtRsp(
@ProtoNumber(1) @JvmField val int32Retcode: Int = 0,
@ProtoNumber(2) @JvmField val downloadUrl: ByteArray = EMPTY_BYTE_ARRAY
@JvmField @ProtoNumber(1) val int32Retcode: Int = 0,
@JvmField @ProtoNumber(2) val downloadUrl: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
@Serializable
internal class PicInfo(
@ProtoNumber(1) @JvmField val idx: Int = 0,
@ProtoNumber(2) @JvmField val size: Int = 0,
@ProtoNumber(3) @JvmField val binMd5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(4) @JvmField val type: Int = 0
@JvmField @ProtoNumber(1) val idx: Int = 0,
@JvmField @ProtoNumber(2) val size: Int = 0,
@JvmField @ProtoNumber(3) val binMd5: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(4) val type: Int = 0
) : ProtoBuf
@Serializable
internal class QQVoiceExtReq(
@ProtoNumber(1) @JvmField val qid: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) @JvmField val fmt: Int = 0,
@ProtoNumber(3) @JvmField val rate: Int = 0,
@ProtoNumber(4) @JvmField val bits: Int = 0,
@ProtoNumber(5) @JvmField val channel: Int = 0,
@ProtoNumber(6) @JvmField val pinyin: Int = 0
@JvmField @ProtoNumber(1) val qid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(2) val fmt: Int = 0,
@JvmField @ProtoNumber(3) val rate: Int = 0,
@JvmField @ProtoNumber(4) val bits: Int = 0,
@JvmField @ProtoNumber(5) val channel: Int = 0,
@JvmField @ProtoNumber(6) val pinyin: Int = 0
) : ProtoBuf
@Serializable
internal class QQVoiceExtRsp(
@ProtoNumber(1) @JvmField val qid: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) @JvmField val int32Retcode: Int = 0,
@ProtoNumber(3) @JvmField val msgResult: List<QQVoiceResult> = emptyList()
@JvmField @ProtoNumber(1) val qid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(2) val int32Retcode: Int = 0,
@JvmField @ProtoNumber(3) val msgResult: List<QQVoiceResult> = emptyList()
) : ProtoBuf
@Serializable
internal class QQVoiceResult(
@ProtoNumber(1) @JvmField val text: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) @JvmField val pinyin: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(3) @JvmField val source: Int = 0
@JvmField @ProtoNumber(1) val text: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(2) val pinyin: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(3) val source: Int = 0
) : ProtoBuf
@Serializable
internal class ShortVideoReqExtInfo(
@ProtoNumber(1) @JvmField val cmd: Int = 0,
@ProtoNumber(2) @JvmField val sessionId: Long = 0L,
@ProtoNumber(3) @JvmField val msgThumbinfo: PicInfo? = null,
@ProtoNumber(4) @JvmField val msgVideoinfo: VideoInfo? = null,
@ProtoNumber(5) @JvmField val msgShortvideoSureReq: ShortVideoSureReqInfo? = null,
@ProtoNumber(6) @JvmField val boolIsMergeCmdBeforeData: Boolean = false
@JvmField @ProtoNumber(1) val cmd: Int = 0,
@JvmField @ProtoNumber(2) val sessionId: Long = 0L,
@JvmField @ProtoNumber(3) val msgThumbinfo: PicInfo? = null,
@JvmField @ProtoNumber(4) val msgVideoinfo: VideoInfo? = null,
@JvmField @ProtoNumber(5) val msgShortvideoSureReq: ShortVideoSureReqInfo? = null,
@JvmField @ProtoNumber(6) val boolIsMergeCmdBeforeData: Boolean = false
) : ProtoBuf
@Serializable
internal class ShortVideoRspExtInfo(
@ProtoNumber(1) @JvmField val cmd: Int = 0,
@ProtoNumber(2) @JvmField val sessionId: Long = 0L,
@ProtoNumber(3) @JvmField val int32Retcode: Int = 0,
@ProtoNumber(4) @JvmField val errinfo: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(5) @JvmField val msgThumbinfo: PicInfo? = null,
@ProtoNumber(6) @JvmField val msgVideoinfo: VideoInfo? = null,
@ProtoNumber(7) @JvmField val msgShortvideoSureRsp: ShortVideoSureRspInfo? = null,
@ProtoNumber(8) @JvmField val retryFlag: Int = 0
@JvmField @ProtoNumber(1) val cmd: Int = 0,
@JvmField @ProtoNumber(2) val sessionId: Long = 0L,
@JvmField @ProtoNumber(3) val int32Retcode: Int = 0,
@JvmField @ProtoNumber(4) val errinfo: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(5) val msgThumbinfo: PicInfo? = null,
@JvmField @ProtoNumber(6) val msgVideoinfo: VideoInfo? = null,
@JvmField @ProtoNumber(7) val msgShortvideoSureRsp: ShortVideoSureRspInfo? = null,
@JvmField @ProtoNumber(8) val retryFlag: Int = 0
) : ProtoBuf
@Serializable
internal class ShortVideoSureReqInfo(
@ProtoNumber(1) @JvmField val fromuin: Long = 0L,
@ProtoNumber(2) @JvmField val chatType: Int = 0,
@ProtoNumber(3) @JvmField val touin: Long = 0L,
@ProtoNumber(4) @JvmField val groupCode: Long = 0L,
@ProtoNumber(5) @JvmField val clientType: Int = 0,
@ProtoNumber(6) @JvmField val msgThumbinfo: PicInfo? = null,
@ProtoNumber(7) @JvmField val msgMergeVideoinfo: List<VideoInfo> = emptyList(),
@ProtoNumber(8) @JvmField val msgDropVideoinfo: List<VideoInfo> = emptyList(),
@ProtoNumber(9) @JvmField val businessType: Int = 0,
@ProtoNumber(10) @JvmField val subBusinessType: Int = 0
@JvmField @ProtoNumber(1) val fromuin: Long = 0L,
@JvmField @ProtoNumber(2) val chatType: Int = 0,
@JvmField @ProtoNumber(3) val touin: Long = 0L,
@JvmField @ProtoNumber(4) val groupCode: Long = 0L,
@JvmField @ProtoNumber(5) val clientType: Int = 0,
@JvmField @ProtoNumber(6) val msgThumbinfo: PicInfo? = null,
@JvmField @ProtoNumber(7) val msgMergeVideoinfo: List<VideoInfo> = emptyList(),
@JvmField @ProtoNumber(8) val msgDropVideoinfo: List<VideoInfo> = emptyList(),
@JvmField @ProtoNumber(9) val businessType: Int = 0,
@JvmField @ProtoNumber(10) val subBusinessType: Int = 0
) : ProtoBuf
@Serializable
internal class ShortVideoSureRspInfo(
@ProtoNumber(1) @JvmField val fileid: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) @JvmField val ukey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(3) @JvmField val msgVideoinfo: VideoInfo? = null,
@ProtoNumber(4) @JvmField val mergeCost: Int = 0
@JvmField @ProtoNumber(1) val fileid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(2) val ukey: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(3) val msgVideoinfo: VideoInfo? = null,
@JvmField @ProtoNumber(4) val mergeCost: Int = 0
) : ProtoBuf
@Serializable
@ -111,31 +118,31 @@ internal class BdhExtinfo : ProtoBuf {
@Serializable
internal class StoryVideoExtRsp(
@ProtoNumber(1) @JvmField val int32Retcode: Int = 0,
@ProtoNumber(2) @JvmField val msg: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(3) @JvmField val cdnUrl: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(4) @JvmField val fileKey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(5) @JvmField val fileId: ByteArray = EMPTY_BYTE_ARRAY
@JvmField @ProtoNumber(1) val int32Retcode: Int = 0,
@JvmField @ProtoNumber(2) val msg: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(3) val cdnUrl: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(4) val fileKey: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(5) val fileId: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
@Serializable
internal class UploadPicExtInfo(
@ProtoNumber(1) @JvmField val fileResid: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) @JvmField val downloadUrl: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(3) @JvmField val thumbDownloadUrl: ByteArray = EMPTY_BYTE_ARRAY
@JvmField @ProtoNumber(1) val fileResid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(2) val downloadUrl: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(3) val thumbDownloadUrl: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
@Serializable
internal class VideoInfo(
@ProtoNumber(1) @JvmField val idx: Int = 0,
@ProtoNumber(2) @JvmField val size: Int = 0,
@ProtoNumber(3) @JvmField val binMd5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(4) @JvmField val format: Int = 0,
@ProtoNumber(5) @JvmField val resLen: Int = 0,
@ProtoNumber(6) @JvmField val resWidth: Int = 0,
@ProtoNumber(7) @JvmField val time: Int = 0,
@ProtoNumber(8) @JvmField val starttime: Long = 0L,
@ProtoNumber(9) @JvmField val isAudio: Int = 0
@JvmField @ProtoNumber(1) val idx: Int = 0,
@JvmField @ProtoNumber(2) val size: Int = 0,
@JvmField @ProtoNumber(3) val binMd5: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(4) val format: Int = 0,
@JvmField @ProtoNumber(5) val resLen: Int = 0,
@JvmField @ProtoNumber(6) val resWidth: Int = 0,
@JvmField @ProtoNumber(7) val time: Int = 0,
@JvmField @ProtoNumber(8) val starttime: Long = 0L,
@JvmField @ProtoNumber(9) val isAudio: Int = 0
) : ProtoBuf
}
@ -143,142 +150,143 @@ internal class BdhExtinfo : ProtoBuf {
internal class CSDataHighwayHead : ProtoBuf {
@Serializable
internal class C2CCommonExtendinfo(
@ProtoNumber(1) @JvmField val infoId: Int = 0,
@ProtoNumber(2) @JvmField val msgFilterExtendinfo: FilterExtendinfo? = null
@JvmField @ProtoNumber(1) val infoId: Int = 0,
@JvmField @ProtoNumber(2) val msgFilterExtendinfo: FilterExtendinfo? = null
) : ProtoBuf
@Serializable
internal class DataHighwayHead(
@ProtoNumber(1) @JvmField val version: Int = 0,
@ProtoNumber(2) @JvmField val uin: String = "", // yes
@ProtoNumber(3) @JvmField val command: String = "",
@ProtoNumber(4) @JvmField val seq: Int = 0,
@ProtoNumber(5) @JvmField val retryTimes: Int = 0,
@ProtoNumber(6) @JvmField val appid: Int = 0,
@ProtoNumber(7) @JvmField val dataflag: Int = 0,
@ProtoNumber(8) @JvmField val commandId: Int = 0,
@ProtoNumber(9) @JvmField val buildVer: String = "",
@ProtoNumber(10) @JvmField val localeId: Int = 0
@JvmField @ProtoNumber(1) val version: Int = 0,
@JvmField @ProtoNumber(2) val uin: String = "",
@JvmField @ProtoNumber(3) val command: String = "",
@JvmField @ProtoNumber(4) val seq: Int = 0,
@JvmField @ProtoNumber(5) val retryTimes: Int = 0,
@JvmField @ProtoNumber(6) val appid: Int = 0,
@JvmField @ProtoNumber(7) val dataflag: Int = 0,
@JvmField @ProtoNumber(8) val commandId: Int = 0,
@JvmField @ProtoNumber(9) val buildVer: String = "",
@JvmField @ProtoNumber(10) val localeId: Int = 0,
@JvmField @ProtoNumber(11) val envId: Int = 0
) : ProtoBuf
@Serializable
internal class DataHole(
@ProtoNumber(1) @JvmField val begin: Long = 0L,
@ProtoNumber(2) @JvmField val end: Long = 0L
@JvmField @ProtoNumber(1) val begin: Long = 0L,
@JvmField @ProtoNumber(2) val end: Long = 0L
) : ProtoBuf
@Serializable
internal class FilterExtendinfo(
@ProtoNumber(1) @JvmField val filterFlag: Int = 0,
@ProtoNumber(2) @JvmField val msgImageFilterRequest: ImageFilterRequest? = null
@JvmField @ProtoNumber(1) val filterFlag: Int = 0,
@JvmField @ProtoNumber(2) val msgImageFilterRequest: ImageFilterRequest? = null
) : ProtoBuf
@Serializable
internal class FilterStyle(
@ProtoNumber(1) @JvmField val styleId: Int = 0,
@ProtoNumber(2) @JvmField val styleName: ByteArray = EMPTY_BYTE_ARRAY
@JvmField @ProtoNumber(1) val styleId: Int = 0,
@JvmField @ProtoNumber(2) val styleName: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
@Serializable
internal class ImageFilterRequest(
@ProtoNumber(1) @JvmField val sessionId: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) @JvmField val clientIp: Int = 0,
@ProtoNumber(3) @JvmField val uin: Long = 0L,
@ProtoNumber(4) @JvmField val style: FilterStyle? = null,
@ProtoNumber(5) @JvmField val width: Int = 0,
@ProtoNumber(6) @JvmField val height: Int = 0,
@ProtoNumber(7) @JvmField val imageData: ByteArray = EMPTY_BYTE_ARRAY
@JvmField @ProtoNumber(1) val sessionId: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(2) val clientIp: Int = 0,
@JvmField @ProtoNumber(3) val uin: Long = 0L,
@JvmField @ProtoNumber(4) val style: FilterStyle? = null,
@JvmField @ProtoNumber(5) val width: Int = 0,
@JvmField @ProtoNumber(6) val height: Int = 0,
@JvmField @ProtoNumber(7) val imageData: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
@Serializable
internal class ImageFilterResponse(
@ProtoNumber(1) @JvmField val retCode: Int = 0,
@ProtoNumber(2) @JvmField val imageData: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(3) @JvmField val costTime: Int = 0
@JvmField @ProtoNumber(1) val retCode: Int = 0,
@JvmField @ProtoNumber(2) val imageData: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(3) val costTime: Int = 0
) : ProtoBuf
@Serializable
internal class LoginSigHead(
@ProtoNumber(1) @JvmField val loginsigType: Int = 0,
@ProtoNumber(2) @JvmField val loginsig: ByteArray = EMPTY_BYTE_ARRAY
@JvmField @ProtoNumber(1) val loginsigType: Int = 0,
@JvmField @ProtoNumber(2) val loginsig: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
@Serializable
internal class NewServiceTicket(
@ProtoNumber(1) @JvmField val signature: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) @JvmField val ukey: ByteArray = EMPTY_BYTE_ARRAY
@JvmField @ProtoNumber(1) val signature: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(2) val ukey: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
@Serializable
internal class PicInfoExt(
@ProtoNumber(1) @JvmField val picWidth: Int = 0,
@ProtoNumber(2) @JvmField val picHeight: Int = 0,
@ProtoNumber(3) @JvmField val picFlag: Int = 0,
@ProtoNumber(4) @JvmField val busiType: Int = 0,
@ProtoNumber(5) @JvmField val srcTerm: Int = 0,
@ProtoNumber(6) @JvmField val platType: Int = 0,
@ProtoNumber(7) @JvmField val netType: Int = 0,
@ProtoNumber(8) @JvmField val imgType: Int = 0,
@ProtoNumber(9) @JvmField val appPicType: Int = 0
@JvmField @ProtoNumber(1) val picWidth: Int = 0,
@JvmField @ProtoNumber(2) val picHeight: Int = 0,
@JvmField @ProtoNumber(3) val picFlag: Int = 0,
@JvmField @ProtoNumber(4) val busiType: Int = 0,
@JvmField @ProtoNumber(5) val srcTerm: Int = 0,
@JvmField @ProtoNumber(6) val platType: Int = 0,
@JvmField @ProtoNumber(7) val netType: Int = 0,
@JvmField @ProtoNumber(8) val imgType: Int = 0,
@JvmField @ProtoNumber(9) val appPicType: Int = 0
) : ProtoBuf
@Serializable
internal class PicRspExtInfo(
@ProtoNumber(1) @JvmField val skey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) @JvmField val clientIp: Int = 0,
@ProtoNumber(3) @JvmField val upOffset: Long = 0L,
@ProtoNumber(4) @JvmField val blockSize: Long = 0L
@JvmField @ProtoNumber(1) val skey: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(2) val clientIp: Int = 0,
@JvmField @ProtoNumber(3) val upOffset: Long = 0L,
@JvmField @ProtoNumber(4) val blockSize: Long = 0L
) : ProtoBuf
@Serializable
internal class QueryHoleRsp(
@ProtoNumber(1) @JvmField val result: Int = 0,
@ProtoNumber(2) @JvmField val dataHole: List<DataHole> = emptyList(),
@ProtoNumber(3) @JvmField val boolCompFlag: Boolean = false
@JvmField @ProtoNumber(1) val result: Int = 0,
@JvmField @ProtoNumber(2) val dataHole: List<DataHole> = emptyList(),
@JvmField @ProtoNumber(3) val boolCompFlag: Boolean = false
) : ProtoBuf
@Serializable
internal class ReqDataHighwayHead(
@ProtoNumber(1) @JvmField val msgBasehead: DataHighwayHead? = null,
@ProtoNumber(2) @JvmField val msgSeghead: SegHead? = null,
@ProtoNumber(3) @JvmField val reqExtendinfo: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(4) @JvmField val timestamp: Long = 0L,
@ProtoNumber(5) @JvmField val msgLoginSigHead: LoginSigHead? = null
@JvmField @ProtoNumber(1) val msgBasehead: DataHighwayHead? = null,
@JvmField @ProtoNumber(2) val msgSeghead: SegHead? = null,
@JvmField @ProtoNumber(3) val reqExtendinfo: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(4) val timestamp: Long = 0L,
@JvmField @ProtoNumber(5) val msgLoginSigHead: LoginSigHead? = null
) : ProtoBuf
@Serializable
internal class RspBody(
@ProtoNumber(1) @JvmField val msgQueryHoleRsp: QueryHoleRsp? = null
@JvmField @ProtoNumber(1) val msgQueryHoleRsp: QueryHoleRsp? = null
) : ProtoBuf
@Serializable
internal class RspDataHighwayHead(
@ProtoNumber(1) @JvmField val msgBasehead: DataHighwayHead? = null,
@ProtoNumber(2) @JvmField val msgSeghead: SegHead? = null,
@ProtoNumber(3) @JvmField val errorCode: Int = 0,
@ProtoNumber(4) @JvmField val allowRetry: Int = 0,
@ProtoNumber(5) @JvmField val cachecost: Int = 0,
@ProtoNumber(6) @JvmField val htcost: Int = 0,
@ProtoNumber(7) @JvmField val rspExtendinfo: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(8) @JvmField val timestamp: Long = 0L,
@ProtoNumber(9) @JvmField val range: Long = 0L,
@ProtoNumber(10) @JvmField val isReset: Int = 0
@JvmField @ProtoNumber(1) val msgBasehead: DataHighwayHead? = null,
@JvmField @ProtoNumber(2) val msgSeghead: SegHead? = null,
@JvmField @ProtoNumber(3) val errorCode: Int = 0,
@JvmField @ProtoNumber(4) val allowRetry: Int = 0,
@JvmField @ProtoNumber(5) val cachecost: Int = 0,
@JvmField @ProtoNumber(6) val htcost: Int = 0,
@JvmField @ProtoNumber(7) val rspExtendinfo: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(8) val timestamp: Long = 0L,
@JvmField @ProtoNumber(9) val range: Long = 0L,
@JvmField @ProtoNumber(10) val isReset: Int = 0
) : ProtoBuf
@Serializable
internal class SegHead(
@ProtoNumber(1) @JvmField val serviceid: Int = 0,
@ProtoNumber(2) @JvmField val filesize: Long = 0L,
@ProtoNumber(3) @JvmField val dataoffset: Long = 0L,
@ProtoNumber(4) @JvmField val datalength: Int = 0,
@ProtoNumber(5) @JvmField val rtcode: Int = 0,
@ProtoNumber(6) @JvmField val serviceticket: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(7) @JvmField val flag: Int = 0,
@ProtoNumber(8) @JvmField val md5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(9) @JvmField val fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(10) @JvmField val cacheAddr: Int = 0,
@ProtoNumber(11) @JvmField val queryTimes: Int = 0,
@ProtoNumber(12) @JvmField val updateCacheip: Int = 0
@JvmField @ProtoNumber(1) val serviceid: Int = 0,
@JvmField @ProtoNumber(2) val filesize: Long = 0L,
@JvmField @ProtoNumber(3) val dataoffset: Long = 0L,
@JvmField @ProtoNumber(4) val datalength: Int = 0,
@JvmField @ProtoNumber(5) val rtcode: Int = 0,
@JvmField @ProtoNumber(6) val serviceticket: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(7) val flag: Int = 0,
@JvmField @ProtoNumber(8) val md5: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(9) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(10) val cacheAddr: Int = 0,
@JvmField @ProtoNumber(11) val queryTimes: Int = 0,
@JvmField @ProtoNumber(12) val updateCacheip: Int = 0
) : ProtoBuf
}
@ -286,31 +294,31 @@ internal class CSDataHighwayHead : ProtoBuf {
internal class HwConfigPersistentPB : ProtoBuf {
@Serializable
internal class HwConfigItemPB(
@ProtoNumber(1) @JvmField val ingKey: String = "",
@ProtoNumber(2) @JvmField val endPointList: List<HwEndPointPB> = emptyList()
@JvmField @ProtoNumber(1) val key: String = "",
@JvmField @ProtoNumber(2) val endPointList: List<HwEndPointPB> = emptyList()
) : ProtoBuf
@Serializable
internal class HwConfigPB(
@ProtoNumber(1) @JvmField val configItemList: List<HwConfigItemPB> = emptyList(),
@ProtoNumber(2) @JvmField val netSegConfList: List<HwNetSegConfPB> = emptyList(),
@ProtoNumber(3) @JvmField val shortVideoNetConf: List<HwNetSegConfPB> = emptyList(),
@ProtoNumber(4) @JvmField val configItemListIp6: List<HwConfigItemPB> = emptyList()
@JvmField @ProtoNumber(1) val configItemList: List<HwConfigItemPB> = emptyList(),
@JvmField @ProtoNumber(2) val netSegConfList: List<HwNetSegConfPB> = emptyList(),
@JvmField @ProtoNumber(3) val shortVideoNetConf: List<HwNetSegConfPB> = emptyList(),
@JvmField @ProtoNumber(4) val configItemListIp6: List<HwConfigItemPB> = emptyList()
) : ProtoBuf
@Serializable
internal class HwEndPointPB(
@ProtoNumber(1) @JvmField val ingHost: String = "",
@ProtoNumber(2) @JvmField val int32Port: Int = 0,
@ProtoNumber(3) @JvmField val int64Timestampe: Long = 0L
@JvmField @ProtoNumber(1) val host: String = "",
@JvmField @ProtoNumber(2) val int32Port: Int = 0,
@JvmField @ProtoNumber(3) val int64Timestampe: Long = 0L
) : ProtoBuf
@Serializable
internal class HwNetSegConfPB(
@ProtoNumber(1) @JvmField val int64NetType: Long = 0L,
@ProtoNumber(2) @JvmField val int64SegSize: Long = 0L,
@ProtoNumber(3) @JvmField val int64SegNum: Long = 0L,
@ProtoNumber(4) @JvmField val int64CurConnNum: Long = 0L
@JvmField @ProtoNumber(1) val int64NetType: Long = 0L,
@JvmField @ProtoNumber(2) val int64SegSize: Long = 0L,
@JvmField @ProtoNumber(3) val int64SegNum: Long = 0L,
@JvmField @ProtoNumber(4) val int64CurConnNum: Long = 0L
) : ProtoBuf
}
@ -318,8 +326,8 @@ internal class HwConfigPersistentPB : ProtoBuf {
internal class HwSessionInfoPersistentPB : ProtoBuf {
@Serializable
internal class HwSessionInfoPB(
@ProtoNumber(1) @JvmField val httpconnSigSession: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) @JvmField val sessionKey: ByteArray = EMPTY_BYTE_ARRAY
@JvmField @ProtoNumber(1) val httpconnSigSession: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(2) val sessionKey: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
}
@ -327,137 +335,145 @@ internal class HwSessionInfoPersistentPB : ProtoBuf {
internal class Subcmd0x501 : ProtoBuf {
@Serializable
internal class ReqBody(
@ProtoNumber(1281) @JvmField val msgSubcmd0x501ReqBody: SubCmd0x501ReqBody? = null
@JvmField @ProtoNumber(1281) val msgSubcmd0x501ReqBody: SubCmd0x501ReqBody? = null
) : ProtoBuf
@Serializable
internal class RspBody(
@ProtoNumber(1281) @JvmField val msgSubcmd0x501RspBody: SubCmd0x501Rspbody? = null
@JvmField @ProtoNumber(1281) val msgSubcmd0x501RspBody: SubCmd0x501Rspbody? = null
) : ProtoBuf
@Serializable
internal class SubCmd0x501ReqBody(
@ProtoNumber(1) @JvmField val uin: Long = 0L,
@ProtoNumber(2) @JvmField val idcId: Int = 0,
@ProtoNumber(3) @JvmField val appid: Int = 0,
@ProtoNumber(4) @JvmField val loginSigType: Int = 0,
@ProtoNumber(5) @JvmField val loginSigTicket: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(6) @JvmField val requestFlag: Int = 0,
@ProtoNumber(7) @JvmField val uint32ServiceTypes: List<Int> = emptyList(),
@ProtoNumber(8) @JvmField val bid: Int = 0,
@ProtoNumber(9) @JvmField val term: Int = 0,
@ProtoNumber(10) @JvmField val plat: Int = 0,
@ProtoNumber(11) @JvmField val net: Int = 0,
@ProtoNumber(12) @JvmField val caller: Int = 0
@JvmField @ProtoNumber(1) val uin: Long = 0L,
@JvmField @ProtoNumber(2) val idcId: Int = 0,
@JvmField @ProtoNumber(3) val appid: Int = 0,
@JvmField @ProtoNumber(4) val loginSigType: Int = 0,
@JvmField @ProtoNumber(5) val loginSigTicket: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(6) val requestFlag: Int = 0,
@JvmField @ProtoNumber(7) val uint32ServiceTypes: List<Int> = emptyList(),
@JvmField @ProtoNumber(8) val bid: Int = 0,
@JvmField @ProtoNumber(9) val term: Int = 0,
@JvmField @ProtoNumber(10) val plat: Int = 0,
@JvmField @ProtoNumber(11) val net: Int = 0,
@JvmField @ProtoNumber(12) val caller: Int = 0
) : ProtoBuf
@Serializable
internal class SubCmd0x501Rspbody(
@ProtoNumber(1) @JvmField val httpconnSigSession: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) @JvmField val sessionKey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(3) @JvmField val msgHttpconnAddrs: List<SrvAddrs> = emptyList(),
@ProtoNumber(4) @JvmField val preConnection: Int = 0,
@ProtoNumber(5) @JvmField val csConn: Int = 0,
@ProtoNumber(6) @JvmField val msgIpLearnConf: IpLearnConf? = null,
@ProtoNumber(7) @JvmField val msgDynTimeoutConf: DynTimeOutConf? = null,
@ProtoNumber(8) @JvmField val msgOpenUpConf: OpenUpConf? = null,
@ProtoNumber(9) @JvmField val msgDownloadEncryptConf: DownloadEncryptConf? = null,
@ProtoNumber(10) @JvmField val msgShortVideoConf: ShortVideoConf? = null,
@ProtoNumber(11) @JvmField val msgPtvConf: PTVConf? = null
@JvmField @ProtoNumber(1) val httpconnSigSession: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(2) val sessionKey: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(3) val msgHttpconnAddrs: List<SrvAddrs> = emptyList(),
@JvmField @ProtoNumber(4) val preConnection: Int = 0,
@JvmField @ProtoNumber(5) val csConn: Int = 0,
@JvmField @ProtoNumber(6) val msgIpLearnConf: IpLearnConf? = null,
@JvmField @ProtoNumber(7) val msgDynTimeoutConf: DynTimeOutConf? = null,
@JvmField @ProtoNumber(8) val msgOpenUpConf: OpenUpConf? = null,
@JvmField @ProtoNumber(9) val msgDownloadEncryptConf: DownloadEncryptConf? = null,
@JvmField @ProtoNumber(10) val msgShortVideoConf: ShortVideoConf? = null,
@JvmField @ProtoNumber(11) val msgPtvConf: PTVConf? = null,
@JvmField @ProtoNumber(12) val shareType: Int = 0,
@JvmField @ProtoNumber(13) val shareChannel: Int = 0,
@JvmField @ProtoNumber(14) val fmtPolicy: Int = 0,
@JvmField @ProtoNumber(15) val bigdataPolicy: Int = 0,
@JvmField @ProtoNumber(16) val connAttemptDelay: Int = 0
) : ProtoBuf {
@Serializable
internal class DownloadEncryptConf(
@ProtoNumber(1) @JvmField val boolEnableEncryptRequest: Boolean = false,
@ProtoNumber(2) @JvmField val boolEnableEncryptedPic: Boolean = false,
@ProtoNumber(3) @JvmField val ctrlFlag: Int = 0
@JvmField @ProtoNumber(1) val boolEnableEncryptRequest: Boolean = false,
@JvmField @ProtoNumber(2) val boolEnableEncryptedPic: Boolean = false,
@JvmField @ProtoNumber(3) val ctrlFlag: Int = 0
) : ProtoBuf
@Serializable
internal class DynTimeOutConf(
@ProtoNumber(1) @JvmField val tbase2g: Int = 0,
@ProtoNumber(2) @JvmField val tbase3g: Int = 0,
@ProtoNumber(3) @JvmField val tbase4g: Int = 0,
@ProtoNumber(4) @JvmField val tbaseWifi: Int = 0,
@ProtoNumber(5) @JvmField val torg2g: Int = 0,
@ProtoNumber(6) @JvmField val torg3g: Int = 0,
@ProtoNumber(7) @JvmField val torg4g: Int = 0,
@ProtoNumber(8) @JvmField val torgWifi: Int = 0,
@ProtoNumber(9) @JvmField val maxTimeout: Int = 0,
@ProtoNumber(10) @JvmField val enableDynTimeout: Int = 0,
@ProtoNumber(11) @JvmField val maxTimeout2g: Int = 0,
@ProtoNumber(12) @JvmField val maxTimeout3g: Int = 0,
@ProtoNumber(13) @JvmField val maxTimeout4g: Int = 0,
@ProtoNumber(14) @JvmField val maxTimeoutWifi: Int = 0,
@ProtoNumber(15) @JvmField val hbTimeout2g: Int = 0,
@ProtoNumber(16) @JvmField val hbTimeout3g: Int = 0,
@ProtoNumber(17) @JvmField val hbTimeout4g: Int = 0,
@ProtoNumber(18) @JvmField val hbTimeoutWifi: Int = 0,
@ProtoNumber(19) @JvmField val hbTimeoutDefault: Int = 0
@JvmField @ProtoNumber(1) val tbase2g: Int = 0,
@JvmField @ProtoNumber(2) val tbase3g: Int = 0,
@JvmField @ProtoNumber(3) val tbase4g: Int = 0,
@JvmField @ProtoNumber(4) val tbaseWifi: Int = 0,
@JvmField @ProtoNumber(5) val torg2g: Int = 0,
@JvmField @ProtoNumber(6) val torg3g: Int = 0,
@JvmField @ProtoNumber(7) val torg4g: Int = 0,
@JvmField @ProtoNumber(8) val torgWifi: Int = 0,
@JvmField @ProtoNumber(9) val maxTimeout: Int = 0,
@JvmField @ProtoNumber(10) val enableDynTimeout: Int = 0,
@JvmField @ProtoNumber(11) val maxTimeout2g: Int = 0,
@JvmField @ProtoNumber(12) val maxTimeout3g: Int = 0,
@JvmField @ProtoNumber(13) val maxTimeout4g: Int = 0,
@JvmField @ProtoNumber(14) val maxTimeoutWifi: Int = 0,
@JvmField @ProtoNumber(15) val hbTimeout2g: Int = 0,
@JvmField @ProtoNumber(16) val hbTimeout3g: Int = 0,
@JvmField @ProtoNumber(17) val hbTimeout4g: Int = 0,
@JvmField @ProtoNumber(18) val hbTimeoutWifi: Int = 0,
@JvmField @ProtoNumber(19) val hbTimeoutDefault: Int = 0
) : ProtoBuf
@Serializable
internal class Ip6Addr(
@ProtoNumber(1) @JvmField val type: Int = 0,
@ProtoNumber(2) @JvmField val ip6: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(3) @JvmField val port: Int = 0,
@ProtoNumber(4) @JvmField val area: Int = 0,
@ProtoNumber(5) @JvmField val sameIsp: Int = 0
@JvmField @ProtoNumber(1) val type: Int = 0,
@JvmField @ProtoNumber(2) val ip6: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(3) val port: Int = 0,
@JvmField @ProtoNumber(4) val area: Int = 0,
@JvmField @ProtoNumber(5) val sameIsp: Int = 0
) : ProtoBuf
@Serializable
internal class IpAddr(
@ProtoNumber(1) @JvmField val type: Int = 0,
@ProtoType(ProtoIntegerType.FIXED) @ProtoNumber(2) @JvmField val ip: Int = 0,
@ProtoNumber(3) @JvmField val port: Int = 0,
@ProtoNumber(4) @JvmField val area: Int = 0,
@ProtoNumber(5) @JvmField val sameIsp: Int = 0
) : ProtoBuf
@JvmField @ProtoNumber(1) val type: Int = 0,
@ProtoType(ProtoIntegerType.FIXED) @JvmField @ProtoNumber(2) val ip: Int = 0,
@JvmField @ProtoNumber(3) val port: Int = 0,
@JvmField @ProtoNumber(4) val area: Int = 0,
@JvmField @ProtoNumber(5) val sameIsp: Int = 0
) : ProtoBuf {
fun decode(): Pair<Int, Int> = ip to port
}
@Serializable
internal class IpLearnConf(
@ProtoNumber(1) @JvmField val refreshCachedIp: Int = 0,
@ProtoNumber(2) @JvmField val enableIpLearn: Int = 0
@JvmField @ProtoNumber(1) val refreshCachedIp: Int = 0,
@JvmField @ProtoNumber(2) val enableIpLearn: Int = 0
) : ProtoBuf
@Serializable
internal class NetSegConf(
@ProtoNumber(1) @JvmField val netType: Int = 0,
@ProtoNumber(2) @JvmField val segsize: Int = 0,
@ProtoNumber(3) @JvmField val segnum: Int = 0,
@ProtoNumber(4) @JvmField val curconnnum: Int = 0
@JvmField @ProtoNumber(1) val netType: Int = 0,
@JvmField @ProtoNumber(2) val segsize: Int = 0,
@JvmField @ProtoNumber(3) val segnum: Int = 0,
@JvmField @ProtoNumber(4) val curconnnum: Int = 0
) : ProtoBuf
@Serializable
internal class OpenUpConf(
@ProtoNumber(1) @JvmField val boolEnableOpenup: Boolean = false,
@ProtoNumber(2) @JvmField val preSendSegnum: Int = 0,
@ProtoNumber(3) @JvmField val preSendSegnum3g: Int = 0,
@ProtoNumber(4) @JvmField val preSendSegnum4g: Int = 0,
@ProtoNumber(5) @JvmField val preSendSegnumWifi: Int = 0
@JvmField @ProtoNumber(1) val boolEnableOpenup: Boolean = false,
@JvmField @ProtoNumber(2) val preSendSegnum: Int = 0,
@JvmField @ProtoNumber(3) val preSendSegnum3g: Int = 0,
@JvmField @ProtoNumber(4) val preSendSegnum4g: Int = 0,
@JvmField @ProtoNumber(5) val preSendSegnumWifi: Int = 0
) : ProtoBuf
@Serializable
internal class PTVConf(
@ProtoNumber(1) @JvmField val channelType: Int = 0,
@ProtoNumber(2) @JvmField val msgNetsegconf: List<NetSegConf> = emptyList(),
@ProtoNumber(3) @JvmField val boolOpenHardwareCodec: Boolean = false
@JvmField @ProtoNumber(1) val channelType: Int = 0,
@JvmField @ProtoNumber(2) val msgNetsegconf: List<NetSegConf> = emptyList(),
@JvmField @ProtoNumber(3) val boolOpenHardwareCodec: Boolean = false
) : ProtoBuf
@Serializable
internal class ShortVideoConf(
@ProtoNumber(1) @JvmField val channelType: Int = 0,
@ProtoNumber(2) @JvmField val msgNetsegconf: List<NetSegConf> = emptyList(),
@ProtoNumber(3) @JvmField val boolOpenHardwareCodec: Boolean = false,
@ProtoNumber(4) @JvmField val boolSendAheadSignal: Boolean = false
@JvmField @ProtoNumber(1) val channelType: Int = 0,
@JvmField @ProtoNumber(2) val msgNetsegconf: List<NetSegConf> = emptyList(),
@JvmField @ProtoNumber(3) val boolOpenHardwareCodec: Boolean = false,
@JvmField @ProtoNumber(4) val boolSendAheadSignal: Boolean = false
) : ProtoBuf
@Serializable
internal class SrvAddrs(
@ProtoNumber(1) @JvmField val serviceType: Int = 0,
@ProtoNumber(2) @JvmField val msgAddrs: List<IpAddr> = emptyList(),
@ProtoNumber(3) @JvmField val fragmentSize: Int = 0,
@ProtoNumber(4) @JvmField val msgNetsegconf: List<NetSegConf> = emptyList(),
@ProtoNumber(5) @JvmField val msgAddrsV6: List<Ip6Addr> = emptyList()
internal data class SrvAddrs(
@JvmField @ProtoNumber(1) val serviceType: Int = 0,
@JvmField @ProtoNumber(2) val msgAddrs: List<IpAddr> = emptyList(),
@JvmField @ProtoNumber(3) val fragmentSize: Int = 0,
@JvmField @ProtoNumber(4) val msgNetsegconf: List<NetSegConf> = emptyList(),
@JvmField @ProtoNumber(5) val msgAddrsV6: List<Ip6Addr> = emptyList()
) : ProtoBuf
}
}

View File

@ -26,6 +26,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.internal.network.protocol.packet.summarycard.SummaryCard
import net.mamoe.mirai.internal.network.readUShortLVByteArray
import net.mamoe.mirai.internal.network.tryDecryptOrNull
import net.mamoe.mirai.internal.utils.crypto.TEA
import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey
import net.mamoe.mirai.utils.*

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 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.
@ -17,6 +17,7 @@ import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x388
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
@ -63,14 +64,12 @@ internal class PttStore {
}
}
@OptIn(ExperimentalStdlibApi::class)
operator fun invoke(
client: QQAndroidClient,
fun createTryUpPttPack(
uin: Long,
groupCode: Long,
resource: ExternalResource
): OutgoingPacket {
val pack = Cmd0x388.ReqBody(
): Cmd0x388.ReqBody {
return Cmd0x388.ReqBody(
netType = 3, // wifi
subcmd = 3,
msgTryupPttReq = listOf(
@ -87,12 +86,22 @@ internal class PttStore {
innerIp = 0,
buildVer = "6.5.5.663".encodeToByteArray(),
voiceLength = 1,
codec = 0, // don't use resource.codec
codec = resource.voiceCodec, // HTTP 时只支持 0
// 2021/1/26 因为 #577 修改为 resource.voiceCodec
voiceType = 1,
boolNewUpChan = true
)
)
)
}
operator fun invoke(
client: QQAndroidClient,
uin: Long,
groupCode: Long,
resource: ExternalResource
): OutgoingPacketWithRespType<Response> {
val pack = createTryUpPttPack(uin, groupCode, resource)
return buildOutgoingUniPacket(client) {
writeProtoBuf(Cmd0x388.ReqBody.serializer(), pack)
}

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,7 @@ import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.Closeable
import kotlinx.io.streams.readPacketAtMost
import kotlinx.io.streams.writePacket
import net.mamoe.mirai.utils.withUse
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.IOException
@ -23,6 +24,8 @@ import java.net.NoRouteToHostException
import java.net.Socket
import java.net.UnknownHostException
import java.util.concurrent.Executors
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* TCP Socket.
@ -97,6 +100,26 @@ internal class PlatformSocket : Closeable {
writeChannel = socket.getOutputStream().buffered()
}
}
companion object {
suspend fun connect(
serverIp: String,
serverPort: Int,
): PlatformSocket {
val socket = PlatformSocket()
socket.connect(serverIp, serverPort)
return socket
}
suspend inline fun <R> withConnection(
serverIp: String,
serverPort: Int,
block: PlatformSocket.() -> R
): R {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return connect(serverIp, serverPort).withUse(block)
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 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.coroutines.withTimeoutOrNull
import kotlin.math.roundToInt
internal suspend inline fun <R> Collection<Pair<Int, Int>>.retryWithServers(
timeoutMillis: Long,
onFail: (exception: Throwable?) -> Nothing,
crossinline block: suspend (ip: String, port: Int) -> R
): R {
require(this.isNotEmpty()) { "receiver of retryWithServers must not be empty" }
var exception: Throwable? = null
for (pair in this) {
return kotlin.runCatching {
withTimeoutOrNull(timeoutMillis) {
block(pair.first.toIpV4AddressString(), pair.second)
}
}.recover {
if (exception != null) {
it.addSuppressed(exception!!)
}
exception = it // so as to show last exception followed by suppressed others
null
}.getOrNull() ?: continue
}
onFail(exception)
}
internal fun Int.sizeToString() = this.toLong().sizeToString()
internal fun Long.sizeToString(): String {
return if (this < 1024) {
"$this B"
} else ((this * 100.0 / 1024).roundToInt() / 100.0).toString() + " KiB"
}