Commit 67c79168 authored by jiahua.liu's avatar jiahua.liu

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt
parents e83377cf 77ddb6f7
package net.mamoe.mirai.qqandroid.network
import kotlinx.coroutines.*
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readBytes
import kotlinx.io.core.use
import kotlinx.io.core.*
import kotlinx.io.pool.ObjectPool
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.network.BotNetworkHandler
......@@ -15,11 +14,8 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.login.LoginPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.PacketId
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.RegPushReason
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.SvcReqRegisterPacket
import net.mamoe.mirai.utils.LockFreeLinkedList
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.unsafeWeakRef
import kotlin.coroutines.CoroutineContext
@UseExperimental(MiraiInternalAPI::class)
......@@ -47,43 +43,137 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
SvcReqRegisterPacket(bot.client, RegPushReason.setOnlineStatus).sendAndExpect<SvcReqRegisterPacket.Response>()
}
/**
* 单线程处理包的接收, 分割和连接.
*/
@Suppress("PrivatePropertyName")
private val PacketReceiveDispatcher = newCoroutineDispatcher(1)
var lastPacket: ByteArray? = null
internal fun launchPacketProcessor(rawInput: ByteReadPacket): Job =
launch(CoroutineName("Incoming Packet handler")) {
rawInput.debugPrint("Received").use { input ->
if (input.remaining == 0L) {
bot.logger.error("Empty packet received. Consider if bad packet was sent.")
return@launch
/**
* 单线程处理包的解析 (协程挂起效率够)
*/
@Suppress("PrivatePropertyName")
private val PacketProcessDispatcher = newCoroutineDispatcher(1)
/**
* 缓存的包
*/
private var cachedPacket: ByteReadPacket? = null
/**
* 缓存的包还差多少长度
*/
private var expectingRemainingLength: Long = 0
/**
* 在 [PacketProcessDispatcher] 调度器中解析包内容.
* [input] 将会被 [ObjectPool.recycle].
*
* @param input 一个完整的包的内容, 去掉开头的 int 包长度
*/
fun parsePacketAsync(input: IoBuffer, pool: ObjectPool<IoBuffer> = IoBuffer.Pool): Job =
this.launch(PacketProcessDispatcher) {
try {
parsePacket(input)
} finally {
input.discard()
input.release(pool)
}
}
/**
* 在 [PacketProcessDispatcher] 调度器中解析包内容.
* [input] 将会被 [Input.close], 因此 [input] 不能为 [IoBuffer]
*
* @param input 一个完整的包的内容, 去掉开头的 int 包长度
*/
fun parsePacketAsync(input: Input): Job {
require(input !is IoBuffer) { "input cannot be IoBuffer" }
return this.launch(PacketProcessDispatcher) {
input.use { parsePacket(it) }
}
}
/**
* 解析包内容
* **注意**: 需要函数调用者 close 这个 [input]
*
* @param input 一个完整的包的内容, 去掉开头的 int 包长度
*/
suspend fun parsePacket(input: Input) {
KnownPacketFactories.parseIncomingPacket(bot, input) { packet: Packet, packetId: PacketId, sequenceId: Int ->
if (PacketReceivedEvent(packet).broadcast().cancelled) {
return@parseIncomingPacket
}
packetListeners.forEach { listener ->
if (listener.filter(packetId, sequenceId) && packetListeners.remove(listener)) {
listener.complete(packet)
}
val fixedInput = if (lastPacket == null) {
input
} else {
ByteReadPacket((lastPacket ?: ByteArray(0)) + input.readBytes(input.remaining.toInt()))
}
}
}
/**
* 处理从服务器接收过来的包. 这些包可能是粘在一起的, 也可能是不完整的. 将会自动处理
*/
@UseExperimental(ExperimentalCoroutinesApi::class)
internal suspend fun processPacket(rawInput: ByteReadPacket): Unit = rawInput.debugPrint("Received").let { input: ByteReadPacket ->
if (input.remaining == 0L) {
return
}
if (cachedPacket == null) {
// 没有缓存
var length: Int = input.readInt() - 4
if (input.remaining == length.toLong()) {
// 捷径: 当包长度正好, 直接传递剩余数据.
parsePacketAsync(input)
return
}
// 循环所有完整的包
while (input.remaining > length) {
parsePacketAsync(input.readIoBuffer(length))
length = input.readInt() - 4
}
if (input.remaining != 0L) {
// 剩余的包长度不够, 缓存后接收下一个包
expectingRemainingLength = length - input.remaining
cachedPacket = input
} else {
cachedPacket = null // 表示包长度正好
}
} else {
// 有缓存
if (input.remaining >= expectingRemainingLength) {
// 剩余长度够, 连接上去, 处理这个包.
parsePacketAsync(buildPacket {
writePacket(cachedPacket!!)
writePacket(input, expectingRemainingLength)
})
cachedPacket = null // 缺少的长度已经给上了.
if (input.remaining != 0L) {
processPacket(input) // 继续处理剩下内容
}
while (true) {
val pk1Length = fixedInput.readInt() - 4
if (pk1Length > fixedInput.remaining) {
lastPacket = pk1Length.toByteArray().plus(fixedInput.readBytes(fixedInput.remaining.toInt()))
break
}
KnownPacketFactories.parseIncomingPacket(
bot,
fixedInput.readBytes(pk1Length).toReadPacket()
) { packet: Packet, packetId: PacketId, sequenceId: Int ->
if (PacketReceivedEvent(packet).broadcast().cancelled) {
return@parseIncomingPacket
}
packetListeners.forEach { listener ->
if (listener.filter(packetId, sequenceId) && packetListeners.remove(listener)) {
listener.complete(packet)
}
}
}
} else {
// 剩余不够, 连接上去
expectingRemainingLength -= input.remaining
cachedPacket = buildPacket {
writePacket(cachedPacket!!)
writePacket(input)
}
}
}
if (input.remaining == 0L) {
bot.logger.error("Empty packet received. Consider if bad packet was sent.")
return
}
}
@UseExperimental(ExperimentalCoroutinesApi::class)
private suspend fun processReceive() {
while (channel.isOpen) {
val rawInput = try {
......@@ -100,7 +190,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
bot.logger.error("Caught unexpected exceptions", e)
continue
}
launchPacketProcessor(rawInput)
launch(context = PacketReceiveDispatcher + CoroutineName("Incoming Packet handler"), start = CoroutineStart.ATOMIC) {
processPacket(rawInput)
}
}
}
......
......@@ -62,16 +62,13 @@ internal object KnownPacketFactories : List<PacketFactory<*>> by mutableListOf(
// 00 00 08 E0 00 00 00 0A 01 00 00 00 00 0E 31 39 39 34 37 30 31 30 32 31 B4 16 A6 D7 A3 E4 9E 53 99 CD 77 14 70 1F 51 3E 8B 79 F6 93 2B E0 92 E4 32 E2 87 6C 3A 9C 1B 29 87 CB 3C 60 45 9C 41 71 63 6A F6 99 FC 05 01 68 86 B3 6F 37 97 52 C5 D3 0E 66 B3 F6 40 CC EB 18 A3 AE 15 3E 31 B1 E9 7C 6F EC E4 4D 31 F1 1E 2C 0C 1C 45 66 CD F7 1B 90 11 9A D8 CE DD 6D 6C 63 9F EB CD 69 33 AF 6C 8E BA 8C CB C3 FF 27 A2 A6 C3 28 06 4A B5 79 79 12 AB 52 04 62 CA 7D 11 59 85 5C 0B D6 8D 2A E7 9C 04 97 62 7D 05 11 3E 2C 11 60 E3 E3 B3 DA 7A 7C 13 AF 22 01 53 80 69 D0 F9 C8 86 EC 25 8C F3 67 5C 82 45 08 FB 34 43 50 01 0E EA 43 77 D8 CF EC 55 E6 4E 66 5B 26 21 C9 E8 78 92 AE 5C 61 F0 5E 0B E7 34 1F 53 D6 EA 28 9C 02 1A E9 F0 55 61 4B 06 F8 56 3B AC 93 B2 2C CD 66 0D D1 18 CB BD 29 50 DE 0F 82 6D 28 63 AB 21 E1 6C BA B1 9F 69 A4 E3 C9 20 F8 11 82 39 04 2B 54 44 50 FA 2E 86 68 6D DC 5D 9E 18 F4 DD 19 09 BC CF E8 41 68 A3 8D 86 42 80 51 C4 C1 ED 54 DB 50 F5 1D A7 28 2A 0D E8 14 1A 4E F7 96 29 00 6C 9D 4A 2E 3E 7B 4C AC 20 78 F1 3C 70 6B 61 96 D7 EC 77 AD CB AD AF BB 47 C3 1F A0 6C 6C 9C 9F F3 6C EB 6C A4 D0 7F 2B E1 AA 68 26 99 B9 C8 A1 F5 C4 7E E7 E7 81 EE 66 00 96 33 49 C0 EE A2 F9 F6 52 C5 A6 5D EE 9D C5 E5 CE DA 31 FC FF 4B 02 97 68 3D 6A 99 4A CF 69 D9 F4 53 68 31 E7 32 2F 85 E7 7F 16 82 AE FA 73 D5 42 09 9C CB 53 26 79 41 63 80 B0 E2 6A 8B B9 C6 71 08 B4 2B E0 48 D3 C4 0F B0 00 D0 FA 8C 29 DE E9 71 6A D7 89 76 E7 5D 33 14 10 6F E2 44 6A A0 DC C1 CB F3 9A C3 13 CB D1 82 2C DF 34 68 79 E3 09 BD CC 2B 25 79 A8 E7 BE 29 6C 97 C3 D7 F4 0E CC 2B 74 71 02 BA 2B 5B 57 1B C2 C8 C2 BF 54 23 72 EA E4 38 54 20 7D 88 E4 39 7C C5 8A 1B C0 EC D2 1E 7D 1B 6B 7A BC EC 73 1E 53 4A 6F 4F EA F0 56 12 80 BD 0B 37 67 BD FD A8 29 23 2D 8E 66 7E 31 A9 F6 CE 7E BC 4F 38 D0 33 D4 C7 4A E9 43 9D 28 2E 8F 7C D5 81 F4 8C F9 6F 21 AC A1 08 FD F4 01 FB E8 CE 61 91 BE 68 5B E4 3A 5F F8 FB DA 5D 9B 2A AF E2 0C D3 A4 1F 42 90 96 E1 28 44 85 8D E1 CF 19 A9 47 04 8D 28 D9 B3 35 79 48 70 D9 ED 45 B6 24 B5 56 FA 1E DE 02 F3 EB 69 08 7D 24 9C 60 35 97 8D 13 4A 5A 57 BA B3 14 C1 EE 70 22 CA B2 65 F7 BB 3F A2 D9 14 AA 4C 52 E6 E4 10 D3 FD C6 2B DD BF C0 CF E5 35 57 9E 9F D0 77 C8 E6 EF 2B 8E 01 88 96 F8 68 95 A7 0D 58 81 30 60 88 44 CC 31 5B C1 D4 92 6E ED 17 CA 0A 01 69 90 4E 6A C0 D7 09 6C E5 33 64 CA 6E 5C 07 C3 AD 46 36 F9 DF DE B7 71 B2 87 CB 3D 76 C0 44 B8 6B 15 27 B2 03 99 C7 51 8A 00 35 C9 1C 76 55 32 AE 49 5A 34 6A 4E FD 20 7A 24 BF 34 E8 B4 18 BC 92 64 A1 F3 0A 2E 7B 00 EA B6 52 E7 AC 34 FD AE FF 1E 5D 6D D6 1F 6D 06 31 09 9D A9 9C 86 DB 5E 05 07 BA 4A 49 2B D2 7F EE 88 64 B2 6F 15 70 39 1B E9 57 6A 4E 29 4A A4 57 EA 80 3D 86 4C E9 F7 F5 2B C4 9F 35 62 76 09 0E 1C A4 99 50 99 82 2F 84 90 0E 9E 9F 75 C3 15 B0 61 34 D1 67 2D 30 16 FE D3 BF 59 6A B1 74 02 C4 EF 92 85 E0 16 4B 0C C5 9D 65 BB 5D 52 8F 52 5B 7C 7B 74 D9 EC 41 A9 5B FA 2D 95 D4 AE 5D F1 68 88 F6 82 ED 09 05 21 2E 5D 93 64 A0 96 15 64 A6 50 3C 03 2B FC 3E 80 89 90 62 CC D9 23 8E D7 BD 05 02 30 86 32 31 6A 5F F8 C4 BD 61 D0 CE B9 54 4E 93 E9 AE B9 4F 2B 98 DC 23 31 CC A8 06 89 A8 08 60 99 DC D4 81 98 13 C9 27 36 32 24 C1 B0 6B F0 3D EB CC 3B 32 5F 20 72 23 B3 DF 0B 48 3C 35 FD F1 FB DC 3E 2A BE B9 0F 42 56 F1 39 94 86 85 C6 1E A0 4C EC B8 69 45 5F 3D AB 3C 3B A2 70 61 91 9D 2C DD 6D C5 E9 EF 47 36 A6 A3 E0 96 C2 B8 EF 92 E9 E0 26 88 C6 B5 51 BA DE FD C5 BA 4C 6A 9A FE 6F DE B8 10 05 7F 9C 5D 40 11 39 75 CD 36 4F 6B A8 A1 94 57 5F 8F F2 D0 E2 36 A0 A4 24 05 FD 9E F5 51 93 C9 6E 5A 10 8D C3 33 2D E5 09 7A E0 DB 44 63 9C EA A5 ED BF 0B 98 32 F1 BA 04 96 F6 14 49 F1 F8 58 EA 6E 5E 5E 49 CA 2D E2 93 E6 AD 20 B2 CD 98 A7 3E BA 3E A8
/**
* full packet without length
*/
// do not inline. Exceptions thrown will not be reported correctly
suspend fun parseIncomingPacket(bot: QQAndroidBot, rawInput: ByteReadPacket, consumer: PacketConsumer) =
suspend fun parseIncomingPacket(bot: QQAndroidBot, rawInput: Input, consumer: PacketConsumer) =
rawInput.debugIfFail("Incoming packet") {
require(remaining < Int.MAX_VALUE) { "rawInput is too long" }
val expectedLength = readUInt().toInt() - 4
if (expectedLength > 16e7) {
bot.logger.warning("Detect incomplete packet, ignoring.")
return@debugIfFail
}
check(remaining.toInt() == expectedLength) { "Invalid packet length. Expected $expectedLength, got ${this.remaining} Probably packets merged? " }
// login
val flag1 = readInt()
......@@ -88,12 +85,12 @@ internal object KnownPacketFactories : List<PacketFactory<*>> by mutableListOf(
//debugPrint("remaining")
when (flag1) {
0x0A -> parseLoginSsoPacket(bot, decryptBy(DECRYPTER_16_ZERO), consumer)
0x0A -> parseLoginSsoPacket(bot, if (flag2 == 2) decryptBy(DECRYPTER_16_ZERO) else decryptBy(bot.client.wLoginSigInfo.d2Key), consumer)
0x0B -> parseUniPacket(bot, decryptBy(DECRYPTER_16_ZERO), consumer)
}
}
private suspend fun parseUniPacket(bot: QQAndroidBot, rawInput: ByteReadPacket, consumer: PacketConsumer) =
private fun parseUniPacket(bot: QQAndroidBot, rawInput: ByteReadPacket, consumer: PacketConsumer) =
rawInput.debugIfFail("Login sso packet") {
readIoBuffer(readInt() - 4).withUse {
//00 01 4E 64 FF FF D8 E8 00 00 00 14 6E 65 65 64 20 41 32 20 61 6E 64 20 49 4D 45 49 00 00 00 04 00 00 00 08 60 7F B6 23 00 00 00 00 00 00 00 04
......
......@@ -68,8 +68,12 @@ private val shareKeyCalculatedByConstPubKey = ECDH.calculateShareKey(
)
fun main() {
val data = """
20da22db750806141ef448110800450001b45126400080060000c0a8030a71600dd0fe501f908b8c585508ceeec6501801fc448900000000018c0000000a0100000044e0e22a59327abb9ce80cf63be86694210344fab2f2b065d7785a32cacfa4075cd509f49cb37a075ad0685cbd472344bfdef1ede611c0ad81129be9e2d7e476d2000000000e31393934373031303231f00754574170e9a5d13ca9426984103d1813e22d9c2147f8da5a0af64f64e4ebffde9ce8bfda6c1f798364a1de449adc701595fdce57fa4643e5e14eb4444ad0aea85261a6d5ee90c42a29b5af461a971a7fe85ecf1ed0d582d1cce60d1c34f6a3dc74a75d2f525d590da954098a1a7a4973773195dd209e662cb5c1e0db69843aa75425993bdc8876f21fd9c875d25fa47b689ebc302f3087de8e4a9862cff283703237602e50fa6bb281e974315b70f04f436d8ae9c5d67e22af3495e64aada419940e63c8d0ddbb066fa3d2b8cb27209a687d782d4ec32e6505593625bf0de257d59332f779ed6edb849502591e2c37b983974dbc3ec4740dfd0a9244baf958219067ca7d435ec8bde8e57b36f04d31622a86cb9c8d59d81074c9af38d57f42bb331a0a15f9ac5e216ae54abe8f8a
20da22db750806141ef448110800450000d4512f400080060000c0a8030a71600dd0fe501f908b8c5c9908cf2416501801ff43a90000000000ac0000000b0100014f0d000000000e3139393437303130323193c94c8ce2871f6d5f6664df9e9231dedce5c7cb914cf5d616cf64af478aea9f210098e7f5efae9a952742b8fff704681885e5cc14a44e40d88910258f4a4a5cfcadd642cc159fdc475478475b18ba225cfad1c8bc2c5c828a9cc3ebaed1aa040d04b94f577fcfdb0861fd19754622733a242c5bcb37ca8597a2935d9910162111e44839b6787bb9be80058ad9c921c2
""".trimIndent()
.trim().split("\n").map {
val bytes = it.trim().autoHexToBytes()
......
......@@ -3,7 +3,8 @@
package test
import net.mamoe.mirai.utils.cryptor.protoFieldNumber
import net.mamoe.mirai.utils.cryptor.protoType
intArrayOf(10, 18, 26, 34, 42, 50, 58, 66, 74).forEach {
println(protoFieldNumber(it.toUInt()))
intArrayOf(8, 18, 26, 34, 80).forEach {
println(protoFieldNumber(it.toUInt()).toString() + " -> " + protoType(it.toUInt()))
}
\ No newline at end of file
......@@ -3,12 +3,15 @@ package net.mamoe.mirai.utils
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.io.ByteArrayOutputStream
import java.io.DataInput
import java.io.EOFException
import java.io.InputStream
import java.net.InetAddress
import java.security.MessageDigest
import java.util.concurrent.Executors
import java.util.zip.CRC32
import java.util.zip.Inflater
......@@ -91,4 +94,8 @@ actual fun ByteArray.unzip(): ByteArray {
}
inflater.end()
return output.toByteArray()
}
actual fun newCoroutineDispatcher(threadCount: Int): CoroutineDispatcher {
return Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ package net.mamoe.mirai.utils
import io.ktor.client.HttpClient
import io.ktor.util.date.GMTDate
import kotlinx.coroutines.CoroutineDispatcher
/**
* 时间戳
......@@ -46,3 +47,5 @@ expect fun localIpAddress(): String
* Ktor HttpClient. 不同平台使用不同引擎.
*/
expect val Http: HttpClient
expect fun newCoroutineDispatcher(threadCount: Int): CoroutineDispatcher
\ No newline at end of file
......@@ -2,7 +2,10 @@ package net.mamoe.mirai.utils.io
import kotlinx.io.core.*
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.MiraiDebugAPI
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.withSwitch
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
......@@ -74,7 +77,7 @@ inline fun <R> Input.debugIfFail(name: String = "", onFail: (ByteArray) -> ByteR
ByteArrayPool.useInstance {
val count = this.readAvailable(it)
try {
return block(it.toReadPacket(0, count))
return it.toReadPacket(0, count).use(block)
} catch (e: Throwable) {
onFail(it.take(count).toByteArray()).readAvailable(it)
DebugLogger.debug("Error in ByteReadPacket $name=" + it.toUHexString(offset = 0, length = count))
......
......@@ -4,15 +4,15 @@ package net.mamoe.mirai.utils
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import kotlinx.io.core.IoBuffer
import kotlinx.io.core.Output
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.io.core.copyTo
import kotlinx.io.core.readBytes
import kotlinx.io.streams.asInput
import kotlinx.io.streams.asOutput
import java.io.*
import java.net.InetAddress
import java.security.MessageDigest
import java.util.concurrent.Executors
import java.util.zip.CRC32
import java.util.zip.Inflater
......@@ -68,4 +68,8 @@ actual fun ByteArray.unzip(): ByteArray {
}
inflater.end()
return output.toByteArray()
}
actual fun newCoroutineDispatcher(threadCount: Int): CoroutineDispatcher {
return Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
}
\ No newline at end of file
......@@ -2,15 +2,17 @@
package test
import kotlinx.serialization.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoBuf
import kotlinx.serialization.protobuf.ProtoNumberType
import kotlinx.serialization.protobuf.ProtoType
import kotlinx.serialization.serializer
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.cryptor.readProtoMap
import net.mamoe.mirai.utils.io.hexToBytes
import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.toUHexString
import kotlin.reflect.KClass
@Serializable
......@@ -25,15 +27,14 @@ data class ProtoTest(
@UseExperimental(MiraiInternalAPI::class)
suspend fun main() {
println("PNG".toUtf8Bytes().toUHexString())
deserializeTest()
}
suspend fun deserializeTest() {
val bytes =
"""
""".trimIndent()
08 02 1A 55 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 1A 25 2F 34 35 35 38 66 39 30 38 2D 37 62 39 61 2D 34 65 32 66 2D 38 63 36 39 2D 34 61 35 32 61 66 62 33 36 35 61 37 20 01 30 04 38 05 40 09 48 01 58 00 60 01 6A 0A 38 2E 32 2E 30 2E 31 32 39 36 70 E0 8C B2 F0 05 78 01 50 03
""".trimIndent()
.replace("\n", " ")
.replace("UVarInt", "", ignoreCase = true)
.replace("uint", "", ignoreCase = true)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment