Basic complete: packet parse, zlib decompress
This commit is contained in:
24 files changed
+410
-63
No files matched your search
@@ -19,12 +19,24 @@ kotlin {
|
||||
commonMain.dependencies {
|
||||
api(project(":bl-core"))
|
||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-cio:$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 {
|
||||
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
|
||||
Reference in New Issue
Block a user