Introduce event dispatcher and fix heartbeat, auth reply events

This commit is contained in:
2026-08-31 15:43:44 +08:00
parent 0e09422b6e
commit fe49e4ff63
34 files changed
+686 -345

No files matched your search

+1 -3
View File
@@ -16,9 +16,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
api("org.kotlincrypto.hash:md:0.8.0")
api("org.jetbrains.kotlinx:kotlinx-io-core:0.9.1")
implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.9.1")
}
commonTest.dependencies {
@@ -1,13 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/30
*/
package cn.rtast.bldm.codec.annotations
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
public annotation class InternalBldmApi
@@ -1,14 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/29
*/
package cn.rtast.bldm.codec
public object BLDMConstants {
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 DM_SERVER_CONF_URL: String =
"https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo"
}
@@ -1,37 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/29
*/
package cn.rtast.bldm.codec.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable
public data class DMServerConf(
val data: ServerConf,
) {
@Serializable
public data class ServerConf(
val token: String,
@SerialName("host_list")
val hostList: List<ServerHost>,
)
@Serializable
public data class ServerHost(
val host: String,
val port: Int,
@SerialName("wss_port")
val wssPort: Int,
@SerialName("ws_port")
val wsPort: Int,
)
@Transient
public val defaultServerHost: ServerHost = ServerHost("broadcastlv.chat.bilibili.com", 2243, 2245, 2244)
}
@@ -1,22 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/29
*/
package cn.rtast.bldm.codec.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
public data class RealRoomId(
val data: RoomId
) {
@Serializable
public data class RoomId(
@SerialName("room_id")
val roomId: Long
)
}
@@ -1,93 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/30
*/
@file:OptIn(ExperimentalUnsignedTypes::class)
package cn.rtast.bldm.codec.data
import cn.rtast.bldm.codec.util.digest
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.time.Clock
@Serializable
public data class UserNavData(
val data: NavData,
) {
@Serializable
public data class NavData(
@SerialName("wbi_img")
val wbiImg: WbiImg,
)
@Serializable
public data class WbiImg(
@SerialName("img_url")
val imgUrl: String,
@SerialName("sub_url")
val subUrl: String,
)
private companion object {
val MIXIN_KEY_ENC_TAB = intArrayOf(
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52
)
fun String.encodeURIComponent(): String {
val bytes = this.encodeToByteArray()
val sb = StringBuilder()
for (b in bytes) {
val c = b.toInt().and(0xFF).toChar()
if (c in 'A'..'Z' || c in 'a'..'z' || c in '0'..'9' || c == '-' || c == '_' || c == '.' || c == '~') {
sb.append(c)
} else {
val hex = (b.toInt() and 0xFF).toString(16).uppercase()
sb.append('%')
if (hex.length == 1) sb.append('0')
sb.append(hex)
}
}
return sb.toString()
}
fun Map<String, Any?>.toQueryString(): String {
return this.mapNotNull { (key, value) ->
if (value != null) {
"${key.encodeURIComponent()}=${value.toString().encodeURIComponent()}"
} else {
null
}
}.joinToString("&")
}
}
/**
* get resorted mixin key
*/
private fun getMixinKey(): String =
(data.wbiImg.imgUrl.substringAfterLast('/').removeSuffix(".png") +
data.wbiImg.subUrl.substringAfterLast('/').removeSuffix(".png")).let { s ->
buildString { repeat(32) { append(s[MIXIN_KEY_ENC_TAB[it]]) } }
}
public fun signWbi(room: Long): String {
// val payload = _wbi_sign_payload(room, 0, Clock.System.now().epochSeconds) // ref
val wts = Clock.System.now().epochSeconds
val params = mapOf("id" to room, "type" to 0)
.entries.sortedBy { it.key }.associate { it.key to it.value }.toMutableMap()
return buildString {
append(params.toQueryString())
params["wts"] = wts
append("&wts=$wts")
append("&w_rid=${(params.toQueryString() + getMixinKey()).digest()}")
}
}
}
@@ -7,9 +7,6 @@
package cn.rtast.bldm.codec.data.protocol
import kotlinx.serialization.Serializable
@Serializable
internal data class AuthPayload(
/**
* user id
@@ -18,12 +15,12 @@ internal data class AuthPayload(
/**
* room id
*/
val roomid: Long,
val roomId: Long,
/**
* protocol version
* always be 2
*/
val protover: Int,
val protocolVersion: Int,
/**
* device id
*/
@@ -39,5 +36,24 @@ internal data class AuthPayload(
/**
* token
*/
val key: String
)
val key: String,
) {
override fun toString(): String =
"""{"uid":$uid,
|"roomid":$roomId,
|"protover":$protocolVersion,
|"buvid":"${buvid.escapeJson()}",
|"platform":"${platform.escapeJson()}",
|"type":$type,
|"key":"${key.escapeJson()}"
|}""".trimMargin()
private fun String.escapeJson(): String = this
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\b", "\\b")
.replace("\u000C", "\\f")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
}
@@ -1,17 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/30
*/
package cn.rtast.bldm.codec.data.protocol
import kotlinx.serialization.Serializable
@Serializable
public data class PacketMetadata(
val packetLength: Int,
val protocolType: Int,
val opCode: Int
)
@@ -1,13 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/30
*/
package cn.rtast.bldm.codec.data.protocol
import kotlinx.serialization.Serializable
@Serializable
public data class RawDanmuMessage(val content: String)
@@ -0,0 +1,14 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/31
*/
package cn.rtast.bldm.codec.event
public sealed interface DanmuEvent {
public data object AuthReplyEvent : DanmuEvent
public data object HeartbeatEvent : DanmuEvent
public data class MessageEvent(val content: String) : DanmuEvent
}
@@ -0,0 +1,25 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/31
*/
package cn.rtast.bldm.codec.event
import kotlin.reflect.KClass
public open class DanmuEventDispatcher {
@PublishedApi
internal val eventHandlers: MutableMap<KClass<out DanmuEvent>, MutableList<suspend (DanmuEvent) -> Unit>> =
mutableMapOf()
public suspend fun dispatch(event: DanmuEvent) {
eventHandlers[event::class]?.forEach { it.invoke(event) }
}
public inline fun <reified T : DanmuEvent> on(crossinline block: (T) -> Unit) {
val handlers = eventHandlers.getOrPut(T::class) { mutableListOf() }
handlers.add { event -> block(event as T) }
}
}
@@ -72,6 +72,9 @@ public data class Packet(
return result
}
/**
* Encoding to [ByteArray]
*/
public fun toByteArray(): ByteArray = Buffer().apply {
writeInt(16 + body.size)
writeShort(headerLength)
@@ -7,12 +7,45 @@
package cn.rtast.bldm.codec.protocol
/**
* packet layer type
* also represents opcode
*/
public enum class PacketType(public val pkCode: Int) {
/**
* auth packet type
* serverbound
*/
AUTH(7),
/**
* server reply auth packet
* clientbound
*/
AUTH_REPLY(8),
/**
* heartbeat type
* serverbound
*/
HEARTBEAT(2),
/**
* server reply heartbeat packet
* clientbound
*/
HEARTBEAT_REPLY(3),
Message(5);
/**
* danmu or event packet
* clientbound
*/
Message(5),
/**
* custom packet type
*/
Unknown(-999);
public companion object {
private val map = PacketType.entries.associateBy { it.pkCode }
@@ -7,8 +7,25 @@
package cn.rtast.bldm.codec.protocol
/**
* danmu packet protocol version
* indicates whether the packet is compressed
*/
internal enum class ProtocolVersion(val proto: Short) {
Raw(0), Zlib(2);
/**
* raw variant 0
*/
Raw0(0),
/**
* raw variant 1
*/
Raw1(1),
/**
* zlib compressed
*/
Zlib(2);
companion object {
private val map = entries.associateBy { it.proto }
@@ -6,63 +6,56 @@
package cn.rtast.bldm.codec.protocol.codec
import cn.rtast.bldm.codec.data.protocol.RawDanmuMessage
import cn.rtast.bldm.codec.event.DanmuEvent
import cn.rtast.bldm.codec.event.DanmuEventDispatcher
import cn.rtast.bldm.codec.protocol.PacketType
import cn.rtast.bldm.codec.protocol.ProtocolVersion
import cn.rtast.bldm.codec.protocol.ProtocolVersion.Raw
import cn.rtast.bldm.codec.protocol.ProtocolVersion.Zlib
import cn.rtast.bldm.codec.protocol.event.PacketEvents
import cn.rtast.bldm.codec.protocol.ProtocolVersion.*
import cn.rtast.bldm.codec.util.zlibDecompress
import kotlinx.io.Buffer
import kotlinx.io.readByteArray
public class PacketDecoder(eventsBuilder: PacketEvents.() -> Unit) {
private val events = PacketEvents().apply(eventsBuilder)
public class PacketDecoder(private val dispatcher: DanmuEventDispatcher) {
private val persistentBuffer = Buffer()
public fun decode(bytes: ByteArray) {
public suspend fun decode(bytes: ByteArray) {
persistentBuffer.write(bytes)
this.decodeBuffer(persistentBuffer)
}
private fun decodeBuffer(buffer: Buffer) {
/**
* process data packet, including the slicing and decoding
*/
private suspend 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()
val headerLength = pk.readShort().toInt()
val protocolVersion = ProtocolVersion.fromCode(pk.readShort())
val packetType = PacketType.fromCode(pk.readInt()) ?: PacketType.Unknown
if (buffer.size < packetLength) break
val currentPacket = buffer.readByteArray(packetLength)
buffer.skip(headerLength.toLong())
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 body = if (bodySize > 0) buffer.readByteArray(bodySize) else null
when (protocolVersion) {
Raw1, Raw0 -> handlePayload(packetType, body)
Zlib -> body?.let {
val decompressed = it.zlibDecompress()
val decompressedBuffer = Buffer().apply { write(decompressed) }
decodeBuffer(decompressedBuffer)
}
null -> {}
else -> {}
}
}
}
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))
private suspend fun handlePayload(packetType: PacketType, body: ByteArray?) {
when (packetType) {
PacketType.AUTH_REPLY -> dispatcher.dispatch(DanmuEvent.AuthReplyEvent)
PacketType.HEARTBEAT_REPLY -> dispatcher.dispatch(DanmuEvent.HeartbeatEvent)
PacketType.Message -> body?.let { dispatcher.dispatch(DanmuEvent.MessageEvent(it.decodeToString())) }
else -> {}
}
}
private fun decodeDanmuMessage(bytes: ByteArray): RawDanmuMessage {
return RawDanmuMessage(bytes.decodeToString())
}
}
@@ -5,16 +5,12 @@
*/
@file:OptIn(InternalBldmApi::class)
package cn.rtast.bldm.codec.protocol.codec
import cn.rtast.bldm.codec.annotations.InternalBldmApi
import cn.rtast.bldm.codec.data.protocol.AuthPayload
import cn.rtast.bldm.codec.protocol.Packet
import cn.rtast.bldm.codec.protocol.PacketType
import cn.rtast.bldm.codec.util.AutoIncrementInt
import cn.rtast.bldm.codec.util._encodeJson
public class PacketEncoder {
public companion object {
@@ -24,10 +20,13 @@ public class PacketEncoder {
private val _sequence by AutoIncrementInt()
public fun authPacket(uid: Long, roomId: Long, buvid: String, token: String): Packet {
val payload = AuthPayload(uid, roomId, 2, buvid, "web", 2, token)
._encodeJson().encodeToByteArray()
val payload = AuthPayload(uid, roomId, 2, buvid, "web", 2, token).toString().encodeToByteArray()
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 = PacketType.HEARTBEAT.pkCode,
sequence = _sequence,
body = HEARTBEAT_BODY
)
}
@@ -1,28 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/30
*/
package cn.rtast.bldm.codec.protocol.event
import cn.rtast.bldm.codec.data.protocol.RawDanmuMessage
public class PacketEvents {
internal var messageHandler: ((RawDanmuMessage) -> Unit)? = null
internal var heartbeatHandler: (() -> Unit)? = null
internal var authReplyHandler: (() -> Unit)? = null
public fun onMessage(block: (RawDanmuMessage) -> Unit) {
messageHandler = block
}
public fun onHeartbeat(block: () -> Unit) {
heartbeatHandler = block
}
public fun onAuthReply(block: () -> Unit) {
authReplyHandler = block
}
}
@@ -1,14 +1,252 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/30
* Date: 2026/8/31
*/
package cn.rtast.bldm.codec.util
import org.kotlincrypto.hash.md.MD5
internal val md5Instance = MD5()
internal object CycloneMd5 {
const val BLOCK_SIZE = 64
const val DIGEST_SIZE = 16
const val MIN_PAD_SIZE = 9
internal fun String.digest(): String = md5Instance.digest(this.encodeToByteArray()).toHexString()
val OID = byteArrayOf(
0x2A.toByte(), 0x86.toByte(), 0x48.toByte(), 0x86.toByte(),
0xF7.toByte(), 0x0D.toByte(), 0x02.toByte(), 0x05.toByte()
)
private val PADDING = ByteArray(64).apply { this[0] = 0x80.toByte() }
private val K = intArrayOf(
0xD76AA478.toInt(), 0xE8C7B756.toInt(), 0x242070DB, 0xC1BDCEEE.toInt(),
0xF57C0FAF.toInt(), 0x4787C62A, 0xA8304613.toInt(), 0xFD469501.toInt(),
0x698098D8, 0x8B44F7AF.toInt(), 0xFFFF5BB1.toInt(), 0x895CD7BE.toInt(),
0x6B901122, 0xFD987193.toInt(), 0xA679438E.toInt(), 0x49B40821,
0xF61E2562.toInt(), 0xC040B340.toInt(), 0x265E5A51, 0xE9B6C7AA.toInt(),
0xD62F105D.toInt(), 0x02441453, 0xD8A1E681.toInt(), 0xE7D3FBC8.toInt(),
0x21E1CDE6, 0xC33707D6.toInt(), 0xF4D50D87.toInt(), 0x455A14ED,
0xA9E3E905.toInt(), 0xFCEFA3F8.toInt(), 0x676F02D9, 0x8D2A4C8A.toInt(),
0xFFFA3942.toInt(), 0x8771F681.toInt(), 0x6D9D6122, 0xFDE5380C.toInt(),
0xA4BEEA44.toInt(), 0x4BDECFA9, 0xF6BB4B60.toInt(), 0xBEBFBC70.toInt(),
0x289B7EC6, 0xEAA127FA.toInt(), 0xD4EF3085.toInt(), 0x04881D05,
0xD9D4D039.toInt(), 0xE6DB99E5.toInt(), 0x1FA27CF8, 0xC4AC5665.toInt(),
0xF4292244.toInt(), 0x432AFF97, 0xAB9423A7.toInt(), 0xFC93A039.toInt(),
0x655B59C3, 0x8F0CCC92.toInt(), 0xFFEFF47D.toInt(), 0x85845DD1.toInt(),
0x6FA87E4F, 0xFE2CE6E0.toInt(), 0xA3014314.toInt(), 0x4E0811A1,
0xF7537E82.toInt(), 0xBD3AF235.toInt(), 0x2AD7D2BB, 0xEB86D391.toInt()
)
private class Context {
val h = IntArray(4)
val buffer = ByteArray(64)
val x = IntArray(16)
var size: Int = 0
var totalSize: Long = 0L
}
fun compute(data: ByteArray): ByteArray {
val digest = ByteArray(DIGEST_SIZE)
val context = Context()
initContext(context)
updateContext(context, data, 0, data.size)
finalContext(context, digest)
return digest
}
fun computeToHex(data: ByteArray): String {
return compute(data).toHexString()
}
fun computeToHex(text: String): String {
return compute(text.encodeToByteArray()).toHexString()
}
private fun initContext(context: Context) {
context.h[0] = 0x67452301
context.h[1] = 0xEFCDAB89.toInt()
context.h[2] = 0x98BADCFE.toInt()
context.h[3] = 0x10325476
context.size = 0
context.totalSize = 0L
}
private fun updateContext(context: Context, data: ByteArray, offset: Int, length: Int) {
var dataOffset = offset
var remLength = length
while (remLength > 0) {
val n = minOf(remLength, 64 - context.size)
data.copyInto(context.buffer, context.size, dataOffset, dataOffset + n)
context.size += n
context.totalSize += n
dataOffset += n
remLength -= n
if (context.size == 64) {
processBlock(context)
context.size = 0
}
}
}
private fun finalContext(context: Context, digest: ByteArray) {
var totalBits: Long = context.totalSize * 8L
val paddingSize = if (context.size < 56) {
56 - context.size
} else {
64 + 56 - context.size
}
updateContext(context, PADDING, 0, paddingSize)
for (i in 0 until 8) {
context.buffer[56 + i] = (totalBits and 0xFFL).toByte()
totalBits = totalBits ushr 8
}
processBlock(context)
for (i in 0 until (DIGEST_SIZE / 4)) {
store32le(context.h[i], digest, i * 4)
}
}
private fun processBlock(context: Context) {
var a = context.h[0]
var b = context.h[1]
var c = context.h[2]
var d = context.h[3]
val x = context.x
for (i in 0 until 16) {
x[i] = load32le(context.buffer, i * 4)
}
// Round 1
a = ff(a, b, c, d, x[0], 7, K[0])
d = ff(d, a, b, c, x[1], 12, K[1])
c = ff(c, d, a, b, x[2], 17, K[2])
b = ff(b, c, d, a, x[3], 22, K[3])
a = ff(a, b, c, d, x[4], 7, K[4])
d = ff(d, a, b, c, x[5], 12, K[5])
c = ff(c, d, a, b, x[6], 17, K[6])
b = ff(b, c, d, a, x[7], 22, K[7])
a = ff(a, b, c, d, x[8], 7, K[8])
d = ff(d, a, b, c, x[9], 12, K[9])
c = ff(c, d, a, b, x[10], 17, K[10])
b = ff(b, c, d, a, x[11], 22, K[11])
a = ff(a, b, c, d, x[12], 7, K[12])
d = ff(d, a, b, c, x[13], 12, K[13])
c = ff(c, d, a, b, x[14], 17, K[14])
b = ff(b, c, d, a, x[15], 22, K[15])
// Round 2
a = gg(a, b, c, d, x[1], 5, K[16])
d = gg(d, a, b, c, x[6], 9, K[17])
c = gg(c, d, a, b, x[11], 14, K[18])
b = gg(b, c, d, a, x[0], 20, K[19])
a = gg(a, b, c, d, x[5], 5, K[20])
d = gg(d, a, b, c, x[10], 9, K[21])
c = gg(c, d, a, b, x[15], 14, K[22])
b = gg(b, c, d, a, x[4], 20, K[23])
a = gg(a, b, c, d, x[9], 5, K[24])
d = gg(d, a, b, c, x[14], 9, K[25])
c = gg(c, d, a, b, x[3], 14, K[26])
b = gg(b, c, d, a, x[8], 20, K[27])
a = gg(a, b, c, d, x[13], 5, K[28])
d = gg(d, a, b, c, x[2], 9, K[29])
c = gg(c, d, a, b, x[7], 14, K[30])
b = gg(b, c, d, a, x[12], 20, K[31])
// Round 3
a = hh(a, b, c, d, x[5], 4, K[32])
d = hh(d, a, b, c, x[8], 11, K[33])
c = hh(c, d, a, b, x[11], 16, K[34])
b = hh(b, c, d, a, x[14], 23, K[35])
a = hh(a, b, c, d, x[1], 4, K[36])
d = hh(d, a, b, c, x[4], 11, K[37])
c = hh(c, d, a, b, x[7], 16, K[38])
b = hh(b, c, d, a, x[10], 23, K[39])
a = hh(a, b, c, d, x[13], 4, K[40])
d = hh(d, a, b, c, x[0], 11, K[41])
c = hh(c, d, a, b, x[3], 16, K[42])
b = hh(b, c, d, a, x[6], 23, K[43])
a = hh(a, b, c, d, x[9], 4, K[44])
d = hh(d, a, b, c, x[12], 11, K[45])
c = hh(c, d, a, b, x[15], 16, K[46])
b = hh(b, c, d, a, x[2], 23, K[47])
// Round 4
a = ii(a, b, c, d, x[0], 6, K[48])
d = ii(d, a, b, c, x[7], 10, K[49])
c = ii(c, d, a, b, x[14], 15, K[50])
b = ii(b, c, d, a, x[5], 21, K[51])
a = ii(a, b, c, d, x[12], 6, K[52])
d = ii(d, a, b, c, x[3], 10, K[53])
c = ii(c, d, a, b, x[10], 15, K[54])
b = ii(b, c, d, a, x[1], 21, K[55])
a = ii(a, b, c, d, x[8], 6, K[56])
d = ii(d, a, b, c, x[15], 10, K[57])
c = ii(c, d, a, b, x[6], 15, K[58])
b = ii(b, c, d, a, x[13], 21, K[59])
a = ii(a, b, c, d, x[4], 6, K[60])
d = ii(d, a, b, c, x[11], 10, K[61])
c = ii(c, d, a, b, x[2], 15, K[62])
b = ii(b, c, d, a, x[9], 21, K[63])
context.h[0] += a
context.h[1] += b
context.h[2] += c
context.h[3] += d
}
private fun ByteArray.toHexString(): String {
val hexChars = CharArray(size * 2)
val hexArray = "0123456789abcdef".toCharArray()
for (i in indices) {
val v = this[i].toInt() and 0xFF
hexChars[i * 2] = hexArray[v ushr 4]
hexChars[i * 2 + 1] = hexArray[v and 0x0F]
}
return hexChars.concatToString()
}
private fun rol32(a: Int, s: Int): Int = (a shl s) or (a ushr (32 - s))
private fun load32le(buf: ByteArray, offset: Int): Int {
return (buf[offset].toInt() and 0xFF) or
((buf[offset + 1].toInt() and 0xFF) shl 8) or
((buf[offset + 2].toInt() and 0xFF) shl 16) or
((buf[offset + 3].toInt() and 0xFF) shl 24)
}
private fun store32le(val32: Int, buf: ByteArray, offset: Int) {
buf[offset] = (val32 and 0xFF).toByte()
buf[offset + 1] = ((val32 ushr 8) and 0xFF).toByte()
buf[offset + 2] = ((val32 ushr 16) and 0xFF).toByte()
buf[offset + 3] = ((val32 ushr 24) and 0xFF).toByte()
}
private fun f(x: Int, y: Int, z: Int): Int = (x and y) or (x.inv() and z)
private fun g(x: Int, y: Int, z: Int): Int = (x and z) or (y and z.inv())
private fun h(x: Int, y: Int, z: Int): Int = x xor y xor z
private fun i(x: Int, y: Int, z: Int): Int = y xor (x or z.inv())
private fun ff(a: Int, b: Int, c: Int, d: Int, x: Int, s: Int, k: Int): Int =
rol32(a + f(b, c, d) + x + k, s) + b
private fun gg(a: Int, b: Int, c: Int, d: Int, x: Int, s: Int, k: Int): Int =
rol32(a + g(b, c, d) + x + k, s) + b
private fun hh(a: Int, b: Int, c: Int, d: Int, x: Int, s: Int, k: Int): Int =
rol32(a + h(b, c, d) + x + k, s) + b
private fun ii(a: Int, b: Int, c: Int, d: Int, x: Int, s: Int, k: Int): Int =
rol32(a + i(b, c, d) + x + k, s) + b
}
internal fun String.digest(): String = CycloneMd5.computeToHex(this)
@@ -1,30 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/30
*/
@file:Suppress("FunctionName")
package cn.rtast.bldm.codec.util
import cn.rtast.bldm.codec.annotations.InternalBldmApi
import kotlinx.serialization.json.Json
internal val dmMessageParser = Json {
ignoreUnknownKeys = true
classDiscriminator = "cmd"
}
@InternalBldmApi
@Suppress("ObjectPropertyName")
public val _json: Json = Json {
ignoreUnknownKeys = true
isLenient = true
}
@InternalBldmApi
public inline fun <reified T> String._fromJson(): T = _json.decodeFromString(this)
@InternalBldmApi
public inline fun <reified T> T._encodeJson(): String = _json.encodeToString(this)
@@ -0,0 +1,58 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/8/31
*/
package cn.rtast.bldm.codec.util
import kotlin.time.Clock
private val MIXIN_KEY_ENC_TAB = intArrayOf(
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52
)
private fun String.encodeURIComponent(): String {
val bytes = this.encodeToByteArray()
val sb = StringBuilder()
for (b in bytes) {
val c = b.toInt().and(0xFF).toChar()
if (c in 'A'..'Z' || c in 'a'..'z' || c in '0'..'9' || c == '-' || c == '_' || c == '.' || c == '~') {
sb.append(c)
} else {
val hex = (b.toInt() and 0xFF).toString(16).uppercase()
sb.append('%')
if (hex.length == 1) sb.append('0')
sb.append(hex)
}
}
return sb.toString()
}
private fun Map<String, Any?>.toQueryString(): String {
return this.mapNotNull { (key, value) ->
if (value != null) "${key.encodeURIComponent()}=${value.toString().encodeURIComponent()}" else null
}.joinToString("&")
}
private fun getMixinKey(imgUrl: String, subUrl: String): String =
(imgUrl.substringAfterLast('/').removeSuffix(".png") + subUrl.substringAfterLast('/')
.removeSuffix(".png")).let { s ->
buildString { repeat(32) { append(s[MIXIN_KEY_ENC_TAB[it]]) } }
}
public fun signWbi(room: Long, imgUrl: String, subUrl: String): String {
val wts = Clock.System.now().epochSeconds
val params = mapOf("id" to room, "type" to 0)
.entries.sortedBy { it.key }.associate { it.key to it.value }.toMutableMap()
return buildString {
append(params.toQueryString())
params["wts"] = wts
append("&wts=$wts")
append("&w_rid=${(params.toQueryString() + getMixinKey(imgUrl, subUrl)).digest()}")
}
}
@@ -7,18 +7,17 @@
package test
import org.kotlincrypto.hash.md.MD5
import cn.rtast.bldm.codec.util.digest
import kotlin.test.Test
import kotlin.test.assertEquals
class TestMd5 {
val target = "e10adc3949ba59abbe56e057f20f883e" // 123456
@Test
fun `test md5`() {
val target = "e10adc3949ba59abbe56e057f20f883e" // 123456
val res = MD5().digest("123456".encodeToByteArray()).toHexString()
assertEquals(target, res)
fun `test self md5`() {
val output = "123456".digest()
assertEquals(target, output)
}
}