Basic complete: packet parse, zlib decompress
This commit is contained in:
24 files changed
+410
-63
No files matched your search
@@ -44,3 +44,4 @@ bin/
|
|||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/.idea/
|
/.idea/
|
||||||
|
/bl-client/src/commonTest/resources/cookie.txt
|
||||||
@@ -19,12 +19,24 @@ kotlin {
|
|||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
api(project(":bl-core"))
|
api(project(":bl-core"))
|
||||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
|
||||||
implementation("io.ktor:ktor-client-websockets:$ktorVersion")
|
implementation("io.ktor:ktor-client-websockets:$ktorVersion")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appleMain.dependencies {
|
||||||
|
implementation("io.ktor:ktor-client-darwin:${ktorVersion}")
|
||||||
|
}
|
||||||
|
|
||||||
|
linuxMain.dependencies {
|
||||||
|
implementation("io.ktor:ktor-client-curl:${ktorVersion}")
|
||||||
|
}
|
||||||
|
|
||||||
|
mingwMain.dependencies {
|
||||||
|
implementation("io.ktor:ktor-client-winhttp:${ktorVersion}")
|
||||||
|
}
|
||||||
|
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(project(":bl-core"))
|
implementation(kotlin("test"))
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/8/30
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@file:OptIn(InternalBldmApi::class)
|
||||||
|
|
||||||
|
package cn.rtast.bldm.client
|
||||||
|
|
||||||
|
import cn.rtast.bldm.core.BLDMConstants
|
||||||
|
import cn.rtast.bldm.core.annotations.InternalBldmApi
|
||||||
|
import cn.rtast.bldm.core.data.DMServerConf
|
||||||
|
import cn.rtast.bldm.core.data.RealRoomId
|
||||||
|
import cn.rtast.bldm.core.data.UserNavData
|
||||||
|
import cn.rtast.bldm.core.protocol.Packet
|
||||||
|
import cn.rtast.bldm.core.protocol.PacketDecoder
|
||||||
|
import cn.rtast.bldm.core.protocol.PacketEncoder
|
||||||
|
import cn.rtast.bldm.core.protocol.event.PacketEvents
|
||||||
|
import cn.rtast.bldm.core.util._fromJson
|
||||||
|
import cn.rtast.bldm.core.util.parseBuvidAndUid
|
||||||
|
import io.ktor.client.plugins.websocket.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.websocket.*
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
private val HEARTBEAT_INTERVAL = 30.seconds
|
||||||
|
|
||||||
|
public suspend fun connectToBlDM(
|
||||||
|
roomId: Long,
|
||||||
|
cookie: String?,
|
||||||
|
events: PacketEvents.() -> Unit,
|
||||||
|
) {
|
||||||
|
val parsedCookie = parseBuvidAndUid(cookie ?: "")
|
||||||
|
val packetEncoder = PacketEncoder()
|
||||||
|
val packetDecoder = PacketDecoder(events)
|
||||||
|
val realRoomId = httpClient.get(BLDMConstants.REAL_ROOM_ID_URL + roomId)
|
||||||
|
.bodyAsText()._fromJson<RealRoomId>()
|
||||||
|
val wbi = httpClient.get(BLDMConstants.USER_NAV_URL)
|
||||||
|
.bodyAsText()._fromJson<UserNavData>().signWbi(realRoomId.data.roomId)
|
||||||
|
val serverConf = httpClient.get(BLDMConstants.DM_SERVER_CONF_URL + "?$wbi") {
|
||||||
|
cookie?.let { header(HttpHeaders.Cookie, it) }
|
||||||
|
}.bodyAsText()._fromJson<DMServerConf>().data
|
||||||
|
val wsUrl = "ws://${serverConf.hostList.first().host}:${serverConf.hostList.first().wsPort}/sub"
|
||||||
|
|
||||||
|
httpClient.webSocket(wsUrl, {
|
||||||
|
// header(HttpHeaders.Host, "${serverConf.hostList.first().host}:${serverConf.hostList.first().wssPort}")
|
||||||
|
// header(HttpHeaders.Origin, "https://www.bilibili.com")
|
||||||
|
}) {
|
||||||
|
val authPacket = packetEncoder.authPacket(
|
||||||
|
parsedCookie.second ?: 0,
|
||||||
|
realRoomId.data.roomId,
|
||||||
|
parsedCookie.first ?: "",
|
||||||
|
serverConf.token
|
||||||
|
)
|
||||||
|
sendPacket(authPacket)
|
||||||
|
val heartbeatJob = launch {
|
||||||
|
val heartbeatPacket = packetEncoder.heartbeatPacket()
|
||||||
|
while (isActive) {
|
||||||
|
delay(HEARTBEAT_INTERVAL)
|
||||||
|
runCatching {
|
||||||
|
sendPacket(heartbeatPacket)
|
||||||
|
}.onFailure {
|
||||||
|
println("heartbeat failed ${it.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (frame in incoming) {
|
||||||
|
if (frame !is Frame.Binary) continue
|
||||||
|
val data = frame.readBytes()
|
||||||
|
packetDecoder.decode(data)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
heartbeatJob.cancel()
|
||||||
|
println("Closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpClient.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal suspend fun DefaultWebSocketSession.sendPacket(packet: Packet) =
|
||||||
|
send(Frame.Binary(true, packet.toByteArray()))
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/8/30
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.bldm.client
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.plugins.defaultRequest
|
||||||
|
import io.ktor.client.plugins.websocket.*
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
|
||||||
|
internal val httpClient = HttpClient {
|
||||||
|
install(WebSockets)
|
||||||
|
|
||||||
|
defaultRequest {
|
||||||
|
header(HttpHeaders.UserAgent, USER_AGENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const val USER_AGENT =
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/8/30
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package test
|
||||||
|
|
||||||
|
import cn.rtast.bldm.client.connectToBlDM
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.io.buffered
|
||||||
|
import kotlinx.io.files.Path
|
||||||
|
import kotlinx.io.files.SystemFileSystem
|
||||||
|
import kotlinx.io.readString
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class TestClient {
|
||||||
|
|
||||||
|
private val cookie = SystemFileSystem.source(Path("src/commonTest/resources/cookie.txt"))
|
||||||
|
.buffered().readString()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test bldm client`() = runTest {
|
||||||
|
connectToBlDM(6, cookie) {
|
||||||
|
onHeartbeat {
|
||||||
|
println("Heartbeat")
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage {
|
||||||
|
println(it.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
onAuthReply {
|
||||||
|
println("auth reply")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Rename this file to cookie.txt and paste cookie here
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/8/30
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.bldm.core.annotations
|
||||||
|
|
||||||
|
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
|
||||||
|
public annotation class InternalBldmApi
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
package cn.rtast.bldm.core
|
package cn.rtast.bldm.core
|
||||||
|
|
||||||
public const val REAL_ROOM_ID_URL: String = "https://api.live.bilibili.com/room/v1/Room/room_init?id="
|
public object BLDMConstants {
|
||||||
public const val DM_SERVER_CONF_URL: String = "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id="
|
public const val REAL_ROOM_ID_URL: String = "https://api.live.bilibili.com/room/v1/Room/room_init?id="
|
||||||
public const val USER_NAV_URL: String = "https://api.bilibili.com/x/web-interface/nav"
|
public const val USER_NAV_URL: String = "https://api.bilibili.com/x/web-interface/nav"
|
||||||
|
public const val DM_SERVER_CONF_URL: String =
|
||||||
|
"https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo"
|
||||||
|
}
|
||||||
@@ -16,9 +16,15 @@ import kotlin.time.Clock
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
public data class UserNavData(
|
public data class UserNavData(
|
||||||
@SerialName("wbi_img")
|
val data: NavData,
|
||||||
val wbiImg: WbiImg,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
public data class NavData(
|
||||||
|
@SerialName("wbi_img")
|
||||||
|
val wbiImg: WbiImg,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
public data class WbiImg(
|
public data class WbiImg(
|
||||||
@SerialName("img_url")
|
@SerialName("img_url")
|
||||||
@@ -67,8 +73,8 @@ public data class UserNavData(
|
|||||||
* get resorted mixin key
|
* get resorted mixin key
|
||||||
*/
|
*/
|
||||||
private fun getMixinKey(): String =
|
private fun getMixinKey(): String =
|
||||||
(wbiImg.imgUrl.substringAfterLast('/').removeSuffix(".png") +
|
(data.wbiImg.imgUrl.substringAfterLast('/').removeSuffix(".png") +
|
||||||
wbiImg.subUrl.substringAfterLast('/').removeSuffix(".png")).let { s ->
|
data.wbiImg.subUrl.substringAfterLast('/').removeSuffix(".png")).let { s ->
|
||||||
buildString { repeat(32) { append(s[MIXIN_KEY_ENC_TAB[it]]) } }
|
buildString { repeat(32) { append(s[MIXIN_KEY_ENC_TAB[it]]) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2026 RTAkland
|
|
||||||
* Author: RTAkland
|
|
||||||
* Date: 2026/8/30
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package cn.rtast.bldm.core.data
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@Suppress("ClassName")
|
|
||||||
internal data class _wbi_sign_payload(
|
|
||||||
/**
|
|
||||||
* room id
|
|
||||||
*/
|
|
||||||
val id: Long,
|
|
||||||
/**
|
|
||||||
* always be 0
|
|
||||||
*/
|
|
||||||
val type: Int,
|
|
||||||
/**
|
|
||||||
* current unix timestamp (seconds)
|
|
||||||
*/
|
|
||||||
val wts: Long
|
|
||||||
)
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
package cn.rtast.bldm.core.data.protocol
|
package cn.rtast.bldm.core.data.protocol
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -15,7 +14,7 @@ internal data class AuthPayload(
|
|||||||
/**
|
/**
|
||||||
* user id
|
* user id
|
||||||
*/
|
*/
|
||||||
val uid: Int,
|
val uid: Long,
|
||||||
/**
|
/**
|
||||||
* room id
|
* room id
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/8/30
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.bldm.core.data.protocol
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
public data class DanmuMessage(
|
||||||
|
val content: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/8/30
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.bldm.core.data.protocol
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
public data class PacketMetadata(
|
||||||
|
val packetLength: Int,
|
||||||
|
val protocolType: Int,
|
||||||
|
val opCode: Int
|
||||||
|
)
|
||||||
@@ -78,6 +78,6 @@ public data class Packet(
|
|||||||
writeShort(protocolVersion)
|
writeShort(protocolVersion)
|
||||||
writeInt(packetType)
|
writeInt(packetType)
|
||||||
writeInt(sequence)
|
writeInt(sequence)
|
||||||
write(body, body.size)
|
write(body, 0, body.size)
|
||||||
}.readByteArray()
|
}.readByteArray()
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,63 @@
|
|||||||
* Date: 2026/8/30
|
* Date: 2026/8/30
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package cn.rtast.bldm.core.protocol
|
package cn.rtast.bldm.core.protocol
|
||||||
|
|
||||||
class PacketDecoder {
|
import cn.rtast.bldm.core.data.protocol.DanmuMessage
|
||||||
|
import cn.rtast.bldm.core.protocol.ProtocolVersion.Raw
|
||||||
|
import cn.rtast.bldm.core.protocol.ProtocolVersion.Zlib
|
||||||
|
import cn.rtast.bldm.core.protocol.event.PacketEvents
|
||||||
|
import cn.rtast.bldm.core.util.zlibDecompress
|
||||||
|
import kotlinx.io.Buffer
|
||||||
|
import kotlinx.io.readByteArray
|
||||||
|
|
||||||
|
public class PacketDecoder(eventsBuilder: PacketEvents.() -> Unit) {
|
||||||
|
private val events = PacketEvents().apply(eventsBuilder)
|
||||||
|
private val persistentBuffer = Buffer()
|
||||||
|
|
||||||
|
public fun decode(bytes: ByteArray) {
|
||||||
|
persistentBuffer.write(bytes)
|
||||||
|
this.decodeBuffer(persistentBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeBuffer(buffer: Buffer) {
|
||||||
|
while (buffer.size >= 16) {
|
||||||
|
val pk = buffer.peek()
|
||||||
|
val packetLength = pk.readInt()
|
||||||
|
val headerLength = pk.readShort()
|
||||||
|
val protocolVersion = pk.readShort()
|
||||||
|
val opCode = pk.readInt()
|
||||||
|
|
||||||
|
if (buffer.size < packetLength) break
|
||||||
|
|
||||||
|
val currentPacket = buffer.readByteArray(packetLength)
|
||||||
|
val bodySize = packetLength - headerLength
|
||||||
|
if (bodySize <= 0) continue
|
||||||
|
|
||||||
|
val body = currentPacket.copyOfRange(headerLength.toInt(), packetLength)
|
||||||
|
|
||||||
|
when (ProtocolVersion.fromCode(protocolVersion)) {
|
||||||
|
Raw -> handlePayload(opCode, body)
|
||||||
|
Zlib -> {
|
||||||
|
val decompressed = body.zlibDecompress()
|
||||||
|
val decompressedBuffer = Buffer().apply { write(decompressed) }
|
||||||
|
decodeBuffer(decompressedBuffer)
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePayload(opCode: Int, body: ByteArray) {
|
||||||
|
when (PacketType.fromCode(opCode)) {
|
||||||
|
PacketType.AUTH_REPLY -> events.authReplyHandler?.invoke()
|
||||||
|
PacketType.HEARTBEAT_REPLY -> events.heartbeatHandler?.invoke()
|
||||||
|
PacketType.Message -> events.messageHandler?.invoke(decodeDanmuMessage(body))
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeDanmuMessage(bytes: ByteArray): DanmuMessage {
|
||||||
|
return DanmuMessage(bytes.decodeToString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,11 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@file:OptIn(InternalBldmApi::class)
|
||||||
|
|
||||||
package cn.rtast.bldm.core.protocol
|
package cn.rtast.bldm.core.protocol
|
||||||
|
|
||||||
|
import cn.rtast.bldm.core.annotations.InternalBldmApi
|
||||||
import cn.rtast.bldm.core.data.protocol.AuthPayload
|
import cn.rtast.bldm.core.data.protocol.AuthPayload
|
||||||
import cn.rtast.bldm.core.util.AutoIncrementInt
|
import cn.rtast.bldm.core.util.AutoIncrementInt
|
||||||
import cn.rtast.bldm.core.util.encodeJson
|
import cn.rtast.bldm.core.util._encodeJson
|
||||||
|
|
||||||
public class PacketEncoder {
|
public class PacketEncoder {
|
||||||
public companion object {
|
public companion object {
|
||||||
@@ -18,10 +21,10 @@ public class PacketEncoder {
|
|||||||
|
|
||||||
private val _sequence by AutoIncrementInt()
|
private val _sequence by AutoIncrementInt()
|
||||||
|
|
||||||
public fun authPacket(uid: Int, roomId: Long, buvid: String, token: String): Packet {
|
public fun authPacket(uid: Long, roomId: Long, buvid: String, token: String): Packet {
|
||||||
val payload = AuthPayload(uid, roomId, 2, buvid, "web", 2, token)
|
val payload = AuthPayload(uid, roomId, 2, buvid, "web", 2, token)
|
||||||
.encodeJson().encodeToByteArray()
|
._encodeJson().encodeToByteArray()
|
||||||
return Packet(packetType = PacketType.AUTH, sequence = _sequence, body = payload)
|
return Packet(packetType = PacketType.AUTH.pkCode, sequence = _sequence, body = payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun heartbeatPacket(): Packet = Packet(packetType = 2, sequence = _sequence, body = HEARTBEAT_BODY)
|
public fun heartbeatPacket(): Packet = Packet(packetType = 2, sequence = _sequence, body = HEARTBEAT_BODY)
|
||||||
|
|||||||
@@ -7,12 +7,15 @@
|
|||||||
|
|
||||||
package cn.rtast.bldm.core.protocol
|
package cn.rtast.bldm.core.protocol
|
||||||
|
|
||||||
public object PacketType {
|
public enum class PacketType(public val pkCode: Int) {
|
||||||
public const val AUTH: Int = 7
|
AUTH(7),
|
||||||
public const val AUTH_REPLY: Int = 8
|
AUTH_REPLY(8),
|
||||||
public const val HEARTBEAT: Int = 2
|
HEARTBEAT(2),
|
||||||
public const val HANDSHAKE_REPLY: Int = 1
|
HEARTBEAT_REPLY(3),
|
||||||
public const val HEARTBEAT_REPLY: Int = 3
|
Message(5);
|
||||||
public const val SEND_MSG_REPLY: Int = 5
|
|
||||||
// TODO
|
public companion object {
|
||||||
|
private val map = PacketType.entries.associateBy { it.pkCode }
|
||||||
|
public fun fromCode(code: Int): PacketType? = map[code]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/8/30
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.bldm.core.protocol
|
||||||
|
|
||||||
|
internal enum class ProtocolVersion(val proto: Short) {
|
||||||
|
Raw(0), Zlib(2);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val map = entries.associateBy { it.proto }
|
||||||
|
fun fromCode(code: Short): ProtocolVersion? = map[code]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/8/30
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.bldm.core.protocol.event
|
||||||
|
|
||||||
|
import cn.rtast.bldm.core.data.protocol.DanmuMessage
|
||||||
|
|
||||||
|
public class PacketEvents {
|
||||||
|
internal var messageHandler: ((DanmuMessage) -> Unit)? = null
|
||||||
|
internal var heartbeatHandler: (() -> Unit)? = null
|
||||||
|
internal var authReplyHandler: (() -> Unit)? = null
|
||||||
|
|
||||||
|
public fun onMessage(block: (DanmuMessage) -> Unit) {
|
||||||
|
messageHandler = block
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun onHeartbeat(block: () -> Unit) {
|
||||||
|
heartbeatHandler = block
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun onAuthReply(block: () -> Unit) {
|
||||||
|
authReplyHandler = block
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/8/30
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.bldm.core.util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get buvid and uid from a cookie
|
||||||
|
* first -> buvid
|
||||||
|
* second -> uid
|
||||||
|
*/
|
||||||
|
public fun parseBuvidAndUid(cookie: String): Pair<String?, Long?> {
|
||||||
|
if (cookie.isBlank()) return Pair(null, null)
|
||||||
|
var buvid: String? = null
|
||||||
|
var uid: Long? = null
|
||||||
|
cookie.split(';').forEach { entry ->
|
||||||
|
val parts = entry.split('=', limit = 2)
|
||||||
|
if (parts.size == 2) {
|
||||||
|
when (parts[0].trim()) {
|
||||||
|
"buvid3" -> buvid = parts[1].trim()
|
||||||
|
"DedeUserID" -> uid = parts[1].trim().toLongOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buvid to uid
|
||||||
|
}
|
||||||
@@ -4,11 +4,21 @@
|
|||||||
* Date: 2026/8/30
|
* Date: 2026/8/30
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("FunctionName")
|
||||||
|
|
||||||
package cn.rtast.bldm.core.util
|
package cn.rtast.bldm.core.util
|
||||||
|
|
||||||
|
import cn.rtast.bldm.core.annotations.InternalBldmApi
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
internal inline fun <reified T> String.fromJson(): T = Json.decodeFromString(this)
|
@InternalBldmApi
|
||||||
|
@Suppress("ObjectPropertyName")
|
||||||
|
public val _json: Json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
internal inline fun <reified T> T.encodeJson(): String = Json.encodeToString(this)
|
@InternalBldmApi
|
||||||
|
public inline fun <reified T> String._fromJson(): T = _json.decodeFromString(this)
|
||||||
|
|
||||||
|
@InternalBldmApi
|
||||||
|
public inline fun <reified T> T._encodeJson(): String = _json.encodeToString(this)
|
||||||
@@ -16,7 +16,7 @@ class TestWbiSign {
|
|||||||
fun `test wbi sign`() {
|
fun `test wbi sign`() {
|
||||||
val imgUrl = "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png"
|
val imgUrl = "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png"
|
||||||
val subUrl = "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png"
|
val subUrl = "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png"
|
||||||
val signWbi = UserNavData(UserNavData.WbiImg(imgUrl, subUrl)).signWbi(27245513)
|
val signWbi = UserNavData(UserNavData.NavData(UserNavData.WbiImg(imgUrl, subUrl))).signWbi(27245513)
|
||||||
println(signWbi)
|
println(signWbi)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.util.zip.Inflater
|
import java.util.zip.Inflater
|
||||||
|
|
||||||
public actual fun ByteArray.zlibDecompress(): ByteArray {
|
public actual fun ByteArray.zlibDecompress(): ByteArray {
|
||||||
val inflater = Inflater(true)
|
val inflater = Inflater(false)
|
||||||
val outputStream = ByteArrayOutputStream(this.size)
|
val outputStream = ByteArrayOutputStream(this.size)
|
||||||
val buffer = ByteArray(1024)
|
val buffer = ByteArray(1024)
|
||||||
inflater.setInput(this)
|
inflater.setInput(this)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
* Date: 2026/8/30
|
* Date: 2026/8/30
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalForeignApi::class)
|
@file:OptIn(ExperimentalForeignApi::class)
|
||||||
|
|
||||||
package cn.rtast.bldm.core.util
|
package cn.rtast.bldm.core.util
|
||||||
@@ -12,7 +11,7 @@ package cn.rtast.bldm.core.util
|
|||||||
import kotlinx.cinterop.*
|
import kotlinx.cinterop.*
|
||||||
import platform.zlib.*
|
import platform.zlib.*
|
||||||
|
|
||||||
private const val MAX_WBITS = 15
|
private const val ENABLE_ZLIB_GZIP_HEADER = 15 + 32
|
||||||
|
|
||||||
public actual fun ByteArray.zlibDecompress(): ByteArray {
|
public actual fun ByteArray.zlibDecompress(): ByteArray {
|
||||||
if (isEmpty()) return ByteArray(0)
|
if (isEmpty()) return ByteArray(0)
|
||||||
@@ -21,7 +20,14 @@ public actual fun ByteArray.zlibDecompress(): ByteArray {
|
|||||||
stream.zalloc = null
|
stream.zalloc = null
|
||||||
stream.zfree = null
|
stream.zfree = null
|
||||||
stream.opaque = null
|
stream.opaque = null
|
||||||
check(inflateInit2_(stream.ptr, MAX_WBITS, ZLIB_VERSION, sizeOf<z_stream>().toInt()) == Z_OK) { "inflateInit2_ failed" }
|
|
||||||
|
val initResult = inflateInit2_(
|
||||||
|
stream.ptr,
|
||||||
|
ENABLE_ZLIB_GZIP_HEADER,
|
||||||
|
ZLIB_VERSION,
|
||||||
|
sizeOf<z_stream>().toInt()
|
||||||
|
)
|
||||||
|
check(initResult == Z_OK) { "inflateInit2_ failed with code: $initResult" }
|
||||||
|
|
||||||
val inputPinned = this@zlibDecompress.pin()
|
val inputPinned = this@zlibDecompress.pin()
|
||||||
try {
|
try {
|
||||||
@@ -31,14 +37,15 @@ public actual fun ByteArray.zlibDecompress(): ByteArray {
|
|||||||
val bufferSize = 4096
|
val bufferSize = 4096
|
||||||
val tempBuffer = ByteArray(bufferSize)
|
val tempBuffer = ByteArray(bufferSize)
|
||||||
val tempPinned = tempBuffer.pin()
|
val tempPinned = tempBuffer.pin()
|
||||||
val output = ArrayList<Byte>(this@zlibDecompress.size * 2)
|
val output = ArrayList<Byte>(this@zlibDecompress.size * 3)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
var result: Int
|
||||||
do {
|
do {
|
||||||
stream.next_out = tempPinned.addressOf(0).reinterpret()
|
stream.next_out = tempPinned.addressOf(0).reinterpret()
|
||||||
stream.avail_out = bufferSize.toUInt()
|
stream.avail_out = bufferSize.toUInt()
|
||||||
|
|
||||||
val result = inflate(stream.ptr, Z_NO_FLUSH)
|
result = inflate(stream.ptr, Z_NO_FLUSH)
|
||||||
check(result == Z_OK || result == Z_STREAM_END) { "inflate error: $result" }
|
check(result == Z_OK || result == Z_STREAM_END) { "inflate error: $result" }
|
||||||
|
|
||||||
val bytesDecompressed = bufferSize - stream.avail_out.toInt()
|
val bytesDecompressed = bufferSize - stream.avail_out.toInt()
|
||||||
@@ -47,7 +54,7 @@ public actual fun ByteArray.zlibDecompress(): ByteArray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result == Z_STREAM_END) break
|
if (result == Z_STREAM_END) break
|
||||||
} while (stream.avail_out == 0u)
|
} while (stream.avail_in > 0u || stream.avail_out == 0u)
|
||||||
} finally {
|
} finally {
|
||||||
tempPinned.unpin()
|
tempPinned.unpin()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user