Commit b2542cd6 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/protocol/packet/chat/data/MsgSvc.kt
#	mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.PushNotify.kt
#	mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt
#	mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/clientToServer.kt
parents 01c478e4 a79032e8
......@@ -5,27 +5,30 @@
[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
**[English](README-eng.md)**
**TIM PC 和 QQ Android 协议** 跨平台 QQ 协议支持库.
**纯 Kotlin 实现协议和支持框架. 目前可运行在 JVM 或 Android.**
跨平台 **TIM PC 和 QQ Android** 协议支持库.
纯 Kotlin 实现协议和支持框架,模块全部开源。
目前可运行在 JVM 或 Android。
**一切开发旨在学习,请勿用于非法用途**
您可在 Gitter 提问, 或加入 QQ 群: 655057127
加入 Gitter, 或加入 QQ 群: 655057127
## Update log
[Project](https://github.com/mamoe/mirai/projects/1) 查看已支持功能和计划
[UpdateLog](https://github.com/mamoe/mirai/blob/master/UpdateLog.md) 查看版本更新记录
[Project](https://github.com/mamoe/mirai/projects/1) 查看已支持功能和计划(更新不及时)
[UpdateLog](https://github.com/mamoe/mirai/blob/master/UpdateLog.md) 查看版本更新记录(准确更新发布的版本)
## Features
## Modules
#### mirai-core
通用 API 模块,请参考此模块调用 Mirai.
通用 API 模块,一套 API 适配两套协议。
**请参考此模块的 API**
#### mirai-core-timpc
TIM PC (2.3.2 版本,2019 年 8 月)协议的实现,相较于 core,仅新增少量 API. 详见 [README.md](mirai-core-timpc/)
支持的功能:
- 消息收发:图片文字复合消息,图片消息
- 群管功能:群员列表,禁言
(目前不再更新,请关注安卓协议)
(目前不再更新此协议,请关注下文的安卓协议)
#### mirai-core-qqandroid
QQ for Android (8.2.0 版本,2019 年 12 月)协议的实现,目前还未完成。
......@@ -42,25 +45,31 @@ QQ for Android (8.2.0 版本,2019 年 12 月)协议的实现,目前还
- 进行中 图片上传和下载
## Use directly
**直接使用Mirai(终端环境/网页面板(将来)).**
[Mirai-Console](https://github.com/mamoe/mirai/tree/master/mirai-console) 插件支持, 在终端中启动Mirai并获得机器人服务
**直接使用 Mirai(终端环境/网页面板(将来)).**
[Mirai-Console](https://github.com/mamoe/mirai/tree/master/mirai-console) 插件支持, 在终端中启动 Mirai 并获得机器人服务
本模块还未完善。
## Use as a library
**mirai-core 为独立设计, 可以作为库内置于您的任意 Java/Android 项目中使用.**
Mirai 只上传在 `jcenter`, 因此请确保在 `build.gradle` 添加 `jcenter()` 仓库
**mirai-core 为独立设计, 可以作为库内置于任意 Java(JVM)/Android 项目中使用.**
### Gradle
Mirai 只发布在 `jcenter`, 因此请确保在 `build.gradle` 添加 `jcenter()` 仓库:
```kotlin
repositories{
jcenter()
}
```
若您需要使用在跨平台项目, 您需要对各个目标平台添加不同的依赖.
**若您只需要使用在单一平台, 则只需要添加一项该平台的依赖. 如只在JVM运行则只需要`-jvm`的依赖**
若您需要使用在跨平台项目, 则要对各个目标平台添加不同的依赖,这与 kotlin 相关跨平台库的依赖是类似的。
**若您只需要使用在单一平台, 则只需要添加一项该平台的依赖. 如只在 JVM 运行则只需要`-jvm`的依赖**
您需要`VERSION` 替换为最新的版本(如 `0.10.6`):
`VERSION` 替换为最新的版本(如 `0.10.6`):
[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
**Mirai 目前还处于实验性阶段, 我们无法保证任何稳定性, API 也可能会随时修改.**
现在 Mirai 只支持 TIM PC 协议. QQ Android 协议正在开发中.
**注意:**
Mirai 核心由 API 模块(`mirai-core`)和协议模块组成。
只添加 API 模块将无法正常工作。
现在只推荐使用 TIMPC 协议,请参照下文选择对应目标平台的依赖添加。
**common**
```kotlin
......@@ -90,7 +99,7 @@ JVM 上需 120M-150M 内存
您的 star 是对我们最大的鼓励(点击项目右上角);
## Wiki
[Wiki](https://github.com/mamoe/mirai/wiki/Development-Guide---Kotlin) 中查看各类帮助,如 API 示例
[Wiki](https://github.com/mamoe/mirai/wiki/Development-Guide---Kotlin) 中查看各类帮助,**如 API 示例**(可能过时,待 QQ Android 协议完成后会重写)
## Try
......@@ -110,6 +119,8 @@ bot.subscribeAlways<MemberPermissionChangedEvent> {
}
```
我们也考虑到了 Java 兼容的问题,这正在计划中,但不是高优先的。
1. Clone
2. Import as Gradle project
3. 运行 Demo 程序: [mirai-demo](#mirai-demo) 示例和演示程序
......@@ -119,6 +130,7 @@ bot.subscribeAlways<MemberPermissionChangedEvent> {
- Kotlin 1.3.61
- JDK 8 (required)
- JDK 11(for protocol tools, optional)
- Android SDK 29 (for Android target, optional)
#### Libraries used
......@@ -144,5 +156,5 @@ bot.subscribeAlways<MemberPermissionChangedEvent> {
- (见 LICENSE 第 4 节) 您可以免费或收费地传递这个项目的源代码或目标代码(即编译结果), **但前提是提供明显的版权声明** (您需要标注本 `GitHub` 项目地址)
## Acknowledgement
特别感谢 [JetBrains](https://www.jetbrains.com/?from=mirai) 提供的免费 [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai) 等 IDE 授权
特别感谢 [JetBrains](https://www.jetbrains.com/?from=mirai) 为开源项目提供免费的 [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai) 等 IDE 的授权
[<img src=".github/jetbrains-variant-3.png" width="200"/>](https://www.jetbrains.com/?from=mirai)
......@@ -8,14 +8,15 @@ import net.mamoe.mirai.data.ImageLink
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.ImageIdQQA
import net.mamoe.mirai.qqandroid.utils.Context
import net.mamoe.mirai.qqandroid.utils.ImageIdQQA
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.LockFreeLinkedList
import net.mamoe.mirai.utils.MiraiInternalAPI
import kotlin.coroutines.CoroutineContext
internal expect class QQAndroidBot(
@UseExperimental(MiraiInternalAPI::class)
internal expect class QQAndroidBot constructor(
context: Context,
account: BotAccount,
configuration: BotConfiguration
......@@ -28,7 +29,7 @@ internal abstract class QQAndroidBotBase constructor(
configuration: BotConfiguration
) : BotImpl<QQAndroidBotNetworkHandler>(account, configuration) {
val client: QQAndroidClient = QQAndroidClient(context, account, bot = @Suppress("LeakingThis") this as QQAndroidBot)
override val uin: Long get() = client.uin
override val qqs: ContactList<QQ> = ContactList(LockFreeLinkedList())
override fun getQQ(id: Long): QQ {
......
package net.mamoe.mirai.qqandroid.io
import kotlinx.io.core.BytePacketBuilder
import kotlinx.io.core.Input
import kotlinx.io.core.readBytes
import kotlinx.io.core.writeFully
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
/**
* 仅有标示作用
*/
interface ProtoBuf
fun <T : ProtoBuf> BytePacketBuilder.writeProtoBuf(serializer: SerializationStrategy<T>, v: T) {
this.writeFully(v.toByteArray(serializer))
}
/**
* dump
*/
fun <T : ProtoBuf> T.toByteArray(serializer: SerializationStrategy<T>): ByteArray {
return kotlinx.serialization.protobuf.ProtoBuf.dump(serializer, this)
}
/**
* load
*/
fun <T : ProtoBuf> ByteArray.loadAs(deserializer: DeserializationStrategy<T>): T {
return kotlinx.serialization.protobuf.ProtoBuf.load(deserializer, this)
}
/**
* load
*/
fun <T : ProtoBuf> Input.readRemainingAsProtoBuf(serializer: DeserializationStrategy<T>): T {
return kotlinx.serialization.protobuf.ProtoBuf.load(serializer, this.readBytes())
}
\ No newline at end of file
......@@ -8,7 +8,7 @@ import kotlinx.serialization.modules.EmptyModule
import kotlinx.serialization.modules.SerialModule
import net.mamoe.mirai.qqandroid.io.JceStruct
import net.mamoe.mirai.qqandroid.network.protocol.packet.withUse
import net.mamoe.mirai.qqandroid.network.protocol.protobuf.ProtoBuf
import net.mamoe.mirai.qqandroid.io.ProtoBuf
import net.mamoe.mirai.utils.io.readIoBuffer
import net.mamoe.mirai.utils.io.readString
import net.mamoe.mirai.utils.io.toIoBuffer
......
......@@ -5,6 +5,7 @@ import kotlinx.atomicfu.atomic
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.toByteArray
import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.RawAccountIdUse
import net.mamoe.mirai.data.OnlineStatus
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketLogger
......@@ -32,7 +33,7 @@ import net.mamoe.mirai.utils.unsafeWeakRef
DOMAINS
Pskey: "openmobile.qq.com"
*/
@UseExperimental(MiraiExperimentalAPI::class, MiraiInternalAPI::class)
@PublishedApi
internal open class QQAndroidClient(
context: Context,
......@@ -53,7 +54,7 @@ internal open class QQAndroidClient(
"tgtgtKey" to tgtgtKey,
"tgtKey" to wLoginSigInfo.tgtKey,
"deviceToken" to wLoginSigInfo.deviceToken,
"shareKeyCalculatedByConstPubKey" to ecdh.keyPair.shareKey
"shareKeyCalculatedByConstPubKey" to ecdh.keyPair.initialShareKey
//"t108" to wLoginSigInfo.t1,
//"t10c" to t10c,
//"t163" to t163
......@@ -69,7 +70,6 @@ internal open class QQAndroidClient(
return null
}
@UseExperimental(MiraiInternalAPI::class)
override fun toString(): String { // net.mamoe.mirai.utils.cryptor.ProtoKt.contentToString
return "QQAndroidClient(account=$account, ecdh=$ecdh, device=$device, tgtgtKey=${tgtgtKey.contentToString()}, randomKey=${randomKey.contentToString()}, miscBitMap=$miscBitMap, mainSigMap=$mainSigMap, subSigMap=$subSigMap, openAppId=$openAppId, apkVersionName=${apkVersionName.contentToString()}, loginState=$loginState, appClientVersion=$appClientVersion, networkType=$networkType, apkSignatureMd5=${apkSignatureMd5.contentToString()}, protocolVersion=$protocolVersion, apkId=${apkId.contentToString()}, t150=${t150?.contentToString()}, rollbackSig=${rollbackSig?.contentToString()}, ipFromT149=${ipFromT149?.contentToString()}, timeDifference=$timeDifference, uin=$uin, t530=${t530?.contentToString()}, t528=${t528?.contentToString()}, ksid='$ksid', pwdFlag=$pwdFlag, loginExtraData=$loginExtraData, wFastLoginInfo=$wFastLoginInfo, reserveUinInfo=$reserveUinInfo, wLoginSigInfo=$wLoginSigInfo, tlv113=${tlv113?.contentToString()}, qrPushSig=${qrPushSig.contentToString()}, mainDisplayName='$mainDisplayName')"
}
......@@ -95,7 +95,6 @@ internal open class QQAndroidClient(
val apkVersionName: ByteArray = "8.2.0".toByteArray()
var loginState = 0
val appClientVersion: Int = 0
......@@ -108,14 +107,14 @@ internal open class QQAndroidClient(
*/
val protocolVersion: Short = 8001
@Suppress("SpellCheckingInspection")
@PublishedApi
internal val apkId: ByteArray = "com.tencent.mobileqq".toByteArray()
/*
* 以下登录使用
*/
@Suppress("SpellCheckingInspection")
@PublishedApi
internal val apkId: ByteArray = "com.tencent.mobileqq".toByteArray()
var loginState = 0
var t150: Tlv? = null
var rollbackSig: ByteArray? = null
......@@ -129,8 +128,12 @@ internal open class QQAndroidClient(
*
* **注意**: 总是使用这个属性, 而不要使用 [BotAccount.id]. 将来它可能会变为 [String]
*/
@UseExperimental(MiraiExperimentalAPI::class, MiraiInternalAPI::class)
var uin: Long = bot.account.id
val uin: Long get() = _uin
@UseExperimental(RawAccountIdUse::class)
@Suppress("PropertyName")
internal var _uin: Long = bot.account.id
var t530: ByteArray? = null
var t528: ByteArray? = null
/**
......
......@@ -95,6 +95,6 @@ internal interface EncryptMethodECDH : EncryptMethod {
})
// encryptAndWrite("26 33 BA EC 86 EB 79 E6 BC E0 20 06 5E A9 56 6C".hexToBytes(), body)
encryptAndWrite(ecdh.keyPair.shareKey, body)
encryptAndWrite(ecdh.keyPair.initialShareKey, body)
}
}
\ No newline at end of file
......@@ -7,8 +7,6 @@ import kotlinx.io.core.buildPacket
import kotlinx.io.core.writeFully
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.cryptor.DecrypterByteArray
import net.mamoe.mirai.utils.cryptor.encryptAndWrite
import net.mamoe.mirai.utils.io.encryptAndWrite
import net.mamoe.mirai.utils.io.writeHex
import net.mamoe.mirai.utils.io.writeIntLVPacket
......@@ -26,7 +24,7 @@ internal class OutgoingPacket constructor(
val delegate: ByteReadPacket
) {
val name: String by lazy {
name ?: commandName.toString()
name ?: commandName
}
}
......@@ -50,22 +48,13 @@ internal val EMPTY_BYTE_ARRAY = ByteArray(0)
*
* byte[] body encrypted by 16 zero
*/
/*
* 00 00 02 34 // remaining.length + 4
* 00 00 00 0B
* 01
* 00 01 4E 73 // sequence
* 00
* 00 00 00 0E
* 31 39 39 34 37 30 31 30 32 31
* 18 5D 8F 17 7D 67 71 61 FE DB 30 A4 4D 16 DD 0E 8D 84 0A F2 44 BE FB BB 11 BB B4 AC 79 50 50 9F 4C 99 CC 77 0B AA B6 E0 06 0C F7 91 79 99 57 31 3D EF 38 92 2C C8 81 33 79 83 FF C6 2F BA 18 2A 33 F8 D9 4E CD 62 07 D8 08 B7 1A 1E C7 EB AC AB B4 1E C9 9D A9 15 9C 29 29 2A 99 F6 BB D0 43 65 D6 5E 9C 93 A8 8D 17 08 5B 6A 29 92 58 6A 75 C9 B5 45 B3 0E A5 D3 52 8F 9A A4 88 36 A0 14 3A 21 F2 46 C3 91 66 A3 73 67 6A 3E F7 9D 8E 44 52 87 7B 8A C7 1B E2 D3 98 62 E8 25 30 2A 43 5C 5A B2 C6 45 F5 39 EC 85 81 BF 7D 22 4C E8 01 87 92 48 38 06 6B A0 83 70 0B 51 ED CF 7A FF E2 F2 06 3E A7 95 4E E5 29 23 32 1C FE 79 C6 08 C5 7A 39 B9 AF CD 4F 80 3E 5D 74 4D 0B E1 10 33 8D F0 54 8E 0E 22 96 B4 06 7F 29 01 1E CA 30 35 FD 8A 2E 51 04 20 79 7B 08 DC DF F6 64 21 6B C5 95 34 B3 40 D2 E8 CE BB DC 69 89 75 62 A6 0B 4A 49 9D 90 BA 68 2B BD 8A 50 2D 68 6B 56 40 0C 39 F2 08 20 1B EB A4 A5 20 1D 1F 7E FA 4B B8 2E 58 79 2A 16 54 26 6C C8 44 6C 4F 64 2D 5C 0C 47 2E 90 13 A9 D7 33 4A 51 17 6E 3F 3E 48 AE 39 D8 45 05 2C 0C 3C 9F 92 39 DB 62 B3 BB 64 EE 7E 91 C5 84 92 10 96 D9 F1 13 02 94 00 EA DA 87 7C 85 7B 68 BA 8D A1 AB F5 CD 9C EB 4C CD A0 38 78 43 80 DD E5 1D 28 25 1F F0 25 EF 0D 95 91 0F 21 5D 41 06 00 03 48 77 E0 98 09 3E 04 5A B0 93 63 3B AE 8E 49 0C C2 12 BA DD C3 5A ED FF 68 98 22 C4 5E F6 1E 85 57 15 E8 7E 26 22 E3 70 C2 57 F4 CE 2F CB C4 DC 39 4A 9C FE DE 27 18 D3 36 66 88 92 D7 69 D0 04 8E 93 9B AD E9 2E 5A 2C 91 CD 28 DF BE 62 CF 2C 72 8E FD A9 1F 0E 8E 00 9E 54 28 50 25 0C E7 DC 98 85 C9 B3 59 A8 97 F5 2E 7F 44 4C 43 3C C4 65 E5 AB DB 5B 3C 50 2D 53 B3 EA 74 3C 39 F4 0A 52 31 34 30 F5 E6 82 CD 36 D9
*/
@UseExperimental(MiraiInternalAPI::class)
internal inline fun PacketFactory<*>.buildOutgingPacket(
internal inline fun PacketFactory<*>.buildOutgoingPacket(
client: QQAndroidClient,
bodyType: Byte = 1, // 1: PB?
name: String? = this.commandName,
commandName: String = this.commandName,
key: ByteArray,
key: ByteArray = client.wLoginSigInfo.d2Key,
body: BytePacketBuilder.(sequenceId: Int) -> Unit
): OutgoingPacket {
val sequenceId: Int = client.nextSsoSequenceId()
......@@ -73,7 +62,7 @@ internal inline fun PacketFactory<*>.buildOutgingPacket(
return OutgoingPacket(name, commandName, sequenceId, buildPacket {
writeIntLVPacket(lengthOffset = { it + 4 }) {
writeInt(0x0B)
writeByte(1)
writeByte(bodyType)
writeInt(sequenceId)
writeByte(0)
client.uin.toString().let {
......@@ -87,6 +76,68 @@ internal inline fun PacketFactory<*>.buildOutgingPacket(
})
}
/**
* buildOutgoingPacket 与 writeUniPacket 的 fast-path
*/
@UseExperimental(MiraiInternalAPI::class)
internal inline fun PacketFactory<*>.buildOutgoingUniPacket(
client: QQAndroidClient,
bodyType: Byte = 1, // 1: PB?
name: String? = this.commandName,
commandName: String = this.commandName,
key: ByteArray = client.wLoginSigInfo.d2Key,
extraData: ByteReadPacket = BRP_STUB,
body: BytePacketBuilder.(sequenceId: Int) -> Unit
): OutgoingPacket {
val sequenceId: Int = client.nextSsoSequenceId()
return OutgoingPacket(name, commandName, sequenceId, buildPacket {
writeIntLVPacket(lengthOffset = { it + 4 }) {
writeInt(0x0B)
writeByte(bodyType)
writeInt(sequenceId)
writeByte(0)
client.uin.toString().let {
writeInt(it.length + 4)
writeStringUtf8(it)
}
encryptAndWrite(key) {
writeUniPacket(commandName, extraData) {
body(sequenceId)
}
}
}
})
}
@UseExperimental(MiraiInternalAPI::class)
internal inline fun BytePacketBuilder.writeUniPacket(
commandName: String,
extraData: ByteReadPacket = BRP_STUB,
body: BytePacketBuilder.() -> Unit
) {
writeIntLVPacket(lengthOffset = { it + 4 }) {
commandName.let {
writeInt(it.length + 4)
writeStringUtf8(it)
}
writeInt(4 + 4)
writeInt(45112203) // 02 B0 5B 8B
if (extraData === BRP_STUB) {
writeInt(0x04)
} else {
writeInt((extraData.remaining + 4).toInt())
writePacket(extraData)
}
}
// body
writeIntLVPacket(lengthOffset = { it + 4 }, builder = body)
}
/**
* 最外层的包. 结构适用于登录.
*
......@@ -213,37 +264,6 @@ internal inline fun BytePacketBuilder.writeSsoPacket(
}
/**
* Outermost packet, not for login
*
* **Packet structure**
* int remaining.length + 4
* int 0x0B
* byte 0x01
* byte 0
* int [uinAccount].length + 4
* byte[] uinAccount
*
* byte[] body encrypted by [sessionKey]
*/
internal inline fun PacketFactory<*>.buildSessionOutgoingPacket(
uinAccount: String,
sessionKey: DecrypterByteArray,
body: BytePacketBuilder.() -> Unit
): ByteReadPacket = buildPacket {
writeIntLVPacket(lengthOffset = { it + 4 }) {
writeInt(0x00_00_00_0B)
writeByte(0x01)
writeByte(0)
writeInt(uinAccount.length + 4)
writeStringUtf8(uinAccount)
encryptAndWrite(sessionKey, body)
}
}
/**
* Writes a request packet
* This is the innermost packet structure
......@@ -293,6 +313,8 @@ internal fun BytePacketBuilder.writeOicqRequestPacket(
writeByte(0x03) // tail
// }
}
/*
00 00 01 64
00 00 00 0A
......
......@@ -2,7 +2,7 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.network.protocol.protobuf.ProtoBuf
import net.mamoe.mirai.qqandroid.io.ProtoBuf
@Serializable
internal class Cmd0x352Packet(
......
......@@ -3,7 +3,7 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.qqandroid.network.protocol.protobuf.ProtoBuf
import net.mamoe.mirai.qqandroid.io.ProtoBuf
import net.mamoe.mirai.utils.currentTimeSeconds
interface ImgReq : ProtoBuf
......
@file:Suppress("ArrayInDataClass")
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumberType
import kotlinx.serialization.protobuf.ProtoType
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.qqandroid.network.protocol.protobuf.ProtoBuf
class MessageCommon {
/**
* 1 -> varint
* 2 -> delimi
* 3 -> varint
* 4 -> varint
* 5 -> varint
* 6 -> varint
* 7 -> delimi
* 8 -> delimi
* 9 -> delimi
* 10 -> delimi
* 11 -> delimi
*/
@Serializable
data class PluginInfo(
@SerialId(1) val resId: Int = 0,
@SerialId(2) val packageName: String = "",
@SerialId(3) val newVer: Int = 0,
@SerialId(4) val resType: Int = 0,
@SerialId(5) val lanType: Int = 0,
@SerialId(6) val priority: Int = 0,
@SerialId(7) val resName: String = "",
@SerialId(8) val resDesc: String = "",
@SerialId(9) val resUrlBig: String = "",
@SerialId(10) val resUrlSmall: String = "",
@SerialId(11) val resConf: String = ""
) : ProtoBuf
@Serializable
data class AppShareInfo(
@ProtoType(ProtoNumberType.FIXED) @SerialId(1) val id: Int = 0,
@SerialId(2) val cookie: ByteArray = EMPTY_BYTE_ARRAY,
@SerialId(3) val resource: PluginInfo = PluginInfo()
) : ProtoBuf
@Serializable
data class ContentHead(
@SerialId(1) val pkgNum: Int = 0,
@SerialId(2) val pkgIndex: Int = 0,
@SerialId(3) val divSeq: Int = 0,
@SerialId(4) val autoReply: Int = 0
) : ProtoBuf
@Serializable
data class Msg(
val s: String
) : ProtoBuf
}
\ No newline at end of file
......@@ -2,8 +2,8 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.io.ProtoBuf
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.qqandroid.network.protocol.protobuf.ProtoBuf
/**
* msf.msgcomm.msg_comm
......@@ -60,7 +60,7 @@ class MsgComm : ProtoBuf {
@SerialId(1) val groupCode: Long = 0L,
@SerialId(2) val groupType: Int = 0,
@SerialId(3) val groupInfoSeq: Long = 0L,
@SerialId(4) val groupCard: ByteArray = EMPTY_BYTE_ARRAY,
@SerialId(4) val groupCard: String = "",
@SerialId(5) val groupRank: ByteArray = EMPTY_BYTE_ARRAY,
@SerialId(6) val groupLevel: Int = 0,
@SerialId(7) val groupCardType: Int = 0,
......@@ -69,9 +69,9 @@ class MsgComm : ProtoBuf {
@Serializable
class Msg(
@SerialId(1) val msgHead: MsgHead? = null,
@SerialId(1) val msgHead: MsgHead,
@SerialId(2) val contentHead: ContentHead? = null,
@SerialId(3) val msgBody: ImMsgBody.MsgBody? = null,
@SerialId(3) val msgBody: ImMsgBody.MsgBody,
@SerialId(4) val appshareInfo: AppShareInfo? = null
) : ProtoBuf
......@@ -145,7 +145,7 @@ class MsgComm : ProtoBuf {
@SerialId(1) val lastReadTime: Int = 0,
@SerialId(2) val peerUin: Long = 0L,
@SerialId(3) val msgCompleted: Int = 0,
@SerialId(4) val msg: List<Msg>? = null,
@SerialId(4) val msg: List<Msg>,
@SerialId(5) val unreadMsgNum: Int = 0,
@SerialId(8) val c2cType: Int = 0,
@SerialId(9) val serviceType: Int = 0,
......
......@@ -2,14 +2,14 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.io.ProtoBuf
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.qqandroid.network.protocol.protobuf.ProtoBuf
@Serializable
class MsgOnlinePush {
@Serializable
data class PbPushMsg(
@SerialId(1) val msg: MsgComm.Msg? = null,
class PbPushMsg(
@SerialId(1) val msg: MsgComm.Msg,
@SerialId(2) val svrip: Int = 0,
@SerialId(3) val pushToken: ByteArray = EMPTY_BYTE_ARRAY,
@SerialId(4) val pingFlag: Int = 0,
......
......@@ -6,8 +6,9 @@ import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.qqandroid.io.JceStruct
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
@Suppress("ArrayInDataClass")
@Serializable
internal class RequestPushNotify(
internal data class RequestPushNotify(
@SerialId(0) val uin: Long = 0L,
@SerialId(1) val ctype: Byte = 0,
@SerialId(2) val strService: String?,
......
......@@ -2,22 +2,25 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.io.JceStruct
import net.mamoe.mirai.qqandroid.io.readRemainingAsProtoBuf
import net.mamoe.mirai.qqandroid.io.serialization.loadAs
import net.mamoe.mirai.qqandroid.io.serialization.readRemainingAsJceStruct
import net.mamoe.mirai.qqandroid.io.writeProtoBuf
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.jce.RequestDataVersion2
import net.mamoe.mirai.qqandroid.network.protocol.jce.RequestPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data.MsgSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data.RequestPushNotify
import net.mamoe.mirai.utils.cryptor.contentToString
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.data.RequestDataVersion2
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.data.RequestPacket
import net.mamoe.mirai.qqandroid.utils.toMessageChain
import net.mamoe.mirai.utils.firstValue
import net.mamoe.mirai.utils.io.debugPrint
import net.mamoe.mirai.utils.io.hexToBytes
import net.mamoe.mirai.utils.io.toReadPacket
class MessageSvc {
......@@ -25,31 +28,64 @@ class MessageSvc {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): RequestPushNotify {
discardExact(8)
@Serializable
class ResponseDataRequestPushNotify(
@SerialId(0) val notify: RequestPushNotify
) : JceStruct
val requestPushNotify = readRemainingAsJceStruct(RequestPacket.serializer()).sBuffer
return readRemainingAsJceStruct(RequestPacket.serializer()).sBuffer
.loadAs(RequestDataVersion2.serializer()).map.firstValue().firstValue()
.toReadPacket().apply { discardExact(1) }
.debugPrint()
.readRemainingAsJceStruct(RequestPushNotify.serializer())
}
println(requestPushNotify.contentToString())
with(bot.network) {
GetMsgRequest(
bot.client,
requestPushNotify
).sendAndExpect<MsgSvc.PbGetMsgResp>()
override suspend fun QQAndroidBot.handle(packet: RequestPushNotify) {
network.run {
PbGetMsg(client, packet).sendAndExpect<MultiPacket<FriendMessage>>()
}
return requestPushNotify
}
}
internal object PbGetMsg : PacketFactory<MultiPacket<FriendMessage>>("MessageSvc.PbGetMsg") {
operator fun invoke(
client: QQAndroidClient,
from: RequestPushNotify
): OutgoingPacket = buildOutgoingUniPacket(
client,
extraData = "08 00 12 33 6D 6F 64 65 6C 3A 78 69 61 6F 6D 69 20 36 3B 6F 73 3A 32 32 3B 76 65 72 73 69 6F 6E 3A 76 32 6D 61 6E 3A 78 69 61 6F 6D 69 73 79 73 3A 4C 4D 59 34 38 5A 18 E4 E1 A4 FF FE 2D 20 E9 E1 A4 FF FE 2D 28 A8 E1 A4 FF FE 2D 30 99 E1 A4 FF FE 2D".hexToBytes().toReadPacket()
) {
writeProtoBuf(
MsgSvc.PbGetMsgReq.serializer(),
MsgSvc.PbGetMsgReq(
msgReqType = from.ctype.toInt(),
contextFlag = 1,
rambleFlag = 0,
latestRambleNumber = 20,
otherRambleNumber = 3,
onlineSyncFlag = 1,
serverBuf = from.serverBuf ?: EMPTY_BYTE_ARRAY
)
)
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): MultiPacket<FriendMessage> {
// 00 00 01 0F 08 00 12 00 1A 34 08 FF C1 C4 F1 05 10 FF C1 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 8A CA 91 D1 0C 48 9B A5 BD 9B 0A 58 DE 9D 99 F8 08 60 1D 68 FF C1 C4 F1 05 70 00 20 02 2A 9D 01 08 F3 C1 C4 F1 05 10 A2 FF 8C F0 03 18 01 22 8A 01 0A 2A 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 A6 01 20 0B 28 AE F9 01 30 F4 C1 C4 F1 05 38 A7 E3 D8 D4 84 80 80 80 01 B8 01 CD B5 01 12 08 08 01 10 00 18 00 20 00 1A 52 0A 50 0A 27 08 00 10 F4 C1 C4 F1 05 18 A7 E3 D8 D4 04 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 08 0A 06 0A 04 4E 4D 53 4C 12 15 AA 02 12 9A 01 0F 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 12 04 4A 02 08 00 30 01 2A 15 08 97 A2 C1 F1 05 10 95 A6 F5 E5 0C 18 01 30 01 40 01 48 81 01 2A 10 08 D3 F7 B5 F1 05 10 DD F1 92 B7 07 18 01 30 01 38 00 42 00 48 00
discardExact(4)
val resp = readRemainingAsProtoBuf(MsgSvc.PbGetMsgResp.serializer())
//println(resp.contentToString())
if (resp.uinPairMsgs == null) {
return MultiPacket(emptyList())
}
return MultiPacket(resp.uinPairMsgs.asSequence().flatMap { it.msg.asSequence() }.mapNotNull {
when (it.msgHead.msgType) {
166 -> {
FriendMessage(
bot,
false, // TODO: 2020/1/29 PREVIOUS??
bot.getQQ(it.msgHead.fromUin),
it.msgBody.richText.toMessageChain()
)
}
else -> null
}
}.toList())
}
}
}
......@@ -7,26 +7,12 @@ import kotlinx.io.core.discardExact
import kotlinx.io.core.readBytes
import kotlinx.serialization.protobuf.ProtoBuf
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.data.ImageLink
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.ImageId
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.toMessage
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data.ImMsgBody
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data.MsgOnlinePush
import net.mamoe.mirai.utils.io.encodeToString
internal class ImageIdQQA(
override val value: String,
originalLink: String
) : ImageId {
val link: ImageLink = ImageLinkQQA("http://gchat.qpic.cn$originalLink")
}
internal inline class ImageLinkQQA(override val original: String) : ImageLink
import net.mamoe.mirai.qqandroid.utils.toMessageChain
internal class OnlinePush {
internal object PbPushGroupMsg : PacketFactory<GroupMessage>("OnlinePush.PbPushGroupMsg") {
......@@ -35,27 +21,18 @@ internal class OnlinePush {
// 00 00 02 E4 0A D5 05 0A 4F 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 52 20 00 28 BC 3D 30 8C 82 AB F1 05 38 D2 80 E0 8C 80 80 80 80 02 4A 21 08 E7 C1 AD B8 02 10 01 18 BA 05 22 09 48 69 6D 31 38 38 6D 6F 65 30 06 38 02 42 05 4D 69 72 61 69 50 01 58 01 60 00 88 01 08 12 06 08 01 10 00 18 00 1A F9 04 0A F6 04 0A 26 08 00 10 87 82 AB F1 05 18 B7 B4 BF 30 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 E6 03 42 E3 03 12 2A 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 22 00 2A 04 03 00 00 00 32 60 15 36 20 39 36 6B 45 31 41 38 35 32 32 39 64 63 36 39 38 34 37 39 37 37 62 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 31 32 31 32 41 38 C6 BB 8A A9 08 40 FB AE 9E C2 09 48 50 50 41 5A 00 60 01 6A 10 4E 18 58 22 0E 7B F8 0F C5 B1 34 48 83 74 D3 9C 72 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 31 39 38 3F 74 65 72 6D 3D 32 82 01 57 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 30 3F 74 65 72 6D 3D 32 B0 01 4D B8 01 2E C8 01 FF 05 D8 01 4D E0 01 2E FA 01 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 34 30 30 3F 74 65 72 6D 3D 32 80 02 4D 88 02 2E 12 45 AA 02 42 50 03 60 00 68 00 9A 01 39 08 09 20 BF 50 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 08 01 90 04 80 80 80 10 B8 04 00 C0 04 00 12 06 4A 04 08 00 40 01 12 14 82 01 11 0A 09 48 69 6D 31 38 38 6D 6F 65 18 06 20 08 28 03 10 8A CA 9D A1 07 1A 00
discardExact(4)
val pbPushMsg = ProtoBuf.load(MsgOnlinePush.PbPushMsg.serializer(), readBytes())
val message = MessageChain(initialCapacity = pbPushMsg.msg!!.msgBody!!.richText!!.elems!!.size)
var extraInfo: ImMsgBody.ExtraInfo? = null
pbPushMsg.msg.msgBody!!.richText!!.elems!!.forEach {
when {
it.customFace != null -> message.add(Image(ImageIdQQA(it.customFace.filePath, it.customFace.origUrl)))
it.text != null -> message.add(it.text.str.encodeToString().toMessage())
it.extraInfo != null -> extraInfo = it.extraInfo
}
}
val extraInfo: ImMsgBody.ExtraInfo? = pbPushMsg.msg.msgBody.richText.elems.firstOrNull { it.extraInfo != null }?.extraInfo
val group = bot.getGroup(pbPushMsg.msg.msgHead!!.groupInfo!!.groupCode)
val group = bot.getGroup(pbPushMsg.msg.msgHead.groupInfo!!.groupCode)
val flags = extraInfo?.flags ?: 0
return GroupMessage(
bot = bot,
group = group,
senderName = pbPushMsg.msg.msgHead.groupInfo!!.groupCard.encodeToString(),
senderName = pbPushMsg.msg.msgHead.groupInfo.groupCard,
sender = group.getMember(pbPushMsg.msg.msgHead.fromUin),
message = message,
message = pbPushMsg.msg.msgBody.richText.toMessageChain(),
permission = when {
flags and 16 != 0 -> MemberPermission.ADMINISTRATOR
flags and 8 != 0 -> MemberPermission.OWNER
......
......@@ -46,7 +46,7 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt
}
}
object SubCommand8 {
object SubCommand7 {
private const val appId = 16L
private const val subAppId = 537062845L
......@@ -59,26 +59,16 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
writeShort(8) // subCommand
writeShort(6) // count of TLVs, probably ignored by server?TODO
writeShort(7) // subCommand
writeShort(7) // count of TLVs, probably ignored by server?TODO
t8(2052)
t104(client.t104)
t116(150470524, 66560)
t174(t174)
t17a(9)
t197(byteArrayOf(0.toByte()))
//t401(md5(client.device.guid + "12 34567890123456".toByteArray() + t402))
//t19e(0)//==tlv408
t17c(phoneNumber.toByteArray())
t401(md5(client.device.guid + "1234567890123456".toByteArray() + t402))
t19e(0)//==tlv408
}
/**
*
* 0x00000008(8)=00 00 00 00 08 04 00 00,//2052固定
0x00000104(260)=41 69 78 39 46 68 4E 44 6C 41 42 30 54 79 46 30 4B 36 67 78 37 45 6E 2B 30 7A 39 35 65 35 30 6E 66 41 3D 3D,//服务器给的tv104
0x00000116(278)=00 08 F7 FF 7C 00 01 04 00 01 5F 5E 10 E2//116(this.mMiscBitmap, this.mSubSigMap, var10._sub_appid_list)
0x00000174(372)=45 66 43 39 46 4B 63 70 47 30 5F 5A 55 41 4F 6A 4E 4C 6F 72 56 30 77 66 4B 67 49 4D 33 33 6E 58 44 37 5F 4B 61 75 56 6D 4F 6F 54 68 6A 64 38 62 72 44 64 69 5F 62 48 51 5A 66 37 6E 4F 6B 78 43 35 6E 47 4E 38 6B 6A 35 39 6D 37 32 71 47 66 78 4E 76 50 51 53 39 33 66 37 6B 72 71 66 71 78 63 5F//服务器给的tv174
0x0000017A(378)=00 00 00 09, //9 固定
0x00000197(407)=00//固定
*/
}
}
}
......
......@@ -7,10 +7,10 @@ import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.io.serialization.writeJceStruct
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.jce.RequestDataStructSvcReqRegister
import net.mamoe.mirai.qqandroid.network.protocol.jce.RequestDataVersion3
import net.mamoe.mirai.qqandroid.network.protocol.jce.RequestPacket
import net.mamoe.mirai.qqandroid.network.protocol.jce.SvcReqRegister
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.data.RequestDataStructSvcReqRegister
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.data.RequestDataVersion3
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.data.RequestPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.data.SvcReqRegister
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildLoginOutgoingPacket
......
package net.mamoe.mirai.qqandroid.network.protocol.jce
package net.mamoe.mirai.qqandroid.network.protocol.packet.login.data
import kotlinx.serialization.Polymorphic
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import net.mamoe.mirai.qqandroid.io.JceStruct
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
......
package net.mamoe.mirai.qqandroid.network.protocol.jce
package net.mamoe.mirai.qqandroid.network.protocol.packet.login.data
import kotlinx.serialization.Polymorphic
import kotlinx.serialization.SerialId
......
......@@ -2,7 +2,7 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.oidb.oidb0x769
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.network.protocol.protobuf.ProtoBuf
import net.mamoe.mirai.qqandroid.io.ProtoBuf
class Oidb0x769 {
@Serializable
......
package net.mamoe.mirai.qqandroid.network.protocol.protobuf
/**
* 仅有标示作用
*/
interface ProtoBuf {
}
\ No newline at end of file
package net.mamoe.mirai.qqandroid.utils
import net.mamoe.mirai.data.ImageLink
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data.ImMsgBody
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.data.MsgSvc
internal fun MessageChain.constructPbSendMsgReq(): MsgSvc.PbSendMsgReq {
val request = MsgSvc.PbSendMsgReq()
this.forEach {
when (it) {
is PlainText -> {
request.msgBody.richText.elems.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue)))
}
is At -> {
}
}
}
return request
}
internal fun ImMsgBody.RichText.toMessageChain() : MessageChain{
val message = MessageChain(initialCapacity = elems.size)
elems.forEach {
when {
it.notOnlineImage != null -> message.add(Image(
ImageIdQQA(
it.notOnlineImage.resId,
it.notOnlineImage.origUrl
)
))
it.customFace != null -> message.add(Image(
ImageIdQQA(
it.customFace.filePath,
it.customFace.origUrl
)
))
it.text != null -> message.add(it.text.str.toMessage())
}
}
return message
}
internal class ImageIdQQA(
override val value: String,
originalLink: String
) : ImageId {
val link: ImageLink =
ImageLinkQQA("http://gchat.qpic.cn$originalLink")
}
internal inline class ImageLinkQQA(override val original: String) : ImageLink
\ No newline at end of file
package net.mamoe.mirai.qqandroid.io.serialization
import net.mamoe.mirai.qqandroid.network.protocol.jce.RequestPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.data.RequestPacket
import net.mamoe.mirai.utils.io.hexToBytes
class TestRequesetPacket {
......
......@@ -143,7 +143,7 @@ fun String.generateProtoBufDataClass(): GeneratedClass {
val javaClassname = substringBetween("class", "{")
val superclasses = javaClassname.split("$").map { it.trim().adjustClassName() }.toMutableList().apply { removeAt(this.lastIndex) }
val className = substringBetween("class", "{").substringAfterLast("$").trim().adjustClassName()
return GeneratedClass(superclasses, className, "@Serializable\nclass $className")
return GeneratedClass(superclasses, className, "@Serializable\nclass $className : ProtoBuf")
}
val superclasses = substringBetween("class", "extends").split("$").map { it.trim().adjustClassName() }.toMutableList()
......@@ -154,7 +154,7 @@ fun String.generateProtoBufDataClass(): GeneratedClass {
val ids = substringBetween("new int[]{", "}").split(",").map { it.trim() }
if (ids.all { it.isBlank() }) {
return GeneratedClass(superclasses, className, "@Serializable\nclass $className")
return GeneratedClass(superclasses, className, "@Serializable\nclass $className : ProtoBuf")
}
val names = substringBetween("new String[]{", "}").split(",").map { it.trim() }
......@@ -326,7 +326,7 @@ fun String.generateProtoBufDataClass(): GeneratedClass {
append("\n")
}
append(")")
append(") : ProtoBuf")
}
return GeneratedClass(superclasses, className, source)
......
......@@ -5,6 +5,8 @@ import io.ktor.client.engine.cio.CIO
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.utils.io.ByteArrayPool
import java.io.ByteArrayOutputStream
import java.io.DataInput
import java.io.EOFException
......@@ -83,17 +85,23 @@ actual fun crc32(key: ByteArray): Int = CRC32().apply { update(key) }.value.toIn
*/
actual fun solveIpAddress(hostname: String): String = InetAddress.getByName(hostname).hostAddress
actual fun ByteArray.unzip(): ByteArray {
actual fun ByteArray.unzip(offset: Int, length: Int): ByteArray {
this.checkOffsetAndLength(offset, length)
if (length == 0) return ByteArray(0)
val inflater = Inflater()
inflater.reset()
val output = ByteArrayOutputStream()
inflater.setInput(this)
val buffer = ByteArray(128)
while (!inflater.finished()) {
output.write(buffer, 0, inflater.inflate(buffer))
ByteArrayOutputStream().use { output ->
inflater.setInput(this, offset, length)
ByteArrayPool.useInstance {
while (!inflater.finished()) {
output.write(it, 0, inflater.inflate(it))
}
}
inflater.end()
return output.toByteArray()
}
inflater.end()
return output.toByteArray()
}
actual fun newCoroutineDispatcher(threadCount: Int): CoroutineDispatcher {
......
......@@ -15,7 +15,7 @@ actual class ECDHKeyPair(
actual val privateKey: ECDHPrivateKey get() = delegate.private
actual val publicKey: ECDHPublicKey get() = delegate.public
actual val shareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey)
actual val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey)
}
@Suppress("FunctionName")
......
......@@ -3,8 +3,6 @@
package net.mamoe.mirai
import kotlinx.io.core.toByteArray
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.md5
import kotlin.annotation.AnnotationTarget.*
......@@ -12,17 +10,17 @@ data class BotAccount(
/**
* **注意**: 在 Android 协议, 总是使用 `QQAndroidClient.uin` 或 [Bot.uin], 而不要使用 [BotAccount.id]. 将来 [BotAccount.id] 可能会变为 [String]
*/
@MiraiExperimentalAPI
@RawAccountIdUse
val id: Long,
val passwordMd5: ByteArray // md5
){
) {
constructor(id: Long, passwordPlainText: String) : this(id, md5(passwordPlainText.toByteArray()))
}
/**
* 标记直接访问 [BotAccount.id], 而不是访问 [Bot.uin]. 这可能会不兼容未来的 API 修改.
* 标记直接访问 [BotAccount.id], 而不是访问 [Bot.uin]. 这可能会不兼容未来的 API 修改.
*/
@Retention(AnnotationRetention.SOURCE)
@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR)
@Experimental
@Experimental(level = Experimental.Level.WARNING)
annotation class RawAccountIdUse
\ No newline at end of file
......@@ -13,6 +13,7 @@ import kotlin.coroutines.CoroutineContext
/*
* 泛型 N 不需要向外(接口)暴露.
*/
@UseExperimental(MiraiExperimentalAPI::class)
@MiraiInternalAPI
abstract class BotImpl<N : BotNetworkHandler> constructor(
account: BotAccount,
......@@ -25,10 +26,9 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
@Suppress("CanBePrimaryConstructorProperty") // for logger
final override val account: BotAccount = account
@UseExperimental(MiraiExperimentalAPI::class)
final override val uin: Long
get() = account.id
final override val logger: MiraiLogger = configuration.logger ?: DefaultLogger("Bot($uin)").also { configuration.logger = it }
@UseExperimental(RawAccountIdUse::class)
override val uin: Long get() = account.id
final override val logger: MiraiLogger by lazy { configuration.logger ?: DefaultLogger("Bot($uin)").also { configuration.logger = it } }
init {
@Suppress("LeakingThis")
......
......@@ -8,14 +8,14 @@ import net.mamoe.mirai.utils.coerceAtLeastOrFail
/**
* 群.
* 群. 在 QQ Android 中叫做 "Troop"
*
* Group ID 与 Group Number 并不是同一个值.
* - Group Number([Group.id]) 是通常使用的群号码.(在 QQ 客户端中可见)
* - Group ID([Group.internalId]) 是与调用 API 时使用的 id.(在 QQ 客户端中不可见)
* @author Him188moe
*/
interface Group : Contact, CoroutineScope/*, Map<UInt, Member>*/ { // TODO: 2019/12/4 在 inline 稳定后实现 Map<UInt, Member>. 目前这样做会导致问题
interface Group : Contact, CoroutineScope/*, Map<UInt, Member>*/ { // TODO: 2020/1/29 实现接口 Map<Long, Memebr>
/**
* 内部 ID. 内部 ID 为 [GroupId] 的映射
*/
......@@ -86,7 +86,10 @@ fun Long.groupInternalId(): GroupInternalId = GroupInternalId(this)
/**
* 将无符号整数格式的 [Long] 转为 [GroupId].
*
* 注: 在 Java 中常用 [Long] 来表示 [UInt]
* 注: 在 Java 中常用 [Long] 来表示 [UInt].
*
* 注: 在 Kotlin/Java, 有符号的数据类型的二进制最高位为符号标志.
* 如一个 byte, `1000 0000` 最高位为 1, 则为负数.
*/
fun Long.groupId(): GroupId = GroupId(this.coerceAtLeastOrFail(0))
......
......@@ -4,3 +4,12 @@ package net.mamoe.mirai.data
* 从服务器收到的包解析之后的结构化数据.
*/
interface Packet
/**
* PacketFactory 可以一次解析多个包出来. 它们将会被分别广播.
*/
class MultiPacket<P : Packet>(delegate: List<P>) : List<P> by delegate, Packet {
override fun toString(): String {
return "MultiPacket<${this.firstOrNull()?.let { it::class.simpleName }?: "?"}>"
}
}
\ No newline at end of file
......@@ -112,7 +112,7 @@ inline fun <reified E : Subscribable, T> CoroutineScope.subscribeUntil(valueIfSt
* @see subscribe 获取更多说明
*/
inline fun <reified E : Subscribable, T> CoroutineScope.subscribeWhile(valueIfContinue: T, crossinline listener: suspend E.(E) -> T): Listener<E> =
E::class.subscribeInternal(Handler { if (it.listener(it) !== valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING })
E::class.subscribeInternal(Handler { if (it.listener(it) != valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING })
// endregion
......
......@@ -5,6 +5,7 @@ package net.mamoe.mirai.network
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableJob
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.io.PlatformDatagramChannel
......@@ -18,22 +19,26 @@ import net.mamoe.mirai.utils.io.PlatformDatagramChannel
*
* [BotNetworkHandler] 的协程包含:
* - UDP 包接收: [PlatformDatagramChannel.read]
* - 心跳 Job [HeartbeatPacket]
* - SKey 刷新 [RequestSKeyPacket]
* - 心跳 Job
* - Key 刷新
* - 所有数据包处理和发送
*
* [BotNetworkHandler.dispose] 时将会 [取消][kotlin.coroutines.CoroutineContext.cancelChildren] 所有此作用域下的协程
*
* A BotNetworkHandler is used to connect with Tencent servers.
* [BotNetworkHandler.dispose] 时将会 [取消][Job.cancel] 所有此作用域下的协程
*/
@Suppress("PropertyName")
abstract class BotNetworkHandler : CoroutineScope {
/**
* 所属 [Bot]. 为弱引用
*/
abstract val bot: Bot
/**
* 监管 child [Job]s
*/
abstract val supervisor: CompletableJob
/**
* 依次尝试登录到可用的服务器. 在任一服务器登录完成后返回登录结果
* 依次尝试登录到可用的服务器. 在任一服务器登录完成后返回.
* 本函数将挂起直到登录成功.
*/
abstract suspend fun login()
......@@ -47,21 +52,12 @@ abstract class BotNetworkHandler : CoroutineScope {
* 关闭网络接口, 停止所有有关协程和任务
*/
open fun dispose(cause: Throwable? = null) {
supervisor.cancel(CancellationException("handler closed", cause))
if (supervisor.isActive) {
if (cause != null) {
supervisor.cancel(CancellationException("handler closed", cause))
} else {
supervisor.cancel()
}
}
}
/*
@PublishedApi
internal abstract fun CoroutineScope.QQ(bot: Bot, id: Long, coroutineContext: CoroutineContext): QQ
@PublishedApi
internal abstract fun CoroutineScope.Group(bot: Bot, groupId: GroupId, info: RawGroupInfo, context: CoroutineContext): Group
@PublishedApi
internal abstract fun Group.Member(delegate: QQ, permission: MemberPermission, coroutineContext: CoroutineContext): Member
*/
}
\ No newline at end of file
......@@ -24,7 +24,10 @@ expect val deviceName: String
*/
expect fun crc32(key: ByteArray): Int
expect fun ByteArray.unzip(): ByteArray
/**
* 解 zip 压缩
*/
expect fun ByteArray.unzip(offset: Int = 0, length: Int = this.size - offset): ByteArray
/**
* MD5 算法
......@@ -48,4 +51,10 @@ expect fun localIpAddress(): String
*/
expect val Http: HttpClient
expect fun newCoroutineDispatcher(threadCount: Int): CoroutineDispatcher
\ No newline at end of file
expect fun newCoroutineDispatcher(threadCount: Int): CoroutineDispatcher
internal fun ByteArray.checkOffsetAndLength(offset: Int, length: Int){
require(offset >= 0) { "offset shouldn't be negative: $offset" }
require(length >= 0) { "length shouldn't be negative: $length" }
require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" }
}
\ No newline at end of file
......@@ -15,23 +15,46 @@ expect class ECDHKeyPair {
val privateKey: ECDHPrivateKey
val publicKey: ECDHPublicKey
val shareKey: ByteArray
/**
* 私匙和固定公匙([initialPublicKey]) 计算得到的 shareKey
*/
val initialShareKey: ByteArray
}
/**
* 椭圆曲线密码, ECDH 加密
*/
expect class ECDH(keyPair: ECDHKeyPair) {
val keyPair: ECDHKeyPair
/**
* 由 [keyPair] 的私匙和 [peerPublicKey] 计算 shareKey
*/
fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray
companion object {
/**
* 由完整的 publicKey ByteArray 得到 [ECDHPublicKey]
*/
fun constructPublicKey(key: ByteArray): ECDHPublicKey
/**
* 生成随机密匙对
*/
fun generateKeyPair(): ECDHKeyPair
/**
* 由一对密匙计算 shareKey
*/
fun calculateShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray
}
override fun toString(): String
}
/**
*
*/
@Suppress("FunctionName")
expect fun ECDH(): ECDH
......
......@@ -38,6 +38,14 @@ fun ByteReadPacket.transferTo(outputStream: OutputStream) {
}
}
fun <R> ByteReadPacket.useBytes(
n: Int = remaining.toInt(),//not that safe but adequate
block: (data: ByteArray, length: Int) -> R
): R = ByteArrayPool.useInstance {
this.readFully(it, 0, n)
block(it, n)
}
fun ByteReadPacket.readRemainingBytes(
n: Int = remaining.toInt()//not that safe but adequate
): ByteArray = ByteArray(n).also { readAvailable(it, 0, n) }
......@@ -75,10 +83,21 @@ fun Input.readUShortLVByteArray(): ByteArray = this.readBytes(this.readUShort().
private inline fun <R> inline(block: () -> R): R = block()
fun Input.readTLVMap(tagSize: Int = 2): MutableMap<Int, ByteArray> = readTLVMap(true, tagSize)
typealias TlvMap = MutableMap<Int, ByteArray>
fun TlvMap.getOrFail(tag: Int): ByteArray {
return this[tag] ?: error("cannot find tlv 0x${tag.toUHexString("")}($tag)")
}
inline fun TlvMap.getOrFail(tag: Int, lazyMessage: (tag: Int) -> String): ByteArray {
return this[tag] ?: error(lazyMessage(tag))
}
fun Input.readTLVMap(tagSize: Int = 2): TlvMap = readTLVMap(true, tagSize)
@Suppress("DuplicatedCode")
fun Input.readTLVMap(expectingEOF: Boolean = true, tagSize: Int): MutableMap<Int, ByteArray> {
fun Input.readTLVMap(expectingEOF: Boolean = true, tagSize: Int): TlvMap {
val map = mutableMapOf<Int, ByteArray>()
var key = 0
......
......@@ -7,8 +7,10 @@ import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.io.core.copyTo
import kotlinx.io.pool.useInstance
import kotlinx.io.streams.asInput
import kotlinx.io.streams.asOutput
import net.mamoe.mirai.utils.io.ByteArrayPool
import java.io.*
import java.net.InetAddress
import java.security.MessageDigest
......@@ -56,18 +58,23 @@ actual fun localIpAddress(): String = InetAddress.getLocalHost().hostAddress
actual val Http: HttpClient get() = HttpClient(CIO)
actual fun ByteArray.unzip(): ByteArray {
actual fun ByteArray.unzip(offset: Int, length: Int): ByteArray {
this.checkOffsetAndLength(offset, length)
if (length == 0) return ByteArray(0)
val inflater = Inflater()
inflater.reset()
val input = this
val output = ByteArrayOutputStream()
inflater.setInput(input)
val buffer = ByteArray(128)
while (!inflater.finished()) {
output.write(buffer, 0, inflater.inflate(buffer))
ByteArrayOutputStream().use { output ->
inflater.setInput(this, offset, length)
ByteArrayPool.useInstance {
while (!inflater.finished()) {
output.write(it, 0, inflater.inflate(it))
}
}
inflater.end()
return output.toByteArray()
}
inflater.end()
return output.toByteArray()
}
actual fun newCoroutineDispatcher(threadCount: Int): CoroutineDispatcher {
......
......@@ -17,7 +17,7 @@ actual class ECDHKeyPair(
actual val privateKey: ECDHPrivateKey get() = delegate.private
actual val publicKey: ECDHPublicKey get() = delegate.public
actual val shareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey)
actual val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey)
}
@Suppress("FunctionName")
......
......@@ -36,7 +36,11 @@ actual class PlatformSocket : Closeable {
* @throws SendPacketInternalException
*/
actual suspend inline fun send(packet: ByteReadPacket) {
writeChannel.writePacket(packet)
try {
writeChannel.writePacket(packet)
} catch (e: Exception) {
throw SendPacketInternalException(e)
}
}
/**
......@@ -45,7 +49,11 @@ actual class PlatformSocket : Closeable {
actual suspend inline fun read(): ByteReadPacket {
// do not use readChannel.readRemaining() !!! this function never returns
ByteArrayPool.useInstance { buffer ->
val count = readChannel.readAvailable(buffer)
val count = try {
readChannel.readAvailable(buffer)
} catch (e: Exception) {
throw ReadPacketInternalException(e)
}
return buffer.toReadPacket(0, count)
}
}
......
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