Introduce event dispatcher and fix heartbeat, auth reply events
This commit is contained in:
34 files changed
+686
-345
No files matched your search
@@ -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 }
|
||||
|
||||
+25
-32
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user