mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 05:12:17 +00:00
Compare commits
49 Commits
1.0.8
...
9bbcc2f160
Author | SHA1 | Date | |
---|---|---|---|
9bbcc2f160 | |||
00b355b877 | |||
071ddbb69a | |||
4d5c054bc4 | |||
87629666f2 | |||
75633f78c4 | |||
c940aea153 | |||
18126b1fda | |||
e9884a5fa8 | |||
aa7b241dba | |||
c70f3eabfe | |||
1c65aab673 | |||
a5cdd64686 | |||
b07ca5bd03 | |||
8f8580d542 | |||
0ed4480878 | |||
c3e0031aa4 | |||
388c963e88 | |||
4283651b1e | |||
50d7dfa06d | |||
b3a2e605fb | |||
14bf5fc0a2 | |||
2c8b57a7dc | |||
8e6c167987 | |||
2c8094c8c8 | |||
62385d6f62 | |||
3b210d7ed0 | |||
63ce2d40bd | |||
36f8b6e54b | |||
58413044e9 | |||
3395cd9d95 | |||
494b1f1fd0 | |||
cf943fd13a | |||
9608b46799 | |||
502956e3ec | |||
27b4c26da7 | |||
65f54360f8 | |||
9a9fad975f | |||
7153b21cd4 | |||
fdb2486090 | |||
d60b2a25d1 | |||
2d8dde6951 | |||
78fd60dade | |||
80dbf6af28 | |||
1e53753b5a | |||
e727877268 | |||
63b69df3ea | |||
b03e02675b | |||
e68a1ffd37 |
BIN
.github/jetbrains-variant-3.png
vendored
Normal file
BIN
.github/jetbrains-variant-3.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 178 KiB |
12
.github/workflows/build-apk.yml
vendored
12
.github/workflows/build-apk.yml
vendored
@ -22,13 +22,13 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup JDK 17
|
- name: Setup JDK 17
|
||||||
uses: actions/setup-java@v4.0.0
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
|
|
||||||
- name: Cache Gradle Dependencies
|
- name: Cache Gradle Dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@ -38,7 +38,7 @@ jobs:
|
|||||||
restore-keys: gradle-deps
|
restore-keys: gradle-deps
|
||||||
|
|
||||||
- name: Cache Gradle Build
|
- name: Cache Gradle Build
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches/build-cache-*
|
~/.gradle/caches/build-cache-*
|
||||||
@ -82,19 +82,19 @@ jobs:
|
|||||||
echo "|x86_64|$x86_64" >> $GITHUB_STEP_SUMMARY
|
echo "|x86_64|$x86_64" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
- name: Upload ALL APK RELEASE
|
- name: Upload ALL APK RELEASE
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: "${{ env.SHAMROCK_VERSION_ALL }}"
|
name: "${{ env.SHAMROCK_VERSION_ALL }}"
|
||||||
path: "${{ env.APK_FILE_ALL }}"
|
path: "${{ env.APK_FILE_ALL }}"
|
||||||
|
|
||||||
- name: Upload ARM64 APK RELEASE
|
- name: Upload ARM64 APK RELEASE
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: "${{ env.SHAMROCK_VERSION_ARM64 }}"
|
name: "${{ env.SHAMROCK_VERSION_ARM64 }}"
|
||||||
path: "${{ env.APK_FILE_ARM64 }}"
|
path: "${{ env.APK_FILE_ARM64 }}"
|
||||||
|
|
||||||
- name: Upload X86_64 APK RELEASE
|
- name: Upload X86_64 APK RELEASE
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: "${{ env.SHAMROCK_VERSION_x86_64 }}"
|
name: "${{ env.SHAMROCK_VERSION_x86_64 }}"
|
||||||
path: "${{ env.APK_FILE_X86_64 }}"
|
path: "${{ env.APK_FILE_X86_64 }}"
|
@ -46,13 +46,19 @@
|
|||||||
|
|
||||||
<img src="https://github.com/whitechi73/OpenShamrock/assets/98259561/f04d60bc-ec40-41fc-bc15-62c146f1a1f1" width="160px" alt="Shamrock"> **我可爱吗?欢迎你的到来,这里是一个很大的地方,有着无限可能,主要是有你啦!**
|
<img src="https://github.com/whitechi73/OpenShamrock/assets/98259561/f04d60bc-ec40-41fc-bc15-62c146f1a1f1" width="160px" alt="Shamrock"> **我可爱吗?欢迎你的到来,这里是一个很大的地方,有着无限可能,主要是有你啦!**
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
感谢[**JetBrains**](https://www.jetbrains.com/zh-cn/community/opensource/#support)提供的开源开发许可证,JetBrains 通过为核心项目贡献者免费提供一套一流的开发者工具来支持非商业开源项目。
|
||||||
|
|
||||||
|
[<img src=".github/jetbrains-variant-3.png" width="200"/>](https://www.jetbrains.com/zh-cn/community/opensource/#support)
|
||||||
|
|
||||||
## 开源协议
|
## 开源协议
|
||||||
|
|
||||||
本项目使用 [GPL-3.0](LICENSE) 协议开放源代码
|
本项目使用 [GPL-3.0](LICENSE) 协议开放源代码
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Shamrock - OneBot standard QQ robot framework based on Xposed implementation
|
Shamrock - OneBot standard QQ robot framework based on Xposed implementation
|
||||||
Copyright (C) 2023 Shamrock Team
|
Copyright (C) 2023 ~ 2024 Shamrock Team
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
@ -102,3 +108,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
[contrib-image]: https://contrib.rocks/image?repo=whitechi73/OpenShamrock
|
[contrib-image]: https://contrib.rocks/image?repo=whitechi73/OpenShamrock
|
||||||
|
|
||||||
[contrib-link]: https://github.com/whitechi73/OpenShamrock/graphs/contributors
|
[contrib-link]: https://github.com/whitechi73/OpenShamrock/graphs/contributors
|
||||||
|
|
||||||
|
12
SECURITY.md
12
SECURITY.md
@ -1,11 +1,19 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
## Support Version
|
## 支持的版本
|
||||||
|
|
||||||
| Version | Supported |
|
| 版本 | 支持状态 |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
|
| 9.0.15 | :white_check_mark: |
|
||||||
| 8.9.75 | :white_check_mark: |
|
| 8.9.75 | :white_check_mark: |
|
||||||
| 8.9.73 | :white_check_mark: |
|
| 8.9.73 | :white_check_mark: |
|
||||||
| 8.9.98 | :white_check_mark: |
|
| 8.9.98 | :white_check_mark: |
|
||||||
| < 8.9.68| :x: |
|
| < 8.9.68| :x: |
|
||||||
|
|
||||||
|
## 频道支持性说明
|
||||||
|
|
||||||
|
如果需要使用`频道`相关功能,请升级QQ到9.0.8版本!
|
||||||
|
|
||||||
|
## Riru检测问题
|
||||||
|
|
||||||
|
QQ自`9.0.8`开始将会检测riru,可能作为封号因素。
|
||||||
|
@ -229,6 +229,16 @@ object ShamrockConfig {
|
|||||||
return preferences.getBoolean("anti_qq_trace", true)
|
return preferences.getBoolean("anti_qq_trace", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isForbidUselessProcess(ctx: Context): Boolean {
|
||||||
|
val preferences = ctx.getSharedPreferences("config", 0)
|
||||||
|
return preferences.getBoolean("forbid_useless_process", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setForbidUselessProcess(ctx: Context, v: Boolean) {
|
||||||
|
val preferences = ctx.getSharedPreferences("config", 0)
|
||||||
|
preferences.edit().putBoolean("forbid_useless_process", v).apply()
|
||||||
|
}
|
||||||
|
|
||||||
fun setAntiTrace(ctx: Context, v: Boolean) {
|
fun setAntiTrace(ctx: Context, v: Boolean) {
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
val preferences = ctx.getSharedPreferences("config", 0)
|
||||||
preferences.edit().putBoolean("anti_qq_trace", v).apply()
|
preferences.edit().putBoolean("anti_qq_trace", v).apply()
|
||||||
@ -333,6 +343,7 @@ object ShamrockConfig {
|
|||||||
"alive_reply" to preferences.getBoolean("alive_reply", false),
|
"alive_reply" to preferences.getBoolean("alive_reply", false),
|
||||||
"enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false),
|
"enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false),
|
||||||
"disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false),
|
"disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false),
|
||||||
|
"forbid_useless_process" to preferences.getBoolean("forbid_useless_process", false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,17 +100,16 @@ fun LabFragment() {
|
|||||||
thickness = 0.2.dp
|
thickness = 0.2.dp
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
|
||||||
Function(
|
Function(
|
||||||
title = "自动清理QQ垃圾",
|
title = "禁止无用进程",
|
||||||
desc = "也许会导致奇怪的问题(无效)。",
|
desc = "禁止QQ生成无用进程浪费内存",
|
||||||
descColor = color,
|
descColor = color,
|
||||||
isSwitch = ShamrockConfig.isAutoClean(ctx)
|
isSwitch = ShamrockConfig.isForbidUselessProcess(ctx)
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setAutoClean(ctx, it)
|
ShamrockConfig.setForbidUselessProcess(ctx, it)
|
||||||
ShamrockConfig.pushUpdate(ctx)
|
ShamrockConfig.pushUpdate(ctx)
|
||||||
return@Function false
|
return@Function true
|
||||||
}*/
|
}
|
||||||
|
|
||||||
Function(
|
Function(
|
||||||
title = "自回复测试",
|
title = "自回复测试",
|
||||||
|
@ -5,53 +5,13 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MessageBody(
|
|
||||||
@ProtoNumber(1) val msgHead: MessageHead? = null,
|
|
||||||
@ProtoNumber(2) val contentHead: MessageContentHead? = null,
|
|
||||||
@ProtoNumber(3) val richMsg: RichMessage? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RichMessage(
|
data class RichMessage(
|
||||||
@ProtoNumber(1) val elements: MessageElementList? = null,
|
@ProtoNumber(1) val font: Font? = null,
|
||||||
@ProtoNumber(2) val rawBuffer: ByteArray? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MessageElementList(
|
|
||||||
@ProtoNumber(2) val elements: List<MessageElement>? = null
|
@ProtoNumber(2) val elements: List<MessageElement>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MessageElement(
|
data class Font(
|
||||||
@ProtoNumber(51) val json: JsonElement? = null,
|
@ProtoNumber(9) val fontName: String? = null
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class JsonElement(
|
|
||||||
@ProtoNumber(1) val data: ByteArray? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MessageHead(
|
|
||||||
@ProtoNumber(1) val peer: Long = Long.MIN_VALUE,
|
|
||||||
@ProtoNumber(2) val peerUid: String? = null,
|
|
||||||
@ProtoNumber(3) val flag: Int = Int.MIN_VALUE,
|
|
||||||
@ProtoNumber(5) val receiver: Long? = null,
|
|
||||||
@ProtoNumber(6) val receiverUid: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MessageContentHead(
|
|
||||||
@ProtoNumber(1) val msgType: Int = Int.MIN_VALUE,
|
|
||||||
@ProtoNumber(2) val msgSubType: Int = Int.MIN_VALUE,
|
|
||||||
@ProtoNumber(4) val u1: Long = Long.MIN_VALUE,
|
|
||||||
@ProtoNumber(5) val msgSeq: Long = Long.MIN_VALUE,
|
|
||||||
@ProtoNumber(6) val msgTime: Long? = null,
|
|
||||||
@ProtoNumber(7) val u2: Int? = null,
|
|
||||||
@ProtoNumber(11) val u3: Long? = null,
|
|
||||||
@ProtoNumber(12) val msgRandom: Long = Long.MIN_VALUE,
|
|
||||||
@ProtoNumber(14) val u4: Long? = null,
|
|
||||||
@ProtoNumber(28) val u5: Long? = null,
|
|
||||||
)
|
)
|
11
protobuf/src/main/java/protobuf/message/MessageBody.kt
Normal file
11
protobuf/src/main/java/protobuf/message/MessageBody.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package protobuf.message
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MessageBody(
|
||||||
|
@ProtoNumber(1) val rich: RichMessage? = null,
|
||||||
|
@ProtoNumber(2) val rawBuffer: ByteArray? = null,
|
||||||
|
@ProtoNumber(3) val MsgEncryptContent: ByteArray? = null
|
||||||
|
)
|
31
protobuf/src/main/java/protobuf/message/MessageContent.kt
Normal file
31
protobuf/src/main/java/protobuf/message/MessageContent.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package protobuf.message
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MessageContent(
|
||||||
|
@ProtoNumber(1) val msgType: Int = Int.MIN_VALUE,
|
||||||
|
@ProtoNumber(2) val msgSubType: Int? = null,
|
||||||
|
@ProtoNumber(3) val u1: Int? = null,
|
||||||
|
@ProtoNumber(4) val msgViaRandom: Long = Long.MIN_VALUE,
|
||||||
|
@ProtoNumber(5) val msgSeq_: Long? = null,
|
||||||
|
@ProtoNumber(6) val msgTime: Long? = null,
|
||||||
|
@ProtoNumber(7) val u2: Int? = null,
|
||||||
|
@ProtoNumber(8) val u6: Int? = null,
|
||||||
|
@ProtoNumber(9) val u7: Int? = null,
|
||||||
|
@ProtoNumber(11) val msgSeq: Long? = null,
|
||||||
|
@ProtoNumber(12) val msgRandom: Long = Long.MIN_VALUE,
|
||||||
|
@ProtoNumber(14) val u4: Long? = null,
|
||||||
|
@ProtoNumber(15) val forwardHead: ForwardHead? = null,
|
||||||
|
@ProtoNumber(28) val u5: Long? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ForwardHead(
|
||||||
|
@ProtoNumber(1) val u1: Int? = null,
|
||||||
|
@ProtoNumber(2) val u2: Int? = null,
|
||||||
|
@ProtoNumber(3) val u3: Int? = null,
|
||||||
|
@ProtoNumber(4) val ub641: String? = null,
|
||||||
|
@ProtoNumber(5) val Avatar: String? = null
|
||||||
|
)
|
13
protobuf/src/main/java/protobuf/message/MessageElement.kt
Normal file
13
protobuf/src/main/java/protobuf/message/MessageElement.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package protobuf.message
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
import protobuf.message.element.*
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MessageElement(
|
||||||
|
@ProtoNumber(1) val text: TextElement? = null,
|
||||||
|
@ProtoNumber(2) val face: FaceElement? = null,
|
||||||
|
@ProtoNumber(51) val json: JsonElement? = null,
|
||||||
|
@ProtoNumber(53) val comm: CommonElement? = null,
|
||||||
|
)
|
33
protobuf/src/main/java/protobuf/message/MessageHead.kt
Normal file
33
protobuf/src/main/java/protobuf/message/MessageHead.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
@file:OptIn(ExperimentalSerializationApi::class)
|
||||||
|
package protobuf.message
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MessageHead(
|
||||||
|
@ProtoNumber(1) val peer: Long = Long.MIN_VALUE,
|
||||||
|
@ProtoNumber(2) val peerUid: String? = null,
|
||||||
|
@ProtoNumber(3) val flag: Int = Int.MIN_VALUE,
|
||||||
|
@ProtoNumber(4) val appId: Int = Int.MIN_VALUE,
|
||||||
|
@ProtoNumber(5) val receiver: Long? = null,
|
||||||
|
@ProtoNumber(6) val receiverUid: String? = null,
|
||||||
|
@ProtoNumber(7) val forward: MessageForward? = null,
|
||||||
|
@ProtoNumber(8) val groupInfo: GroupInfo? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MessageForward(
|
||||||
|
@ProtoNumber(6) val friendName: String? = null,
|
||||||
|
@ProtoNumber(11) val u1: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GroupInfo(
|
||||||
|
@ProtoNumber(1) val groupCode: ULong = ULong.MIN_VALUE,
|
||||||
|
@ProtoNumber(4) val memberCard: String? = null,
|
||||||
|
@ProtoNumber(5) val u1: Int? = null,
|
||||||
|
@ProtoNumber(7) val groupName: String? = null,
|
||||||
|
@ProtoNumber(10) val u2: Int? = null,
|
||||||
|
)
|
11
protobuf/src/main/java/protobuf/message/NtMessage.kt
Normal file
11
protobuf/src/main/java/protobuf/message/NtMessage.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package protobuf.message
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class NtMessage(
|
||||||
|
@ProtoNumber(1) val msgHead: MessageHead? = null,
|
||||||
|
@ProtoNumber(2) val contentHead: MessageContent? = null,
|
||||||
|
@ProtoNumber(3) val body: MessageBody? = null,
|
||||||
|
)
|
@ -0,0 +1,11 @@
|
|||||||
|
package protobuf.message.element
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CommonElement(
|
||||||
|
@ProtoNumber(1) val type: Int? = null,
|
||||||
|
@ProtoNumber(2) val data: ByteArray? = null,
|
||||||
|
@ProtoNumber(3) val u1: Int? = null,
|
||||||
|
)
|
@ -0,0 +1,9 @@
|
|||||||
|
package protobuf.message.element
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FaceElement(
|
||||||
|
@ProtoNumber(1) val id: Int? = null,
|
||||||
|
)
|
@ -0,0 +1,9 @@
|
|||||||
|
package protobuf.message.element
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class JsonElement(
|
||||||
|
@ProtoNumber(1) val data: ByteArray? = null,
|
||||||
|
)
|
@ -0,0 +1,40 @@
|
|||||||
|
package protobuf.message.element
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TextElement(
|
||||||
|
@ProtoNumber(1) val text: String? = null,
|
||||||
|
@ProtoNumber(2) val link: String? = null,
|
||||||
|
@ProtoNumber(3) val attr6Buf: ByteArray? = null,
|
||||||
|
@ProtoNumber(4) val attr7Buf: ByteArray? = null,
|
||||||
|
@ProtoNumber(11) val buf: ByteArray? = null,
|
||||||
|
@ProtoNumber(12) val pbReserve: TextResvAttr? = null,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Serializable
|
||||||
|
data class TextResvAttr(
|
||||||
|
@ProtoNumber(1) val wording: ByteArray? = null,
|
||||||
|
@ProtoNumber(2) val textAnalysisResult: Int? = null,
|
||||||
|
@ProtoNumber(3) val atType: Int? = null,
|
||||||
|
@ProtoNumber(4) val atMemberUin: Long? = null,
|
||||||
|
@ProtoNumber(5) val atMemberTinyid: Long? = null,
|
||||||
|
@ProtoNumber(6) val atChannelInfo: ExtChannelInfo? = null,
|
||||||
|
@ProtoNumber(7) val atRoleInfo: ExtRoleInfo? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExtChannelInfo(
|
||||||
|
@ProtoNumber(1) val guildId: Long? = null,
|
||||||
|
@ProtoNumber(2) val channelId: Long? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExtRoleInfo(
|
||||||
|
@ProtoNumber(1) val id: Long? = null,
|
||||||
|
@ProtoNumber(2) val info: ByteArray? = null,
|
||||||
|
@ProtoNumber(3) val flag: Int? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
62
protobuf/src/main/java/protobuf/message/longmsg/LongMsg.kt
Normal file
62
protobuf/src/main/java/protobuf/message/longmsg/LongMsg.kt
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
@file:OptIn(ExperimentalSerializationApi::class)
|
||||||
|
package protobuf.message.longmsg
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LongMsgSettings(
|
||||||
|
@ProtoNumber(1) val field1: Int? = null,
|
||||||
|
@ProtoNumber(2) val field2: Int? = null,
|
||||||
|
@ProtoNumber(3) val field3: Int? = null,
|
||||||
|
@ProtoNumber(4) val field4: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LongMsgUid(
|
||||||
|
@ProtoNumber(2) val uid: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RecvLongMsgInfo(
|
||||||
|
@ProtoNumber(1) val uid: LongMsgUid? = null,
|
||||||
|
@ProtoNumber(2) val resId: String? = null,
|
||||||
|
@ProtoNumber(3) val u1: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SendLongMsgInfo(
|
||||||
|
@ProtoNumber(1) val type: Int? = null,
|
||||||
|
@ProtoNumber(2) val uid: LongMsgUid? = null,
|
||||||
|
@ProtoNumber(3) val groupUin: Int? = null,
|
||||||
|
@ProtoNumber(4) val payload: ByteArray? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LongMsgReq(
|
||||||
|
@ProtoNumber(1) val recvInfo: RecvLongMsgInfo? = null,
|
||||||
|
@ProtoNumber(2) val sendInfo: SendLongMsgInfo? = null,
|
||||||
|
@ProtoNumber(15) val setting: LongMsgSettings? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LongMsgRsp(
|
||||||
|
@ProtoNumber(1) val recvResult: RecvLongMsgResult? = null,
|
||||||
|
@ProtoNumber(2) val sendResult: SendLongMsgResult? = null,
|
||||||
|
@ProtoNumber(15) val setting: LongMsgSettings? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Serializable
|
||||||
|
data class SendLongMsgResult(
|
||||||
|
@ProtoNumber(3) val resId: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RecvLongMsgResult(
|
||||||
|
@ProtoNumber(3) val resId: String? = null,
|
||||||
|
@ProtoNumber(4) val payload: ByteArray? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
@file:OptIn(ExperimentalSerializationApi::class)
|
||||||
|
|
||||||
|
package protobuf.message.longmsg
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
import protobuf.message.MessageBody
|
||||||
|
import protobuf.message.MessageContent
|
||||||
|
import protobuf.message.MessageHead
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PushMsgBody(
|
||||||
|
@ProtoNumber(1) val head: MessageHead? = null,
|
||||||
|
@ProtoNumber(2) val content: MessageContent? = null,
|
||||||
|
@ProtoNumber(3) val body: MessageBody? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LongMsgContent(
|
||||||
|
@ProtoNumber(1) val body: List<PushMsgBody>? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LongMsgAction(
|
||||||
|
@ProtoNumber(1) val command: String? = null,
|
||||||
|
@ProtoNumber(2) val data: LongMsgContent? = null
|
||||||
|
)
|
||||||
|
@Serializable
|
||||||
|
data class LongMsgPayload(
|
||||||
|
@ProtoNumber(2) val action: List<LongMsgAction>? = null
|
||||||
|
)
|
@ -0,0 +1,54 @@
|
|||||||
|
@file:OptIn(ExperimentalSerializationApi::class)
|
||||||
|
package protobuf.message.multimedia
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RichMediaForPicData(
|
||||||
|
@ProtoNumber(1) val info: MediaInfo?,
|
||||||
|
@ProtoNumber(2) val display: DisplayMediaInfo?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Serializable
|
||||||
|
data class MediaInfo(
|
||||||
|
@ProtoNumber(1) val picture: Picture? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Picture(
|
||||||
|
@ProtoNumber(1) val info: PictureInfo? = null,
|
||||||
|
@ProtoNumber(2) val fileId: String? = null,
|
||||||
|
@ProtoNumber(4) val time: ULong? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PictureInfo(
|
||||||
|
@ProtoNumber(2) val md5Hex: String? = null,
|
||||||
|
@ProtoNumber(3) val sha: String? = null,
|
||||||
|
@ProtoNumber(4) val name: String? = null,
|
||||||
|
@ProtoNumber(6) val width: Int? = null,
|
||||||
|
@ProtoNumber(7) val height: Int? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DisplayMediaInfo(
|
||||||
|
@ProtoNumber(1) val show: Show? = null,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Serializable
|
||||||
|
data class Show(
|
||||||
|
@ProtoNumber(2) val text: String? = null,
|
||||||
|
@ProtoNumber(12) val download: Download? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Download(
|
||||||
|
@ProtoNumber(30) val url: String? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
104
protobuf/src/main/java/protobuf/msg/ImMsgBody.kt
Normal file
104
protobuf/src/main/java/protobuf/msg/ImMsgBody.kt
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package protobuf.msg
|
||||||
|
|
||||||
|
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MsgBody(
|
||||||
|
@ProtoNumber(1) var richText: RichText,
|
||||||
|
//@ProtoNumber(2) var msgContent: ByteArray = EMPTY_BYTE_ARRAY,
|
||||||
|
//@/ProtoNumber(3) var msgEncryptContent: ByteArray = EMPTY_BYTE_ARRAY,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RichText(
|
||||||
|
//@ProtoNumber(1) var attr: Attr? = null,
|
||||||
|
@ProtoNumber(2) var elems: ArrayList<Elem>? = null,
|
||||||
|
//@ProtoNumber(3) var not_online_file: NotOnlineFile? = null,
|
||||||
|
//@ProtoNumber(4) var ptt: Ptt? = null,
|
||||||
|
//@ProtoNumber(5) var tmp_ptt: TmpPtt? = null,
|
||||||
|
//@ProtoNumber(6) var trans_211_tmp_msg: Trans211TmpMsg? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Elem(
|
||||||
|
/*@ProtoNumber(1) var text: TextMsg? = null,
|
||||||
|
@ProtoNumber(2) var face: FaceMsg? = null,
|
||||||
|
@ProtoNumber(3) var online_image: OnlineImage? = null,
|
||||||
|
@ProtoNumber(4) var not_online_image: NotOnlineImage? = null,
|
||||||
|
@ProtoNumber(5) var trans_elem_info: TransElem? = null,
|
||||||
|
@ProtoNumber(6) var market_face: MarketFace? = null,
|
||||||
|
@ProtoNumber(7) var elem_flags: ElemFlags? = null,
|
||||||
|
@ProtoNumber(8) var customFace: CustomFace? = null,
|
||||||
|
@ProtoNumber(9) var elem_flags2: ElemFlags2? = null,
|
||||||
|
@ProtoNumber(10) var fun_face: FunFace? = null,
|
||||||
|
@ProtoNumber(11) var secret_file: SecretFileMsg? = null,
|
||||||
|
@ProtoNumber(12) var rich_msg: RichMsg? = null,
|
||||||
|
@ProtoNumber(13) var group_file: GroupFile? = null,
|
||||||
|
@ProtoNumber(14) var pub_group: PubGroup? = null,
|
||||||
|
@ProtoNumber(15) var market_trans: MarketTrans? = null,
|
||||||
|
@ProtoNumber(16) var extra_info: ExtraInfo? = null,
|
||||||
|
@ProtoNumber(17) var shake_window: ShakeWindow? = null,
|
||||||
|
@ProtoNumber(18) var pub_account: PubAccount? = null,
|
||||||
|
@ProtoNumber(19) var video_file: VideoFile? = null,
|
||||||
|
@ProtoNumber(20) var tips_info: TipsInfo? = null,
|
||||||
|
@ProtoNumber(21) var anon_group_msg: AnonymousGroupMsg? = null,
|
||||||
|
@ProtoNumber(22) var qq_live_old: QQLiveOld? = null,
|
||||||
|
@ProtoNumber(23) var life_online: LifeOnlineAccount? = null,
|
||||||
|
@ProtoNumber(24) var qqwallet_msg: QQWalletMsg? = null,
|
||||||
|
@ProtoNumber(25) var crm_elem: CrmElem? = null,
|
||||||
|
@ProtoNumber(26) var conference_tips_info: ConferenceTipsInfo? = null,
|
||||||
|
@ProtoNumber(27) var redbag_info: RedBagInfo? = null,
|
||||||
|
@ProtoNumber(28) var low_version_tips: LowVersionTips? = null,
|
||||||
|
@ProtoNumber(29) var bankcode_ctrl_info: ByteArray = EMPTY_BYTE_ARRAY,
|
||||||
|
@ProtoNumber(30) var near_by_msg: NearByMessageType? = null,
|
||||||
|
@ProtoNumber(31) var custom_elem: CustomElem? = null,
|
||||||
|
@ProtoNumber(32) var location_info: LocationInfo? = null,
|
||||||
|
@ProtoNumber(33) var pub_acc_info: PubAccInfo? = null,
|
||||||
|
@ProtoNumber(34) var small_emoji: SmallEmoji? = null,
|
||||||
|
@ProtoNumber(35) var fsj_msg_elem: FSJMessageElem? = null,
|
||||||
|
@ProtoNumber(36) var ark_app: ArkAppElem? = null,
|
||||||
|
*/
|
||||||
|
@ProtoNumber(37) var generalFlags: GeneralFlags? = null,
|
||||||
|
/*
|
||||||
|
@ProtoNumber(38) var hc_flash_pic: CustomFace? = null,
|
||||||
|
@ProtoNumber(39) var deliver_gift_msg: DeliverGiftMsg? = null,
|
||||||
|
@ProtoNumber(40) var bitapp_msg: BitAppMsg? = null,
|
||||||
|
@ProtoNumber(41) var open_qq_data: OpenQQData? = null,
|
||||||
|
@ProtoNumber(42) var apollo_msg: ApolloActMsg? = null,
|
||||||
|
@ProtoNumber(43) var group_pub_acc_info: GroupPubAccountInfo? = null,
|
||||||
|
@ProtoNumber(44) var bless_msg: BlessingMessage? = null,
|
||||||
|
@ProtoNumber(45) var src_msg: SourceMsg? = null,
|
||||||
|
@ProtoNumber(46) var lola_msg: LolaMsg? = null,
|
||||||
|
@ProtoNumber(47) var group_business_msg: GroupBusinessMsg? = null,
|
||||||
|
@ProtoNumber(48) var msg_workflow_notify: WorkflowNotifyMsg? = null,
|
||||||
|
@ProtoNumber(49) var pat_elem: PatsElem? = null,
|
||||||
|
@ProtoNumber(50) var group_post_elem: GroupPostElem? = null,
|
||||||
|
@ProtoNumber(51) var light_app: LightAppElem? = null,
|
||||||
|
@ProtoNumber(52) var eim_info: EIMInfo? = null,
|
||||||
|
@ProtoNumber(53) var commonElem: CommonElem? = null,*/
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GeneralFlags(
|
||||||
|
@ProtoNumber(1) var uint32_bubble_diy_text_id: UInt = 0u,
|
||||||
|
@ProtoNumber(2) var uint32_group_flag_new: UInt = 0u,
|
||||||
|
@ProtoNumber(3) var uint64_uin: ULong = 0u,
|
||||||
|
@ProtoNumber(4) var bytes_rp_id: ByteArray = EMPTY_BYTE_ARRAY,
|
||||||
|
@ProtoNumber(5) var uint32_prp_fold: UInt = 0u,
|
||||||
|
@ProtoNumber(6) var long_text_flag: UInt = 0u,
|
||||||
|
@ProtoNumber(7) var long_text_resid: ByteArray = EMPTY_BYTE_ARRAY,
|
||||||
|
@ProtoNumber(8) var uint32_group_type: UInt = 0u,
|
||||||
|
@ProtoNumber(9) var uint32_to_uin_flag: UInt = 0u,
|
||||||
|
@ProtoNumber(10) var uint32_glamour_level: UInt = 0u,
|
||||||
|
@ProtoNumber(11) var uint32_member_level: UInt = 0u,
|
||||||
|
@ProtoNumber(12) var uint64_group_rank_seq: ULong = 0u,
|
||||||
|
@ProtoNumber(13) var uint32_olympic_torch: UInt = 0u,
|
||||||
|
@ProtoNumber(14) var babyq_guide_msg_cookie: ByteArray = EMPTY_BYTE_ARRAY,
|
||||||
|
@ProtoNumber(15) var uin32_expert_flag: UInt = 0u,
|
||||||
|
@ProtoNumber(16) var uint32_bubble_sub_id: UInt = 0u,
|
||||||
|
@ProtoNumber(17) var pendantId: ULong = 0u,
|
||||||
|
@ProtoNumber(18) var bytes_rp_index: ByteArray = EMPTY_BYTE_ARRAY,
|
||||||
|
@ProtoNumber(19) var reserve: ByteArray = EMPTY_BYTE_ARRAY,
|
||||||
|
)
|
66
protobuf/src/main/java/protobuf/msg/SendMessage.kt
Normal file
66
protobuf/src/main/java/protobuf/msg/SendMessage.kt
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package protobuf.msg
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PbSendMsgReq(
|
||||||
|
@ProtoNumber(1) var routingHead: RoutingHead,
|
||||||
|
@ProtoNumber(2) var contentHead: ContentHead,
|
||||||
|
@ProtoNumber(3) var msgBody: MsgBody,
|
||||||
|
@ProtoNumber(4) var msgSeq: ULong = 0u,
|
||||||
|
@ProtoNumber(5) var msgRand: UInt = 0u,
|
||||||
|
//@ProtoNumber(6) var sync_cookie: ByteArray = EMPTY_BYTE_ARRAY,
|
||||||
|
//@ProtoNumber(7) var app_share: AppShareInfo? = null,
|
||||||
|
@ProtoNumber(8) var msgVia: UInt = 0u,
|
||||||
|
//@ProtoNumber(9) var data_statist: UInt = 0u,
|
||||||
|
//@ProtoNumber(10) var multi_msg_assist: MultiMsgAssist? = null,
|
||||||
|
//@ProtoNumber(11) var input_notify_info: PbInputNotifyInfo? = null,
|
||||||
|
//@ProtoNumber(12) var msgCtrl: MsgCtrl? = null,
|
||||||
|
//@ProtoNumber(13) var receipt_req: ReceiptReq? = null,
|
||||||
|
//@ProtoNumber(14) var multi_send_seq: UInt = 0u,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ContentHead(
|
||||||
|
@ProtoNumber(1) var pkg_num: UInt = 0u,
|
||||||
|
@ProtoNumber(2) var pkg_index: UInt = 0u,
|
||||||
|
@ProtoNumber(3) var div_seq: UInt = 0u,
|
||||||
|
@ProtoNumber(4) var auto_reply: UInt = 0u,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class RoutingHead(
|
||||||
|
@ProtoNumber(1) var c2c: C2C? = null,
|
||||||
|
@ProtoNumber(2) var grp: Grp? = null,
|
||||||
|
// @ProtoNumber(3) var grp_tmp: GrpTmp? = null,
|
||||||
|
//@ProtoNumber(4) var dis: Dis? = null,
|
||||||
|
// @ProtoNumber(5) var dis_tmp: DisTmp? = null,
|
||||||
|
// @ProtoNumber(6) var wpa_tmp: WPATmp? = null,
|
||||||
|
// @ProtoNumber(7) var secret_file: SecretFileHead? = null,
|
||||||
|
// @ProtoNumber(8) var public_plat: PublicPlat? = null,
|
||||||
|
/*@ProtoNumber(9) var trans_msg: TransMsg? = null,
|
||||||
|
@ProtoNumber(10) var address_list: AddressListTmp? = null,
|
||||||
|
@ProtoNumber(11) var rich_status_tmp: RichStatusTmp? = null,
|
||||||
|
@ProtoNumber(12) var trans_cmd: TransCmd? = null,
|
||||||
|
@ProtoNumber(13) var accost_tmp: AccostTmp? = null,
|
||||||
|
@ProtoNumber(14) var pub_group_tmp: PubGroupTmp? = null,
|
||||||
|
@ProtoNumber(15) var trans_0x211: Trans0x211? = null,
|
||||||
|
@ProtoNumber(16) var business_wpa_tmp: BusinessWPATmp? = null,
|
||||||
|
@ProtoNumber(17) var auth_tmp: AuthTmp? = null,
|
||||||
|
@ProtoNumber(18) var bsns_tmp: BsnsTmp? = null,
|
||||||
|
@ProtoNumber(19) var qq_querybusiness_tmp: QQQueryBusinessTmp? = null,
|
||||||
|
@ProtoNumber(20) var nearby_dating_tmp: NearByDatingTmp? = null,
|
||||||
|
@ProtoNumber(21) var nearby_assistant_tmp: NearByAssistantTmp? = null,
|
||||||
|
@ProtoNumber(22) var comm_tmp: CommTmp? = null,*/
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class C2C(
|
||||||
|
@ProtoNumber(1) var to_uin: ULong = 0u,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Grp(
|
||||||
|
@ProtoNumber(1) var groupCode: ULong = 0u,
|
||||||
|
)
|
@ -2,11 +2,11 @@ package protobuf.push
|
|||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import protobuf.message.MessageBody
|
import protobuf.message.NtMessage
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MessagePush(
|
data class MessagePush(
|
||||||
@ProtoNumber(1) val msgBody: MessageBody? = null,
|
@ProtoNumber(1) val msgBody: NtMessage? = null,
|
||||||
@ProtoNumber(4) val clientInfo: MessagePushClientInfo? = null,
|
@ProtoNumber(4) val clientInfo: MessagePushClientInfo? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.tencent.guild.api.transfile;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.qroute.QRouteApi;
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.BigDataTicket;
|
||||||
|
|
||||||
|
public interface IGuildTransFileApi extends QRouteApi {
|
||||||
|
//@Nullable
|
||||||
|
//ArrayList<ServerAddress> getBigDataIpList(boolean z, @Nullable IpType ipType);
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
BigDataTicket getBigDataTicket();
|
||||||
|
|
||||||
|
//@Nullable
|
||||||
|
//ArrayList<ServerAddress> getIpDirectList(@Nullable String str, @Nullable IpType ipType);
|
||||||
|
|
||||||
|
void pullConfigIfNeed();
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.tencent.libra.download;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.tencent.libra.request.Option;
|
||||||
|
|
||||||
|
public interface ILibraDownloader {
|
||||||
|
class PicDownLoadListener {
|
||||||
|
Option mOption;
|
||||||
|
|
||||||
|
public PicDownLoadListener(@NonNull Option option) {
|
||||||
|
this.mOption = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onResult(boolean success, int code) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean canDownload(Option option);
|
||||||
|
|
||||||
|
void cancel(Option option);
|
||||||
|
|
||||||
|
void downLoad(Option option, PicDownLoadListener picDownLoadListener);
|
||||||
|
|
||||||
|
boolean needDownloadOnWorkThread();
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.tencent.libra.request;
|
||||||
|
|
||||||
|
public class Option {
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package com.tencent.mobileqq.perf.block;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import epic.EIPCClient;
|
||||||
|
import epic.EIPCResult;
|
||||||
|
import kotlin.Metadata;
|
||||||
|
import kotlin.jvm.JvmStatic;
|
||||||
|
|
||||||
|
public final class BinderMethodProxy {
|
||||||
|
@NotNull
|
||||||
|
public static final BinderMethodProxy INSTANCE;
|
||||||
|
|
||||||
|
static {
|
||||||
|
INSTANCE = new BinderMethodProxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
public static EIPCResult callServer(@NotNull EIPCClient client, @Nullable String module, @Nullable String action, @Nullable Bundle bundle) {
|
||||||
|
//MainBlockMethodMonitor.onMethodStart();
|
||||||
|
//EIPCResult callServer = client.callServer(str, str2, bundle);
|
||||||
|
//MainBlockMethodMonitor.onMethodEnd();
|
||||||
|
//return callServer;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.tencent.mobileqq.qipc;
|
||||||
|
|
||||||
|
import epic.EIPCClient;
|
||||||
|
|
||||||
|
public class QIPCClientHelper {
|
||||||
|
public static synchronized QIPCClientHelper getInstance() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EIPCClient getClient() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.tencent.mobileqq.qmmkv;
|
||||||
|
|
||||||
|
public class MMKVOptionEntity {
|
||||||
|
public String decodeString(String str, String str2) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.tencent.mobileqq.qmmkv;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class QMMKV {
|
||||||
|
public static MMKVOptionEntity from(Context context, String str) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.tencent.qqnt.aio.api;
|
||||||
|
|
||||||
|
import com.tencent.libra.download.ILibraDownloader;
|
||||||
|
import com.tencent.mobileqq.qroute.QRouteApi;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public interface IAIOPicDownloaderProvider extends QRouteApi {
|
||||||
|
@NotNull
|
||||||
|
ILibraDownloader provideDownloader();
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
package com.tencent.qqnt.kernel.api.impl;
|
package com.tencent.qqnt.kernel.api.impl;
|
||||||
|
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.IGetTempChatInfoCallback;
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener;
|
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener;
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
|
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.RichMediaElementGetReq;
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo;
|
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo;
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo;
|
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo;
|
||||||
|
|
||||||
@ -9,6 +11,10 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
public class MsgService {
|
public class MsgService {
|
||||||
|
void getRichMediaElement(@NotNull RichMediaElementGetReq req) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public void addMsgListener(IKernelMsgListener listener) {
|
public void addMsgListener(IKernelMsgListener listener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,4 +30,8 @@ public class MsgService {
|
|||||||
public void prepareTempChat(TempChatPrepareInfo tempChatPrepareInfo, IOperateCallback cb) {
|
public void prepareTempChat(TempChatPrepareInfo tempChatPrepareInfo, IOperateCallback cb) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void getTempChatInfo(int chatType, @Nullable String uid, @Nullable IGetTempChatInfoCallback cb) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
|
public final class BigDataTicket {
|
||||||
|
public String sessionKey;
|
||||||
|
public String sessionSig;
|
||||||
|
|
||||||
|
public BigDataTicket() {
|
||||||
|
this.sessionSig = "";
|
||||||
|
this.sessionKey = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionKey() {
|
||||||
|
return this.sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionSig() {
|
||||||
|
return this.sessionSig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return "BigDataTicket{sessionSig=" + this.sessionSig + ",sessionKey=" + this.sessionKey + ",}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDataTicket(String str, String str2) {
|
||||||
|
this.sessionSig = "";
|
||||||
|
this.sessionKey = "";
|
||||||
|
this.sessionSig = str;
|
||||||
|
this.sessionKey = str2;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
|
public interface IGetTempChatInfoCallback {
|
||||||
|
void onResult(int code, String msg, TempChatInfo info);
|
||||||
|
}
|
@ -79,6 +79,10 @@ public final class InlineKeyboardButton {
|
|||||||
return "InlineKeyboardButton{id=" + this.id + ",label=" + this.label + ",visitedLabel=" + this.visitedLabel + ",style=" + this.style + ",type=" + this.type + ",clickLimit=" + this.clickLimit + ",unsupportTips=" + this.unsupportTips + ",data=" + this.data + ",atBotShowChannelList=" + this.atBotShowChannelList + ",permissionType=" + this.permissionType + ",specifyRoleIds=" + this.specifyRoleIds + ",specifyTinyids=" + this.specifyTinyids + ",}";
|
return "InlineKeyboardButton{id=" + this.id + ",label=" + this.label + ",visitedLabel=" + this.visitedLabel + ",style=" + this.style + ",type=" + this.type + ",clickLimit=" + this.clickLimit + ",unsupportTips=" + this.unsupportTips + ",data=" + this.data + ",atBotShowChannelList=" + this.atBotShowChannelList + ",permissionType=" + this.permissionType + ",specifyRoleIds=" + this.specifyRoleIds + ",specifyTinyids=" + this.specifyTinyids + ",}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InlineKeyboardButton(String str, String str2, String str3, int i, int i2, int i3, String str4, String str5, boolean z, int i4, ArrayList<String> arrayList, ArrayList<String> arrayList2, boolean z2, int i5, boolean z3, ArrayList<SubscribeMsgTemplateID> arrayList3) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public InlineKeyboardButton(String str, String str2, String str3, int i2, int i3, int i4, String str4, String str5, boolean z, int i5, ArrayList<String> arrayList, ArrayList<String> arrayList2) {
|
public InlineKeyboardButton(String str, String str2, String str3, int i2, int i3, int i4, String str4, String str5, boolean z, int i5, ArrayList<String> arrayList, ArrayList<String> arrayList2) {
|
||||||
this.id = "";
|
this.id = "";
|
||||||
this.label = "";
|
this.label = "";
|
||||||
|
@ -264,6 +264,10 @@ public final class PicElement implements IKernelModel {
|
|||||||
this.transferStatus = num;
|
this.transferStatus = num;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getStoreID() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "PicElement{picSubType=" + this.picSubType + ",fileName=" + this.fileName + ",fileSize=" + this.fileSize + ",picWidth=" + this.picWidth + ",picHeight=" + this.picHeight + ",original=" + this.original + ",md5HexStr=" + this.md5HexStr + ",sourcePath=" + this.sourcePath + ",thumbPath=" + this.thumbPath + ",transferStatus=" + this.transferStatus + ",progress=" + this.progress + ",picType=" + this.picType + ",invalidState=" + this.invalidState + ",fileUuid=" + this.fileUuid + ",fileSubId=" + this.fileSubId + ",thumbFileSize=" + this.thumbFileSize + ",fileBizId=" + this.fileBizId + ",downloadIndex=" + this.downloadIndex + ",summary=" + this.summary + ",emojiFrom=" + this.emojiFrom + ",emojiWebUrl=" + this.emojiWebUrl + ",emojiAd=" + this.emojiAd + ",emojiMall=" + this.emojiMall + ",emojiZplan=" + this.emojiZplan + ",originImageMd5=" + this.originImageMd5 + ",originImageUrl=" + this.originImageUrl + ",importRichMediaContext=" + this.importRichMediaContext + ",isFlashPic=" + this.isFlashPic + ",}";
|
return "PicElement{picSubType=" + this.picSubType + ",fileName=" + this.fileName + ",fileSize=" + this.fileSize + ",picWidth=" + this.picWidth + ",picHeight=" + this.picHeight + ",original=" + this.original + ",md5HexStr=" + this.md5HexStr + ",sourcePath=" + this.sourcePath + ",thumbPath=" + this.thumbPath + ",transferStatus=" + this.transferStatus + ",progress=" + this.progress + ",picType=" + this.picType + ",invalidState=" + this.invalidState + ",fileUuid=" + this.fileUuid + ",fileSubId=" + this.fileSubId + ",thumbFileSize=" + this.thumbFileSize + ",fileBizId=" + this.fileBizId + ",downloadIndex=" + this.downloadIndex + ",summary=" + this.summary + ",emojiFrom=" + this.emojiFrom + ",emojiWebUrl=" + this.emojiWebUrl + ",emojiAd=" + this.emojiAd + ",emojiMall=" + this.emojiMall + ",emojiZplan=" + this.emojiZplan + ",originImageMd5=" + this.originImageMd5 + ",originImageUrl=" + this.originImageUrl + ",importRichMediaContext=" + this.importRichMediaContext + ",isFlashPic=" + this.isFlashPic + ",}";
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,118 @@
|
|||||||
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
|
public final class RichMediaElementGetReq implements IKernelModel {
|
||||||
|
public int chatType;
|
||||||
|
public int downSourceType;
|
||||||
|
public int downloadType;
|
||||||
|
public long elementId;
|
||||||
|
public long fileModelId;
|
||||||
|
public String filePath;
|
||||||
|
public long msgId;
|
||||||
|
public String peerUid;
|
||||||
|
public int thumbSize;
|
||||||
|
public int triggerType;
|
||||||
|
|
||||||
|
public RichMediaElementGetReq() {
|
||||||
|
this.peerUid = "";
|
||||||
|
this.filePath = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getChatType() {
|
||||||
|
return this.chatType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDownSourceType() {
|
||||||
|
return this.downSourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDownloadType() {
|
||||||
|
return this.downloadType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getElementId() {
|
||||||
|
return this.elementId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFileModelId() {
|
||||||
|
return this.fileModelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFilePath() {
|
||||||
|
return this.filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMsgId() {
|
||||||
|
return this.msgId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPeerUid() {
|
||||||
|
return this.peerUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getThumbSize() {
|
||||||
|
return this.thumbSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTriggerType() {
|
||||||
|
return this.triggerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatType(int i2) {
|
||||||
|
this.chatType = i2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDownSourceType(int i2) {
|
||||||
|
this.downSourceType = i2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDownloadType(int i2) {
|
||||||
|
this.downloadType = i2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setElementId(long j2) {
|
||||||
|
this.elementId = j2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileModelId(long j2) {
|
||||||
|
this.fileModelId = j2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilePath(String str) {
|
||||||
|
this.filePath = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMsgId(long j2) {
|
||||||
|
this.msgId = j2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPeerUid(String str) {
|
||||||
|
this.peerUid = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setThumbSize(int i2) {
|
||||||
|
this.thumbSize = i2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTriggerType(int i2) {
|
||||||
|
this.triggerType = i2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return "RichMediaElementGetReq{msgId=" + this.msgId + ",peerUid=" + this.peerUid + ",chatType=" + this.chatType + ",elementId=" + this.elementId + ",downloadType=" + this.downloadType + ",thumbSize=" + this.thumbSize + ",filePath=" + this.filePath + ",fileModelId=" + this.fileModelId + ",downSourceType=" + this.downSourceType + ",triggerType=" + this.triggerType + ",}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public RichMediaElementGetReq(long j2, String str, int i2, long j3, int i3, int i4, String str2, long j4, int i5, int i6) {
|
||||||
|
this.peerUid = "";
|
||||||
|
this.filePath = "";
|
||||||
|
this.msgId = j2;
|
||||||
|
this.peerUid = str;
|
||||||
|
this.chatType = i2;
|
||||||
|
this.elementId = j3;
|
||||||
|
this.downloadType = i3;
|
||||||
|
this.thumbSize = i4;
|
||||||
|
this.filePath = str2;
|
||||||
|
this.fileModelId = j4;
|
||||||
|
this.downSourceType = i5;
|
||||||
|
this.triggerType = i6;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
|
public class SubscribeMsgTemplateID {
|
||||||
|
}
|
4
qqinterface/src/main/java/epic/EIPCClient.java
Normal file
4
qqinterface/src/main/java/epic/EIPCClient.java
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package epic;
|
||||||
|
|
||||||
|
public class EIPCClient {
|
||||||
|
}
|
11
qqinterface/src/main/java/epic/EIPCResult.java
Normal file
11
qqinterface/src/main/java/epic/EIPCResult.java
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package epic;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public class EIPCResult {
|
||||||
|
public Bundle data;
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -31,10 +31,24 @@ public class oidb_cmd0xb77 {
|
|||||||
// public final PBUInt32Field service_id = PBField.initUInt32(0);
|
// public final PBUInt32Field service_id = PBField.initUInt32(0);
|
||||||
// public final PBStringField xml = PBField.initString("");
|
// public final PBStringField xml = PBField.initString("");
|
||||||
//};
|
//};
|
||||||
//public oidb_cmd0xb77$MiniAppMsgBody mini_app_msg_body = new oidb_cmd0xb77$MiniAppMsgBody();
|
public MiniAppMsgBody mini_app_msg_body = new MiniAppMsgBody();
|
||||||
public final PBUInt64Field recv_guild_id = PBField.initUInt64(0);
|
public final PBUInt64Field recv_guild_id = PBField.initUInt64(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class MiniAppMsgBody extends MessageMicro<MiniAppMsgBody> {
|
||||||
|
//static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(
|
||||||
|
// new int[]{8, 18, 26, 32, 42, 50, 82}, new String[]{"mini_app_appid", "mini_app_path", "web_page_url", "mini_app_type", "title", "desc", "json_str"}
|
||||||
|
|
||||||
|
//, new Object[]{0L, "", "", 0, "", "", ""}, oidb_cmd0xb77$MiniAppMsgBody.class);
|
||||||
|
public final PBUInt64Field mini_app_appid = PBField.initUInt64(0);
|
||||||
|
public final PBStringField mini_app_path = PBField.initString("");
|
||||||
|
public final PBStringField web_page_url = PBField.initString("");
|
||||||
|
public final PBUInt32Field mini_app_type = PBField.initUInt32(0);
|
||||||
|
public final PBStringField title = PBField.initString("");
|
||||||
|
public final PBStringField desc = PBField.initString("");
|
||||||
|
public final PBStringField json_str = PBField.initString("");
|
||||||
|
}
|
||||||
|
|
||||||
public static class ArkMsgBody extends MessageMicro<ArkMsgBody> {
|
public static class ArkMsgBody extends MessageMicro<ArkMsgBody> {
|
||||||
public final PBStringField app = PBField.initString("");
|
public final PBStringField app = PBField.initString("");
|
||||||
public final PBStringField view = PBField.initString("");
|
public final PBStringField view = PBField.initString("");
|
||||||
|
@ -11,9 +11,10 @@ import io.ktor.utils.io.core.BytePacketBuilder
|
|||||||
import io.ktor.utils.io.core.readBytes
|
import io.ktor.utils.io.core.readBytes
|
||||||
import io.ktor.utils.io.core.writeFully
|
import io.ktor.utils.io.core.writeFully
|
||||||
import io.ktor.utils.io.core.writeInt
|
import io.ktor.utils.io.core.writeInt
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
@ -28,10 +29,11 @@ import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest
|
|||||||
import protobuf.oidb.TrpcOidb
|
import protobuf.oidb.TrpcOidb
|
||||||
import mqq.app.MobileQQ
|
import mqq.app.MobileQQ
|
||||||
import tencent.im.oidb.oidb_sso
|
import tencent.im.oidb.oidb_sso
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
internal abstract class BaseSvc {
|
internal abstract class BaseSvc {
|
||||||
companion object {
|
companion object Default: CoroutineScope {
|
||||||
val currentUin: String
|
val currentUin: String
|
||||||
get() = app.currentAccountUin
|
get() = app.currentAccountUin
|
||||||
|
|
||||||
@ -46,7 +48,7 @@ internal abstract class BaseSvc {
|
|||||||
val seq = MsfCore.getNextSeq()
|
val seq = MsfCore.getNextSeq()
|
||||||
val buffer = withTimeoutOrNull(timeout) {
|
val buffer = withTimeoutOrNull(timeout) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
launch(Dispatchers.Default) {
|
||||||
DynamicReceiver.register(IPCRequest(cmd, seq) {
|
DynamicReceiver.register(IPCRequest(cmd, seq) {
|
||||||
val buffer = it.getByteArrayExtra("buffer")!!
|
val buffer = it.getByteArrayExtra("buffer")!!
|
||||||
continuation.resume(buffer)
|
continuation.resume(buffer)
|
||||||
@ -75,7 +77,7 @@ internal abstract class BaseSvc {
|
|||||||
val seq = MsfCore.getNextSeq()
|
val seq = MsfCore.getNextSeq()
|
||||||
val buffer = withTimeoutOrNull<ByteArray?>(timeout) {
|
val buffer = withTimeoutOrNull<ByteArray?>(timeout) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
launch(Dispatchers.Default) {
|
||||||
DynamicReceiver.register(IPCRequest(cmd, seq) {
|
DynamicReceiver.register(IPCRequest(cmd, seq) {
|
||||||
val buffer = it.getByteArrayExtra("buffer")!!
|
val buffer = it.getByteArrayExtra("buffer")!!
|
||||||
continuation.resume(buffer)
|
continuation.resume(buffer)
|
||||||
@ -143,6 +145,11 @@ internal abstract class BaseSvc {
|
|||||||
toServiceMsg.addAttribute("shamrock_seq", seq)
|
toServiceMsg.addAttribute("shamrock_seq", seq)
|
||||||
app.sendToService(toServiceMsg)
|
app.sendToService(toServiceMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
override val coroutineContext: CoroutineContext by lazy {
|
||||||
|
Dispatchers.IO.limitedParallelism(12)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun send(toServiceMsg: ToServiceMsg) {
|
protected fun send(toServiceMsg: ToServiceMsg) {
|
||||||
@ -153,7 +160,7 @@ internal abstract class BaseSvc {
|
|||||||
val seq = MsfCore.getNextSeq()
|
val seq = MsfCore.getNextSeq()
|
||||||
val buffer = withTimeoutOrNull<ByteArray?>(timeout) {
|
val buffer = withTimeoutOrNull<ByteArray?>(timeout) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
launch(Dispatchers.Default) {
|
||||||
DynamicReceiver.register(IPCRequest(toServiceMsg.serviceCmd, seq) {
|
DynamicReceiver.register(IPCRequest(toServiceMsg.serviceCmd, seq) {
|
||||||
val buffer = it.getByteArrayExtra("buffer")!!
|
val buffer = it.getByteArrayExtra("buffer")!!
|
||||||
continuation.resume(buffer)
|
continuation.resume(buffer)
|
||||||
|
@ -59,6 +59,7 @@ import moe.fuqiuluo.shamrock.remote.service.data.EssenceMessage
|
|||||||
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncement
|
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncement
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessage
|
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessage
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessageImage
|
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessageImage
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.data.push.MemberRole
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonArray
|
import moe.fuqiuluo.shamrock.tools.EmptyJsonArray
|
||||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
||||||
import moe.fuqiuluo.shamrock.tools.asInt
|
import moe.fuqiuluo.shamrock.tools.asInt
|
||||||
@ -96,6 +97,8 @@ import java.nio.ByteBuffer
|
|||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
internal object GroupSvc: BaseSvc() {
|
internal object GroupSvc: BaseSvc() {
|
||||||
|
private const val GET_MEMBER_ROLE_BY_NT = false
|
||||||
|
|
||||||
private val RefreshTroopMemberInfoLock by lazy {
|
private val RefreshTroopMemberInfoLock by lazy {
|
||||||
Mutex()
|
Mutex()
|
||||||
}
|
}
|
||||||
@ -394,6 +397,27 @@ internal object GroupSvc: BaseSvc() {
|
|||||||
.filter { it != 0L }
|
.filter { it != 0L }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMemberRole(groupId: Long, memberUin: Long): MemberRole {
|
||||||
|
if (!GET_MEMBER_ROLE_BY_NT) {
|
||||||
|
return when (memberUin) {
|
||||||
|
getOwner(groupId.toString()) -> MemberRole.Owner
|
||||||
|
in getAdminList(groupId.toString()) -> MemberRole.Admin
|
||||||
|
else -> MemberRole.Member
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return when(getTroopMemberInfoByUinViaNt(groupId.toString(), memberUin, 3000).getOrNull()?.role) {
|
||||||
|
com.tencent.qqnt.kernel.nativeinterface.MemberRole.STRANGER -> MemberRole.Stranger
|
||||||
|
com.tencent.qqnt.kernel.nativeinterface.MemberRole.MEMBER -> MemberRole.Member
|
||||||
|
com.tencent.qqnt.kernel.nativeinterface.MemberRole.ADMIN -> MemberRole.Admin
|
||||||
|
com.tencent.qqnt.kernel.nativeinterface.MemberRole.OWNER -> MemberRole.Owner
|
||||||
|
com.tencent.qqnt.kernel.nativeinterface.MemberRole.UNSPECIFIED, null -> when (memberUin) {
|
||||||
|
getOwner(groupId.toString()) -> MemberRole.Owner
|
||||||
|
in getAdminList(groupId.toString()) -> MemberRole.Admin
|
||||||
|
else -> MemberRole.Member
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getOwner(groupId: String): Long {
|
fun getOwner(groupId: String): Long {
|
||||||
val groupInfo = getGroupInfo(groupId)
|
val groupInfo = getGroupInfo(groupId)
|
||||||
return groupInfo.troopowneruin?.toLong() ?: 0
|
return groupInfo.troopowneruin?.toLong() ?: 0
|
||||||
@ -566,24 +590,53 @@ internal object GroupSvc: BaseSvc() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getTroopMemberInfoByUinViaNt(groupId: String, qq: Long): Result<MemberInfo> {
|
suspend fun getTroopMemberInfoByUinV2(
|
||||||
val kernelService = NTServiceFetcher.kernelService
|
groupId: String,
|
||||||
val sessionService = kernelService.wrapperSession
|
uin: String,
|
||||||
val groupService = sessionService.groupService
|
refresh: Boolean = false
|
||||||
val info = suspendCancellableCoroutine {
|
): Result<TroopMemberInfo> {
|
||||||
groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data ->
|
val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
|
||||||
if (code != 0) {
|
var info = service.getTroopMember(groupId, uin)
|
||||||
it.resume(null)
|
if (refresh || !service.isMemberInCache(groupId, uin) || info == null || info.troopnick == null) {
|
||||||
return@getTransferableMemberInfo
|
info = requestTroopMemberInfo(service, groupId.toLong(), uin.toLong(), timeout = 2000).getOrNull()
|
||||||
|
}
|
||||||
|
if (info == null) {
|
||||||
|
info = getTroopMemberInfoByUinViaNt(groupId, uin.toLong(), timeout = 2000L).getOrNull()?.let {
|
||||||
|
TroopMemberInfo().apply {
|
||||||
|
troopnick = it.cardName
|
||||||
|
friendnick = it.nick
|
||||||
}
|
}
|
||||||
data.forEach { (_, info) ->
|
}
|
||||||
if (info.uin == qq) {
|
}
|
||||||
it.resume(info)
|
try {
|
||||||
return@forEach
|
if (info != null && (info.alias == null || info.alias.isBlank())) {
|
||||||
|
val req = group_member_info.ReqBody()
|
||||||
|
req.uint64_group_code.set(groupId.toLong())
|
||||||
|
req.uint64_uin.set(uin.toLong())
|
||||||
|
req.bool_new_client.set(true)
|
||||||
|
req.uint32_client_type.set(1)
|
||||||
|
req.uint32_rich_card_name_ver.set(1)
|
||||||
|
val respBuffer = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray(), timeout = 2000)
|
||||||
|
if (respBuffer != null) {
|
||||||
|
val rsp = group_member_info.RspBody()
|
||||||
|
rsp.mergeFrom(respBuffer.slice(4))
|
||||||
|
if (rsp.msg_meminfo.str_location.has()) {
|
||||||
|
info.alias = rsp.msg_meminfo.str_location.get().toStringUtf8()
|
||||||
|
}
|
||||||
|
if (rsp.msg_meminfo.uint32_age.has()) {
|
||||||
|
info.age = rsp.msg_meminfo.uint32_age.get().toByte()
|
||||||
|
}
|
||||||
|
if (rsp.msg_meminfo.bytes_group_honor.has()) {
|
||||||
|
val honorBytes = rsp.msg_meminfo.bytes_group_honor.get().toByteArray()
|
||||||
|
val honor = troop_honor.GroupUserCardHonor()
|
||||||
|
honor.mergeFrom(honorBytes)
|
||||||
|
info.level = honor.level.get()
|
||||||
|
// 10315: medal_id not real group level
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
it.resume(null)
|
|
||||||
}
|
}
|
||||||
|
} catch (err: Throwable) {
|
||||||
|
LogCenter.log(err.stackTraceToString(), Level.WARN)
|
||||||
}
|
}
|
||||||
return if (info != null) {
|
return if (info != null) {
|
||||||
Result.success(info)
|
Result.success(info)
|
||||||
@ -592,6 +645,40 @@ internal object GroupSvc: BaseSvc() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getTroopMemberInfoByUinViaNt(
|
||||||
|
groupId: String,
|
||||||
|
qq: Long,
|
||||||
|
timeout: Long = 5000L
|
||||||
|
): Result<MemberInfo> {
|
||||||
|
return runCatching {
|
||||||
|
val kernelService = NTServiceFetcher.kernelService
|
||||||
|
val sessionService = kernelService.wrapperSession
|
||||||
|
val groupService = sessionService.groupService
|
||||||
|
val info = withTimeoutOrNull(timeout) {
|
||||||
|
suspendCancellableCoroutine {
|
||||||
|
groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data ->
|
||||||
|
if (code != 0) {
|
||||||
|
it.resume(null)
|
||||||
|
return@getTransferableMemberInfo
|
||||||
|
}
|
||||||
|
data.forEach { (_, info) ->
|
||||||
|
if (info.uin == qq) {
|
||||||
|
it.resume(info)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (info != null) {
|
||||||
|
Result.success(info)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("获取群成员信息失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getTroopMemberInfoByUid(groupId: Long, uid: String): Result<MemberInfo> {
|
suspend fun getTroopMemberInfoByUid(groupId: Long, uid: String): Result<MemberInfo> {
|
||||||
val kernelService = NTServiceFetcher.kernelService
|
val kernelService = NTServiceFetcher.kernelService
|
||||||
val sessionService = kernelService.wrapperSession
|
val sessionService = kernelService.wrapperSession
|
||||||
@ -748,7 +835,7 @@ internal object GroupSvc: BaseSvc() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long): Result<TroopMemberInfo> {
|
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long, timeout: Long = 10_000): Result<TroopMemberInfo> {
|
||||||
val info = RefreshTroopMemberInfoLock.withLock {
|
val info = RefreshTroopMemberInfoLock.withLock {
|
||||||
val groupIdStr = groupId.toString()
|
val groupIdStr = groupId.toString()
|
||||||
val memberUinStr = memberUin.toString()
|
val memberUinStr = memberUin.toString()
|
||||||
@ -758,7 +845,7 @@ internal object GroupSvc: BaseSvc() {
|
|||||||
requestMemberInfoV2(groupId, memberUin)
|
requestMemberInfoV2(groupId, memberUin)
|
||||||
requestMemberInfo(groupId, memberUin)
|
requestMemberInfo(groupId, memberUin)
|
||||||
|
|
||||||
withTimeoutOrNull(10000) {
|
withTimeoutOrNull(timeout) {
|
||||||
while (!service.isMemberInCache(groupIdStr, memberUinStr)) {
|
while (!service.isMemberInCache(groupIdStr, memberUinStr)) {
|
||||||
delay(200)
|
delay(200)
|
||||||
}
|
}
|
||||||
|
@ -5,33 +5,34 @@ package moe.fuqiuluo.qqinterface.servlet
|
|||||||
import com.tencent.mobileqq.qroute.QRoute
|
import com.tencent.mobileqq.qroute.QRoute
|
||||||
import com.tencent.mobileqq.troop.api.ITroopMemberNameService
|
import com.tencent.mobileqq.troop.api.ITroopMemberNameService
|
||||||
import com.tencent.qqnt.kernel.api.IKernelService
|
import com.tencent.qqnt.kernel.api.IKernelService
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback
|
import com.tencent.qqnt.kernel.nativeinterface.*
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo
|
|
||||||
import com.tencent.qqnt.msg.api.IMsgService
|
import com.tencent.qqnt.msg.api.IMsgService
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import kotlinx.serialization.decodeFromByteArray
|
||||||
|
import kotlinx.serialization.encodeToByteArray
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.msg.messageelement.toSegments
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.msg.toListMap
|
||||||
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||||
import moe.fuqiuluo.shamrock.helper.SendMsgException
|
import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.data.MessageSender
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
|
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
|
||||||
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
|
import moe.fuqiuluo.shamrock.tools.*
|
||||||
|
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.msgService
|
import moe.fuqiuluo.shamrock.xposed.helper.msgService
|
||||||
import java.util.UUID
|
import protobuf.message.longmsg.*
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
internal object MsgSvc: BaseSvc() {
|
internal object MsgSvc : BaseSvc() {
|
||||||
suspend fun prepareTempChatFromGroup(
|
suspend fun prepareTempChatFromGroup(
|
||||||
groupId: String,
|
groupId: String,
|
||||||
peerId: String
|
peerId: String
|
||||||
@ -39,13 +40,19 @@ internal object MsgSvc: BaseSvc() {
|
|||||||
LogCenter.log("主动临时消息,创建临时会话。", Level.INFO)
|
LogCenter.log("主动临时消息,创建临时会话。", Level.INFO)
|
||||||
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
|
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
|
||||||
?: return Result.failure(Exception("获取消息服务失败"))
|
?: return Result.failure(Exception("获取消息服务失败"))
|
||||||
msgService.prepareTempChat(TempChatPrepareInfo(
|
msgService.prepareTempChat(
|
||||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
|
TempChatPrepareInfo(
|
||||||
ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
|
MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
|
||||||
app.getRuntimeService(ITroopMemberNameService::class.java, "all")
|
ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
|
||||||
.getTroopMemberNameRemarkFirst(groupId, peerId),
|
app.getRuntimeService(ITroopMemberNameService::class.java, "all")
|
||||||
groupId, EMPTY_BYTE_ARRAY, app.currentUid, "", TempChatGameSession()
|
.getTroopMemberNameRemarkFirst(groupId, peerId),
|
||||||
)) { code, reason ->
|
groupId,
|
||||||
|
EMPTY_BYTE_ARRAY,
|
||||||
|
app.currentUid,
|
||||||
|
"",
|
||||||
|
TempChatGameSession()
|
||||||
|
)
|
||||||
|
) { code, reason ->
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR)
|
LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR)
|
||||||
}
|
}
|
||||||
@ -53,6 +60,24 @@ internal object MsgSvc: BaseSvc() {
|
|||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getTempChatInfo(chatType: Int, uid: String): Result<TempChatInfo> {
|
||||||
|
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
|
||||||
|
?: return Result.failure(Exception("获取消息服务失败"))
|
||||||
|
val info: TempChatInfo = withTimeoutOrNull(5000) {
|
||||||
|
suspendCancellableCoroutine {
|
||||||
|
msgService.getTempChatInfo(chatType, uid) { code, msg, tempChatInfo ->
|
||||||
|
if (code == 0) {
|
||||||
|
it.resume(tempChatInfo)
|
||||||
|
} else {
|
||||||
|
LogCenter.log("获取临时会话信息失败: $code:$msg", Level.ERROR)
|
||||||
|
it.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: return Result.failure(Exception("获取临时会话信息失败"))
|
||||||
|
return Result.success(info)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 正常获取
|
* 正常获取
|
||||||
*/
|
*/
|
||||||
@ -61,7 +86,7 @@ internal object MsgSvc: BaseSvc() {
|
|||||||
?: return Result.failure(Exception("没有对应消息映射,消息获取失败"))
|
?: return Result.failure(Exception("没有对应消息映射,消息获取失败"))
|
||||||
|
|
||||||
val peerId = mapping.peerId
|
val peerId = mapping.peerId
|
||||||
val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId ?: "")
|
val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId)
|
||||||
|
|
||||||
val msg = withTimeoutOrNull(5000) {
|
val msg = withTimeoutOrNull(5000) {
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
val service = QRoute.api(IMsgService::class.java)
|
||||||
@ -152,7 +177,7 @@ internal object MsgSvc: BaseSvc() {
|
|||||||
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
|
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
|
||||||
?: return -1 to "无法找到消息映射"
|
?: return -1 to "无法找到消息映射"
|
||||||
|
|
||||||
val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId ?: "")
|
val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId)
|
||||||
|
|
||||||
return suspendCancellableCoroutine { continuation ->
|
return suspendCancellableCoroutine { continuation ->
|
||||||
msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why ->
|
msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why ->
|
||||||
@ -182,9 +207,10 @@ internal object MsgSvc: BaseSvc() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val result = MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
|
val result =
|
||||||
result.onFailure {
|
MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
|
||||||
LogCenter.log("sendToAio: " + it.stackTraceToString(), Level.ERROR)
|
if (result.isFailure) {
|
||||||
|
LogCenter.log("sendToAio: " + result.exceptionOrNull()?.stackTraceToString(), Level.ERROR)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
val sendResult = result.getOrThrow()
|
val sendResult = result.getOrThrow()
|
||||||
@ -198,48 +224,109 @@ internal object MsgSvc: BaseSvc() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMultiMsg(resId: String): Result<List<MsgRecord>> {
|
suspend fun sendMultiMsg(
|
||||||
val kernelService = NTServiceFetcher.kernelService
|
uid: String,
|
||||||
val sessionService = kernelService.wrapperSession
|
groupUin: String?,
|
||||||
val msgService = sessionService.msgService
|
messages: List<PushMsgBody>,
|
||||||
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin())
|
): Result<String> {
|
||||||
|
val payload = LongMsgPayload(
|
||||||
|
action = listOf(
|
||||||
|
LongMsgAction(
|
||||||
|
command = "MultiMsg",
|
||||||
|
data = LongMsgContent(
|
||||||
|
body = messages
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
LogCenter.log(ProtoBuf.encodeToByteArray(payload).toHexString(), Level.DEBUG)
|
||||||
|
|
||||||
val content = "{\"app\":\"com.tencent.multimsg\",\"config\":{\"autosize\":1,\"forward\":1,\"round\":1,\"type\":\"normal\",\"width\":300},\"desc\":\"[聊天记录]\",\"extra\":\"\",\"meta\":{\"detail\":{\"news\":[{\"text\":\"Shamrock: 这是条假消息!\"}],\"resid\":\"$resId\",\"source\":\"聊天记录\",\"summary\":\"转发消息\",\"uniseq\":\"${UUID.randomUUID()}\"}},\"prompt\":\"[聊天记录]\",\"ver\":\"0.0.0.5\",\"view\":\"contact\"}"
|
val req = LongMsgReq(
|
||||||
val msgId = PacketSvc.fakeSelfRecvJsonMsg(msgService, content)
|
sendInfo = SendLongMsgInfo(
|
||||||
if (msgId < 0) {
|
type = if (groupUin == null) 1 else 3,
|
||||||
return Result.failure(Exception("获取合并转发消息ID失败"))
|
uid = LongMsgUid(groupUin ?: uid),
|
||||||
}
|
groupUin = groupUin?.toInt(),
|
||||||
val msgList = withTimeoutOrNull(5000L) {
|
payload = DeflateTools.gzip(ProtoBuf.encodeToByteArray(payload))
|
||||||
suspendCancellableCoroutine<ArrayList<MsgRecord>> {
|
),
|
||||||
val job = GlobalScope.launch {
|
setting = LongMsgSettings(
|
||||||
var hasResult = false
|
field1 = 4,
|
||||||
while (!hasResult) {
|
field2 = 2,
|
||||||
msgService.getMultiMsg(contact, msgId, msgId) { code, why, msgList ->
|
field3 = 9,
|
||||||
if (code == 0) {
|
field4 = 0
|
||||||
it.resume(msgList)
|
)
|
||||||
hasResult = true
|
)
|
||||||
} else {
|
val buffer = sendBufferAW(
|
||||||
LogCenter.log("获取合并转发消息失败: $code($why): $msgId", Level.ERROR)
|
"trpc.group.long_msg_interface.MsgService.SsoSendLongMsg",
|
||||||
}
|
true,
|
||||||
}
|
ProtoBuf.encodeToByteArray(req)
|
||||||
delay(200)
|
) ?: return Result.failure(Exception("unable to upload multi message"))
|
||||||
}
|
val rsp = ProtoBuf.decodeFromByteArray<LongMsgRsp>(buffer.slice(4))
|
||||||
}
|
return rsp.sendResult?.resId?.let { Result.success(it) }
|
||||||
it.invokeOnCancellation {
|
?: Result.failure(Exception("unable to upload multi message"))
|
||||||
job.cancel()
|
}
|
||||||
|
|
||||||
|
suspend fun getMultiMsg(resId: String): Result<List<MessageDetail>> {
|
||||||
|
val req = LongMsgReq(
|
||||||
|
recvInfo = RecvLongMsgInfo(
|
||||||
|
uid = LongMsgUid(TicketSvc.getUid()),
|
||||||
|
resId = resId,
|
||||||
|
u1 = 3
|
||||||
|
),
|
||||||
|
setting = LongMsgSettings(
|
||||||
|
field1 = 2,
|
||||||
|
field2 = 2,
|
||||||
|
field3 = 9,
|
||||||
|
field4 = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val buffer = sendBufferAW(
|
||||||
|
"trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg",
|
||||||
|
true,
|
||||||
|
ProtoBuf.encodeToByteArray(req)
|
||||||
|
) ?: return Result.failure(Exception("unable to get multi message"))
|
||||||
|
val rsp = ProtoBuf.decodeFromByteArray<LongMsgRsp>(buffer.slice(4))
|
||||||
|
val zippedPayload = DeflateTools.ungzip(
|
||||||
|
rsp.recvResult?.payload ?: return Result.failure(Exception("unable to get multi message"))
|
||||||
|
)
|
||||||
|
LogCenter.log(zippedPayload.toHexString(), Level.DEBUG)
|
||||||
|
val payload = ProtoBuf.decodeFromByteArray<LongMsgPayload>(zippedPayload)
|
||||||
|
payload.action?.forEach {
|
||||||
|
if (it.command == "MultiMsg") {
|
||||||
|
return Result.success(it.data?.body?.map { msg ->
|
||||||
|
val chatType =
|
||||||
|
if (msg.content!!.msgType == 82) MsgConstant.KCHATTYPEGROUP else MsgConstant.KCHATTYPEC2C
|
||||||
|
MessageDetail(
|
||||||
|
time = msg.content?.msgTime?.toInt() ?: 0,
|
||||||
|
msgType = MessageHelper.obtainDetailTypeByMsgType(chatType),
|
||||||
|
msgId = 0, // MessageHelper.generateMsgIdHash(chatType, msg.content!!.msgViaRandom), msgViaRandom 为空
|
||||||
|
realId = msg.content!!.msgSeq?.toInt() ?: 0,
|
||||||
|
sender = MessageSender(
|
||||||
|
msg.head?.peer ?: 0,
|
||||||
|
msg.head?.groupInfo?.memberCard?.ifEmpty { msg.head?.forward?.friendName }
|
||||||
|
?: msg.head?.forward?.friendName ?: "",
|
||||||
|
"unknown",
|
||||||
|
0,
|
||||||
|
msg.head?.peerUid ?: "",
|
||||||
|
msg.head?.peerUid ?: ""
|
||||||
|
),
|
||||||
|
message = msg.body?.rich?.elements?.toSegments(chatType, msg.head?.peer.toString(), "0")
|
||||||
|
?.toListMap() ?: emptyList(),
|
||||||
|
peerId = msg.head?.peer ?: 0,
|
||||||
|
groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.head?.groupInfo?.groupCode?.toLong()
|
||||||
|
?: 0 else 0,
|
||||||
|
targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.head?.peer ?: 0 else 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
?: return Result.failure(Exception("Msg is empty")))
|
||||||
}
|
}
|
||||||
} ?: return Result.failure(Exception("获取合并转发消息失败"))
|
}
|
||||||
|
return Result.failure(Exception("Can't find msg"))
|
||||||
//msgService.deleteMsg(contact, arrayListOf(msgId), null)
|
|
||||||
|
|
||||||
return Result.success(msgList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageCallback(
|
class MessageCallback(
|
||||||
private val peerId: String,
|
private val peerId: String,
|
||||||
var msgHash: Int
|
var msgHash: Int
|
||||||
): IOperateCallback {
|
) : IOperateCallback {
|
||||||
override fun onResult(code: Int, reason: String?) {
|
override fun onResult(code: Int, reason: String?) {
|
||||||
if (code != 0 && msgHash != 0) {
|
if (code != 0 && msgHash != 0) {
|
||||||
MessageHelper.removeMsgByHashCode(msgHash)
|
MessageHelper.removeMsgByHashCode(msgHash)
|
||||||
|
@ -15,13 +15,13 @@ import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg
|
|||||||
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
|
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
|
||||||
import moe.fuqiuluo.shamrock.tools.broadcast
|
import moe.fuqiuluo.shamrock.tools.broadcast
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||||
import protobuf.message.JsonElement
|
import protobuf.message.element.JsonElement
|
||||||
import protobuf.message.MessageBody
|
import protobuf.message.NtMessage
|
||||||
import protobuf.message.MessageContentHead
|
import protobuf.message.MessageContent
|
||||||
import protobuf.message.MessageElement
|
import protobuf.message.MessageElement
|
||||||
import protobuf.message.MessageElementList
|
|
||||||
import protobuf.message.MessageHead
|
|
||||||
import protobuf.message.RichMessage
|
import protobuf.message.RichMessage
|
||||||
|
import protobuf.message.MessageHead
|
||||||
|
import protobuf.message.MessageBody
|
||||||
import protobuf.push.MessagePush
|
import protobuf.push.MessagePush
|
||||||
import mqq.app.MobileQQ
|
import mqq.app.MobileQQ
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
@ -51,7 +51,7 @@ internal object PacketSvc: BaseSvc() {
|
|||||||
val msgSeq = (latestMsg?.msgSeq ?: 0) + 1
|
val msgSeq = (latestMsg?.msgSeq ?: 0) + 1
|
||||||
|
|
||||||
val msgPush = MessagePush(
|
val msgPush = MessagePush(
|
||||||
msgBody = MessageBody(
|
msgBody = NtMessage(
|
||||||
msgHead = MessageHead(
|
msgHead = MessageHead(
|
||||||
peer = app.longAccountUin,
|
peer = app.longAccountUin,
|
||||||
peerUid = app.currentUid,
|
peerUid = app.currentUid,
|
||||||
@ -59,28 +59,33 @@ internal object PacketSvc: BaseSvc() {
|
|||||||
receiver = app.longAccountUin,
|
receiver = app.longAccountUin,
|
||||||
receiverUid = app.currentUid
|
receiverUid = app.currentUid
|
||||||
),
|
),
|
||||||
contentHead = MessageContentHead(
|
contentHead = MessageContent(
|
||||||
msgType = 166,
|
msgType = 166,
|
||||||
msgSubType = 11,
|
msgSubType = 11,
|
||||||
msgSeq = msgSeq,
|
msgSeq = msgSeq,
|
||||||
u1 = msgSeq,
|
msgViaRandom = msgSeq,
|
||||||
msgTime = System.currentTimeMillis() / 1000,
|
msgTime = System.currentTimeMillis() / 1000,
|
||||||
u2 = 1,
|
u2 = 1,
|
||||||
u3 = msgSeq,
|
msgSeq_ = msgSeq,
|
||||||
msgRandom = msgService.getMsgUniqueId(System.currentTimeMillis()),
|
msgRandom = msgService.getMsgUniqueId(System.currentTimeMillis()),
|
||||||
u4 = msgSeq - 2,
|
u4 = msgSeq - 2,
|
||||||
u5 = msgSeq
|
u5 = msgSeq
|
||||||
),
|
),
|
||||||
richMsg = RichMessage(MessageElementList(builder()))
|
body = MessageBody(RichMessage(
|
||||||
|
elements = builder()
|
||||||
|
))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
fakeReceive("trpc.msg.olpush.OlPushService.MsgPush", 10000, ProtoBuf.encodeToByteArray(msgPush))
|
fakeReceive("trpc.msg.olpush.OlPushService.MsgPush", 10000, ProtoBuf.encodeToByteArray(msgPush))
|
||||||
return withTimeoutOrNull(5000L) {
|
return withTimeoutOrNull(5000L) {
|
||||||
suspendCancellableCoroutine {
|
suspendCancellableCoroutine {
|
||||||
AioListener.messageLessListenerMap[msgSeq] = {
|
AioListener.registerTemporaryMsgListener(msgSeq) {
|
||||||
it.resume(this.msgId)
|
it.resume(this.msgId)
|
||||||
}
|
}
|
||||||
|
it.invokeOnCancellation {
|
||||||
|
AioListener.unregisterTemporaryMsgListener(msgSeq)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} ?: -1L
|
} ?: -1L
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,11 @@ import tencent.im.oidb.oidb_sso
|
|||||||
|
|
||||||
internal object TicketSvc: BaseSvc() {
|
internal object TicketSvc: BaseSvc() {
|
||||||
object SigType {
|
object SigType {
|
||||||
const val WLOGIN_A2 = 64
|
|
||||||
const val WLOGIN_A5 = 2
|
const val WLOGIN_A5 = 2
|
||||||
|
const val WLOGIN_RESERVED = 16
|
||||||
|
const val WLOGIN_STWEB = 32 // TLV 103
|
||||||
|
const val WLOGIN_A2 = 64
|
||||||
|
const val WLOGIN_ST = 128
|
||||||
const val WLOGIN_AQSIG = 2097152
|
const val WLOGIN_AQSIG = 2097152
|
||||||
const val WLOGIN_D2 = 262144
|
const val WLOGIN_D2 = 262144
|
||||||
const val WLOGIN_DA2 = 33554432
|
const val WLOGIN_DA2 = 33554432
|
||||||
@ -26,14 +29,17 @@ internal object TicketSvc: BaseSvc() {
|
|||||||
const val WLOGIN_PSKEY = 1048576
|
const val WLOGIN_PSKEY = 1048576
|
||||||
const val WLOGIN_PT4Token = 134217728
|
const val WLOGIN_PT4Token = 134217728
|
||||||
const val WLOGIN_QRPUSH = 67108864
|
const val WLOGIN_QRPUSH = 67108864
|
||||||
const val WLOGIN_RESERVED = 16
|
|
||||||
const val WLOGIN_SID = 524288
|
const val WLOGIN_SID = 524288
|
||||||
const val WLOGIN_SIG64 = 8192
|
const val WLOGIN_SIG64 = 8192
|
||||||
const val WLOGIN_SKEY = 4096
|
const val WLOGIN_SKEY = 4096
|
||||||
const val WLOGIN_ST = 128
|
|
||||||
const val WLOGIN_STWEB = 32 // TLV 103
|
|
||||||
const val WLOGIN_TOKEN = 32768
|
const val WLOGIN_TOKEN = 32768
|
||||||
const val WLOGIN_VKEY = 131072
|
const val WLOGIN_VKEY = 131072
|
||||||
|
|
||||||
|
val ALL_TICKET = arrayOf(
|
||||||
|
WLOGIN_A5, WLOGIN_RESERVED, WLOGIN_STWEB, WLOGIN_A2, WLOGIN_ST, WLOGIN_AQSIG, WLOGIN_D2, WLOGIN_DA2,
|
||||||
|
WLOGIN_LHSIG, WLOGIN_LSKEY, WLOGIN_OPENKEY, WLOGIN_PAYTOKEN, WLOGIN_PF, WLOGIN_PSKEY, WLOGIN_PT4Token,
|
||||||
|
WLOGIN_QRPUSH, WLOGIN_SID, WLOGIN_SIG64, WLOGIN_SKEY, WLOGIN_TOKEN, WLOGIN_VKEY
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUin(): String {
|
fun getUin(): String {
|
||||||
@ -44,6 +50,14 @@ internal object TicketSvc: BaseSvc() {
|
|||||||
return app.longAccountUin
|
return app.longAccountUin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUid(): String {
|
||||||
|
return app.currentUid.ifBlank { "u_" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNickname(): String {
|
||||||
|
return app.currentNickname
|
||||||
|
}
|
||||||
|
|
||||||
fun getCookie(): String {
|
fun getCookie(): String {
|
||||||
val uin = getUin()
|
val uin = getUin()
|
||||||
val skey = getRealSkey(uin)
|
val skey = getRealSkey(uin)
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
package moe.fuqiuluo.qqinterface.servlet.ark
|
||||||
|
sealed class ArkAppInfo(
|
||||||
|
val appId: Long,
|
||||||
|
val version: String,
|
||||||
|
val packageName: String,
|
||||||
|
val signature: String,
|
||||||
|
val miniAppId: Long = 0
|
||||||
|
) {
|
||||||
|
data object QQMusic: ArkAppInfo(
|
||||||
|
appId = 100497308,
|
||||||
|
version = "0.0.0",
|
||||||
|
packageName = "com.tencent.qqmusic",
|
||||||
|
signature = "cbd27cd7c861227d013a25b2d10f0799"
|
||||||
|
)
|
||||||
|
data object NetEaseMusic: ArkAppInfo(
|
||||||
|
appId = 100495085,
|
||||||
|
version = "0.0.0",
|
||||||
|
packageName = "com.netease.cloudmusic",
|
||||||
|
signature = "da6b069da1e2982db3e386233f68d76d"
|
||||||
|
)
|
||||||
|
|
||||||
|
data object DanMaKu: ArkAppInfo(
|
||||||
|
appId = 100951776,
|
||||||
|
version = "0.0.0",
|
||||||
|
packageName = "tv.danmaku.bili",
|
||||||
|
signature = "7194d531cbe7960a22007b9f6bdaa38b",
|
||||||
|
miniAppId = 1109937557
|
||||||
|
)
|
||||||
|
}
|
@ -1,18 +1,16 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.ark
|
package moe.fuqiuluo.qqinterface.servlet.ark
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.pb.ByteStringMicro
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||||
|
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
|
||||||
import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
|
import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
|
||||||
|
import kotlin.coroutines.resume
|
||||||
sealed class ArkAppInfo(
|
import kotlin.time.Duration.Companion.seconds
|
||||||
val appId: Long,
|
|
||||||
val version: String,
|
|
||||||
val packageName: String,
|
|
||||||
val signature: String
|
|
||||||
) {
|
|
||||||
object QQMusic: ArkAppInfo(100497308, "0.0.0", "com.tencent.qqmusic", "cbd27cd7c861227d013a25b2d10f0799")
|
|
||||||
object NeteaseMusic: ArkAppInfo(100495085, "0.0.0", "com.netease.cloudmusic", "da6b069da1e2982db3e386233f68d76d")
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object ArkMsgSvc: BaseSvc() {
|
internal object ArkMsgSvc: BaseSvc() {
|
||||||
fun tryShareMusic(
|
fun tryShareMusic(
|
||||||
@ -54,4 +52,49 @@ internal object ArkMsgSvc: BaseSvc() {
|
|||||||
}
|
}
|
||||||
sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
|
sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun tryShareJsonMessage(
|
||||||
|
jsonString: String,
|
||||||
|
arkAppInfo: ArkAppInfo = ArkAppInfo.DanMaKu,
|
||||||
|
): Result<String> {
|
||||||
|
val msgSeq = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEC2C).qqMsgId
|
||||||
|
val req = oidb_cmd0xb77.ReqBody()
|
||||||
|
req.appid.set(arkAppInfo.appId)
|
||||||
|
req.app_type.set(1)
|
||||||
|
req.msg_style.set(10)
|
||||||
|
req.client_info.set(oidb_cmd0xb77.ClientInfo().also {
|
||||||
|
it.platform.set(1)
|
||||||
|
it.sdk_version.set(arkAppInfo.version)
|
||||||
|
it.android_package_name.set(arkAppInfo.packageName)
|
||||||
|
it.android_signature.set(arkAppInfo.signature)
|
||||||
|
})
|
||||||
|
req.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
|
||||||
|
it.tag_name.set(ByteStringMicro.copyFromUtf8("shamrock"))
|
||||||
|
it.msg_seq.set(msgSeq)
|
||||||
|
})
|
||||||
|
req.send_type.set(0)
|
||||||
|
req.recv_uin.set(TicketSvc.getLongUin())
|
||||||
|
req.mini_app_msg_body.set(oidb_cmd0xb77.MiniAppMsgBody().also {
|
||||||
|
it.mini_app_appid.set(arkAppInfo.miniAppId)
|
||||||
|
it.mini_app_path.set("pages")
|
||||||
|
it.web_page_url.set("https://im.qq.com/index/")
|
||||||
|
it.title.set("title")
|
||||||
|
it.desc.set("desc")
|
||||||
|
it.json_str.set(jsonString)
|
||||||
|
})
|
||||||
|
sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
|
||||||
|
val signedJson: String = withTimeoutOrNull(5.seconds) {
|
||||||
|
suspendCancellableCoroutine {
|
||||||
|
AioListener.registerTemporaryMsgListener(msgSeq) {
|
||||||
|
it.resume(elements.first {
|
||||||
|
it.elementType == MsgConstant.KELEMTYPEARKSTRUCT
|
||||||
|
}.arkElement.bytesData)
|
||||||
|
}
|
||||||
|
it.invokeOnCancellation {
|
||||||
|
AioListener.unregisterTemporaryMsgListener(msgSeq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: return Result.failure(Exception("unable to sign json"))
|
||||||
|
return Result.success(signedJson)
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package moe.fuqiuluo.qqinterface.servlet.ark
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class Region(
|
||||||
|
val adcode: Int,
|
||||||
|
val province: String?,
|
||||||
|
val city: String?
|
||||||
|
)
|
@ -15,13 +15,6 @@ import moe.fuqiuluo.shamrock.helper.LogCenter
|
|||||||
import moe.fuqiuluo.shamrock.tools.*
|
import moe.fuqiuluo.shamrock.tools.*
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
|
|
||||||
@Serializable
|
|
||||||
internal data class Region(
|
|
||||||
val adcode: Int,
|
|
||||||
val province: String?,
|
|
||||||
val city: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
internal object WeatherSvc {
|
internal object WeatherSvc {
|
||||||
suspend fun fetchWeatherCard(code: Int): Result<JsonObject> {
|
suspend fun fetchWeatherCard(code: Int): Result<JsonObject> {
|
||||||
val cookie = TicketSvc.getCookie("mp.qq.com")
|
val cookie = TicketSvc.getCookie("mp.qq.com")
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package moe.fuqiuluo.qqinterface.servlet.msg
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import moe.fuqiuluo.shamrock.tools.json
|
||||||
|
|
||||||
|
|
||||||
|
internal data class MessageSegment(
|
||||||
|
val type: String,
|
||||||
|
val data: Map<String, Any> = emptyMap()
|
||||||
|
) {
|
||||||
|
fun toJson(): JsonObject {
|
||||||
|
return hashMapOf(
|
||||||
|
"type" to type.json,
|
||||||
|
"data" to data.json
|
||||||
|
).json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun List<MessageSegment>.toJson(): JsonArray {
|
||||||
|
return this.map {
|
||||||
|
it.toJson()
|
||||||
|
}.json
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> {
|
||||||
|
return this.map {
|
||||||
|
hashMapOf(
|
||||||
|
"type" to it.type.json,
|
||||||
|
"data" to it.data.json
|
||||||
|
).json
|
||||||
|
}
|
||||||
|
}
|
@ -1,132 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.msg.convert
|
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.MessageElemConverter.*
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
|
||||||
import moe.fuqiuluo.shamrock.tools.json
|
|
||||||
|
|
||||||
internal typealias MessageSegmentList = ArrayList<MessageSegment>
|
|
||||||
|
|
||||||
internal data class MessageSegment(
|
|
||||||
val type: String,
|
|
||||||
val data: Map<String, Any> = emptyMap()
|
|
||||||
) {
|
|
||||||
fun toJson(): Map<String, JsonElement> {
|
|
||||||
return hashMapOf(
|
|
||||||
"type" to type.json,
|
|
||||||
"data" to data.json
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun MsgRecord.toSegments(): ArrayList<MessageSegment> {
|
|
||||||
return MessageConvert.convertMessageRecordToMsgSegment(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun MsgRecord.toCQCode(): String {
|
|
||||||
return MessageConvert.convertMessageRecordToCQCode(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun List<MsgElement>.toSegments(chatType: Int, peerId: String, subPeer: String): MessageSegmentList {
|
|
||||||
return MessageConvert.convertMessageElementsToMsgSegment(chatType, this, peerId, subPeer)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun List<MsgElement>.toCQCode(chatType: Int, peerId: String, subPeer: String): String {
|
|
||||||
return MessageConvert.convertMsgElementsToCQCode(this, chatType, peerId, subPeer)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal object MessageConvert {
|
|
||||||
private val convertMap by lazy {
|
|
||||||
mutableMapOf<Int, IMessageConvert>(
|
|
||||||
MsgConstant.KELEMTYPETEXT to TextConverter,
|
|
||||||
MsgConstant.KELEMTYPEFACE to FaceConverter,
|
|
||||||
MsgConstant.KELEMTYPEPIC to ImageConverter,
|
|
||||||
MsgConstant.KELEMTYPEPTT to VoiceConverter,
|
|
||||||
MsgConstant.KELEMTYPEVIDEO to VideoConverter,
|
|
||||||
MsgConstant.KELEMTYPEMARKETFACE to MarketFaceConverter,
|
|
||||||
MsgConstant.KELEMTYPEARKSTRUCT to StructJsonConverter,
|
|
||||||
MsgConstant.KELEMTYPEREPLY to ReplyConverter,
|
|
||||||
MsgConstant.KELEMTYPEGRAYTIP to GrayTipsConverter,
|
|
||||||
MsgConstant.KELEMTYPEFILE to FileConverter,
|
|
||||||
MsgConstant.KELEMTYPEMARKDOWN to MarkdownConverter,
|
|
||||||
//MsgConstant.KELEMTYPEMULTIFORWARD to XmlMultiMsgConverter,
|
|
||||||
//MsgConstant.KELEMTYPESTRUCTLONGMSG to XmlLongMsgConverter,
|
|
||||||
MsgConstant.KELEMTYPEFACEBUBBLE to BubbleFaceConverter,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun convertMessageElementsToMsgSegment(
|
|
||||||
chatType: Int,
|
|
||||||
elements: List<MsgElement>,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String
|
|
||||||
): ArrayList<MessageSegment> {
|
|
||||||
val messageData = arrayListOf<MessageSegment>()
|
|
||||||
elements.forEach { msg ->
|
|
||||||
kotlin.runCatching {
|
|
||||||
val elementId = msg.elementType
|
|
||||||
val converter = convertMap[elementId]
|
|
||||||
converter?.convert(chatType, peerId, subPeer, msg)
|
|
||||||
?: throw UnsupportedOperationException("不支持的消息element类型:$elementId")
|
|
||||||
}.onSuccess {
|
|
||||||
messageData.add(it)
|
|
||||||
}.onFailure {
|
|
||||||
if (it is UnknownError) {
|
|
||||||
// 不处理的消息类型,抛出unknown error
|
|
||||||
} else {
|
|
||||||
LogCenter.log("消息element转换错误:$it, elementType: ${msg.elementType}", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return messageData
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun convertMessageRecordToMsgSegment(record: MsgRecord, chatType: Int = record.chatType): ArrayList<MessageSegment> {
|
|
||||||
val peerId = when(chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> record.guildId
|
|
||||||
else -> record.peerUin.toString()
|
|
||||||
}
|
|
||||||
return convertMessageElementsToMsgSegment(chatType, record.elements, peerId, record.channelId ?: peerId)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun convertMsgElementsToCQCode(
|
|
||||||
elements: List<MsgElement>,
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String
|
|
||||||
): String {
|
|
||||||
if(elements.isEmpty()) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
val msgList = convertMessageElementsToMsgSegment(chatType, elements, peerId, subPeer).map {
|
|
||||||
it.toJson()
|
|
||||||
}
|
|
||||||
return MessageHelper.encodeCQCode(msgList)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun convertMessageRecordToCQCode(record: MsgRecord, chatType: Int = record.chatType): String {
|
|
||||||
val peerId = when(chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> record.guildId
|
|
||||||
else -> record.peerUin.toString()
|
|
||||||
}
|
|
||||||
return MessageHelper.encodeCQCode(
|
|
||||||
convertMessageElementsToMsgSegment(
|
|
||||||
chatType,
|
|
||||||
record.elements,
|
|
||||||
peerId,
|
|
||||||
record.channelId ?: peerId
|
|
||||||
).map { it.toJson() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun interface IMessageConvert {
|
|
||||||
suspend fun convert(chatType: Int, peerId: String, subPeer: String, element: MsgElement): MessageSegment
|
|
||||||
}
|
|
||||||
|
|
@ -1,493 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.msg.convert
|
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
|
||||||
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.ImageDB
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asString
|
|
||||||
import moe.fuqiuluo.shamrock.tools.json
|
|
||||||
|
|
||||||
internal sealed class MessageElemConverter: IMessageConvert {
|
|
||||||
/**
|
|
||||||
* 文本 / 艾特 消息转换消息段
|
|
||||||
*/
|
|
||||||
data object TextConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val text = element.textElement
|
|
||||||
return if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
|
|
||||||
MessageSegment(
|
|
||||||
type = "at",
|
|
||||||
data = hashMapOf(
|
|
||||||
"qq" to ContactHelper.getUinByUidAsync(text.atNtUid),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
MessageSegment(
|
|
||||||
type = "text",
|
|
||||||
data = hashMapOf(
|
|
||||||
"text" to text.content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 小表情 / 戳一戳 消息转换消息段
|
|
||||||
*/
|
|
||||||
data object FaceConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val face = element.faceElement
|
|
||||||
|
|
||||||
if (face.faceType == 5) {
|
|
||||||
return MessageSegment(
|
|
||||||
type = "poke",
|
|
||||||
data = hashMapOf(
|
|
||||||
"type" to face.pokeType,
|
|
||||||
"id" to face.vaspokeId,
|
|
||||||
"strength" to face.pokeStrength
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
when (face.faceIndex) {
|
|
||||||
114 -> {
|
|
||||||
return MessageSegment(
|
|
||||||
type = "basketball",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.resultId.ifEmpty { "0" }.toInt(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
358 -> {
|
|
||||||
if (face.sourceType == 1) return MessageSegment("new_dice")
|
|
||||||
return MessageSegment(
|
|
||||||
type = "new_dice",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.resultId.ifEmpty { "0" }.toInt()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
359 -> {
|
|
||||||
if (face.resultId.isEmpty()) return MessageSegment("new_rps")
|
|
||||||
return MessageSegment(
|
|
||||||
type = "new_rps",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.resultId.ifEmpty { "0" }.toInt()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
394 -> {
|
|
||||||
//LogCenter.log(face.toString())
|
|
||||||
return MessageSegment(
|
|
||||||
type = "face",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.faceIndex,
|
|
||||||
"big" to (face.faceType == 3),
|
|
||||||
"result" to (face.resultId ?: "1")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> return MessageSegment(
|
|
||||||
type = "face",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.faceIndex,
|
|
||||||
"big" to (face.faceType == 3)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片消息转换消息段
|
|
||||||
*/
|
|
||||||
data object ImageConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val image = element.picElement
|
|
||||||
val md5 = image.md5HexStr ?: image.fileName
|
|
||||||
.replace("{", "")
|
|
||||||
.replace("}", "")
|
|
||||||
.replace("-", "").split(".")[0]
|
|
||||||
|
|
||||||
ImageDB.getInstance().imageMappingDao().insert(
|
|
||||||
ImageMapping(md5.uppercase(), chatType, image.fileSize)
|
|
||||||
)
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "image",
|
|
||||||
data = hashMapOf(
|
|
||||||
"file" to md5,
|
|
||||||
"url" to when(chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(md5)
|
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(md5)
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(md5)
|
|
||||||
else -> unknownChatType(chatType)
|
|
||||||
},
|
|
||||||
"subType" to image.picSubType,
|
|
||||||
"type" to if (image.isFlashPic == true) "flash" else if(image.original) "original" else "show"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 语音消息转换消息段
|
|
||||||
*/
|
|
||||||
data object VoiceConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val record = element.pttElement
|
|
||||||
|
|
||||||
val md5 = if (record.fileName.startsWith("silk"))
|
|
||||||
record.fileName.substring(5)
|
|
||||||
else record.md5HexStr
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "record",
|
|
||||||
data = hashMapOf(
|
|
||||||
"file" to md5,
|
|
||||||
"url" to when(chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl("0", record.md5HexStr, record.fileUuid)
|
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl("0", record.md5HexStr, record.fileUuid)
|
|
||||||
else -> unknownChatType(chatType)
|
|
||||||
}
|
|
||||||
).also {
|
|
||||||
if(record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) {
|
|
||||||
it["magic"] = "1".json
|
|
||||||
}
|
|
||||||
if ((it["url"] as String).isBlank()) {
|
|
||||||
it.remove("url")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 视频消息转换消息段
|
|
||||||
*/
|
|
||||||
data object VideoConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val video = element.videoElement
|
|
||||||
val md5 = video.fileName.split(".")[0]
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "video",
|
|
||||||
data = hashMapOf(
|
|
||||||
"file" to video.fileName,
|
|
||||||
"url" to when(chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl(peerId, md5, video.fileUuid)
|
|
||||||
else -> unknownChatType(chatType)
|
|
||||||
}
|
|
||||||
).also {
|
|
||||||
if ((it["url"] as String).isBlank())
|
|
||||||
it.remove("url")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 商城大表情消息转换消息段
|
|
||||||
*/
|
|
||||||
data object MarketFaceConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val face = element.marketFaceElement
|
|
||||||
return when (face.emojiId.lowercase()) {
|
|
||||||
"4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
|
|
||||||
"83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
|
|
||||||
else -> MessageSegment(
|
|
||||||
type = "mface",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.emojiId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON消息转消息段
|
|
||||||
*/
|
|
||||||
data object StructJsonConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val data = element.arkElement.bytesData.asJsonObject
|
|
||||||
return when (data["app"].asString) {
|
|
||||||
"com.tencent.multimsg" -> {
|
|
||||||
val info = data["meta"].asJsonObject["detail"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "forward",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to info["resid"].asString
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"com.tencent.troopsharecard" -> {
|
|
||||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "contact",
|
|
||||||
data = hashMapOf(
|
|
||||||
"type" to "group",
|
|
||||||
"id" to info["jumpUrl"].asString.split("group_code=")[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"com.tencent.contact.lua" -> {
|
|
||||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "contact",
|
|
||||||
data = hashMapOf(
|
|
||||||
"type" to "private",
|
|
||||||
"id" to info["jumpUrl"].asString.split("uin=")[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"com.tencent.map" -> {
|
|
||||||
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "location",
|
|
||||||
data = hashMapOf(
|
|
||||||
"lat" to info["lat"].asString,
|
|
||||||
"lon" to info["lng"].asString,
|
|
||||||
"content" to info["address"].asString,
|
|
||||||
"title" to info["name"].asString
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> MessageSegment(
|
|
||||||
type = "json",
|
|
||||||
data = mapOf(
|
|
||||||
"data" to element.arkElement.bytesData.asJsonObject.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回复消息转消息段
|
|
||||||
*/
|
|
||||||
data object ReplyConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val reply = element.replyElement
|
|
||||||
val msgId = reply.replayMsgId
|
|
||||||
val msgHash = if (msgId != 0L) {
|
|
||||||
MessageHelper.generateMsgIdHash(chatType, msgId)
|
|
||||||
} else {
|
|
||||||
MessageDB.getInstance().messageMappingDao()
|
|
||||||
.queryByMsgSeq(chatType, peerId, reply.replayMsgSeq?.toInt() ?: 0)?.msgHashId
|
|
||||||
?:
|
|
||||||
kotlin.run {
|
|
||||||
LogCenter.log("消息映射关系未找到: Message($reply)", Level.WARN)
|
|
||||||
MessageHelper.generateMsgIdHash(chatType, reply.sourceMsgIdInRecords)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "reply",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to msgHash
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 灰色提示条消息过滤
|
|
||||||
*/
|
|
||||||
data object GrayTipsConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val tip = element.grayTipElement
|
|
||||||
when(tip.subElementType) {
|
|
||||||
MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
|
|
||||||
val notify = tip.jsonGrayTipElement
|
|
||||||
when(notify.busiId) {
|
|
||||||
/* 新人入群 */ 17L, /* 群戳一戳 */1061L,
|
|
||||||
/* 群撤回 */1014L, /* 群设精消息 */2401L,
|
|
||||||
/* 群头衔 */2407L -> {}
|
|
||||||
else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
|
|
||||||
val notify = tip.xmlElement
|
|
||||||
when(notify.busiId) {
|
|
||||||
/* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
|
|
||||||
else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
|
|
||||||
}
|
|
||||||
// 提示类消息,这里提供的是一个xml,不具备解析通用性
|
|
||||||
// 在这里不推送
|
|
||||||
throw UnknownError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件消息转换消息段
|
|
||||||
*/
|
|
||||||
data object FileConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val fileMsg = element.fileElement
|
|
||||||
val fileName = fileMsg.fileName
|
|
||||||
val fileSize = fileMsg.fileSize
|
|
||||||
val expireTime = fileMsg.expireTime ?: 0
|
|
||||||
val fileId = fileMsg.fileUuid
|
|
||||||
val bizId = fileMsg.fileBizId ?: 0
|
|
||||||
val fileSubId = fileMsg.fileSubId ?: ""
|
|
||||||
val url = when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
|
|
||||||
else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "file",
|
|
||||||
data = mapOf(
|
|
||||||
"name" to fileName,
|
|
||||||
"size" to fileSize,
|
|
||||||
"expire" to expireTime,
|
|
||||||
"id" to fileId,
|
|
||||||
"url" to url,
|
|
||||||
"biz" to bizId,
|
|
||||||
"sub" to fileSubId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 老板QQ的合并转发信息
|
|
||||||
*/
|
|
||||||
data object XmlMultiMsgConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val multiMsg = element.multiForwardMsgElement
|
|
||||||
return MessageSegment(
|
|
||||||
type = "forward",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to multiMsg.resId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data object XmlLongMsgConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val longMsg = element.structLongMsgElement
|
|
||||||
return MessageSegment(
|
|
||||||
type = "forward",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to longMsg.resId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data object MarkdownConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val markdown = element.markdownElement
|
|
||||||
return MessageSegment(
|
|
||||||
type = "markdown",
|
|
||||||
data = mapOf(
|
|
||||||
"content" to markdown.content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data object BubbleFaceConverter: MessageElemConverter() {
|
|
||||||
override suspend fun convert(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val bubbleElement = element.faceBubbleElement
|
|
||||||
return MessageSegment(
|
|
||||||
type = "bubble_face",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to bubbleElement.yellowFaceInfo.index,
|
|
||||||
"count" to (bubbleElement.faceCount ?: 1),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun unknownChatType(chatType: Int) {
|
|
||||||
throw UnsupportedOperationException("Not supported chat type: $chatType")
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,616 @@
|
|||||||
|
package moe.fuqiuluo.qqinterface.servlet.msg.messageelement
|
||||||
|
|
||||||
|
import kotlinx.io.core.ByteReadPacket
|
||||||
|
import kotlinx.io.core.discardExact
|
||||||
|
import kotlinx.io.core.readUInt
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||||
|
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||||
|
import moe.fuqiuluo.shamrock.tools.asString
|
||||||
|
import protobuf.message.MessageElement
|
||||||
|
|
||||||
|
|
||||||
|
internal suspend fun List<MessageElement>.toSegments(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String
|
||||||
|
): List<MessageSegment> {
|
||||||
|
val messageData = arrayListOf<MessageSegment>()
|
||||||
|
this.forEach { msg ->
|
||||||
|
kotlin.runCatching {
|
||||||
|
val elementType = if (msg.text != null) {
|
||||||
|
1
|
||||||
|
} else if (msg.face != null) {
|
||||||
|
2
|
||||||
|
} else if (msg.json != null) {
|
||||||
|
51
|
||||||
|
} else if (msg.comm != null) {
|
||||||
|
53
|
||||||
|
} else
|
||||||
|
throw UnsupportedOperationException("不支持的消息element类型:$msg")
|
||||||
|
val converter = MessageElementConverter[elementType]
|
||||||
|
converter?.invoke(chatType, peerId, subPeer, msg)
|
||||||
|
?: throw UnsupportedOperationException("不支持的消息element类型:$elementType")
|
||||||
|
}.onSuccess {
|
||||||
|
messageData.add(it)
|
||||||
|
}.onFailure {
|
||||||
|
if (it is UnknownError) {
|
||||||
|
// 不处理的消息类型,抛出unknown error
|
||||||
|
} else {
|
||||||
|
LogCenter.log("消息element转换错误:$it", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messageData
|
||||||
|
}
|
||||||
|
|
||||||
|
internal typealias IMessageElementConverter = suspend (Int, String, String, MessageElement) -> MessageSegment
|
||||||
|
|
||||||
|
internal object MessageElementConverter {
|
||||||
|
private val convertMap = hashMapOf(
|
||||||
|
1 to MessageElementConverter::convertTextElem,
|
||||||
|
// MsgConstant.KELEMTYPEFACE to MessageElementConverter::convertFaceElem,
|
||||||
|
// MsgConstant.KELEMTYPEPIC to MessageElementConverter::convertImageElem,
|
||||||
|
// MsgConstant.KELEMTYPEPTT to MessageElementConverter::convertVoiceElem,
|
||||||
|
// MsgConstant.KELEMTYPEVIDEO to MessageElementConverter::convertVideoElem,
|
||||||
|
// MsgConstant.KELEMTYPEMARKETFACE to MessageElementConverter::convertMarketFaceElem,
|
||||||
|
51 to MessageElementConverter::convertStructJsonElem,
|
||||||
|
// MsgConstant.KELEMTYPEREPLY to MessageElementConverter::convertReplyElem,
|
||||||
|
// MsgConstant.KELEMTYPEGRAYTIP to MessageElementConverter::convertGrayTipsElem,
|
||||||
|
// MsgConstant.KELEMTYPEFILE to MessageElementConverter::convertFileElem,
|
||||||
|
// MsgConstant.KELEMTYPEMARKDOWN to MessageElementConverter::convertMarkdownElem,
|
||||||
|
// //MsgConstant.KELEMTYPEMULTIFORWARD to MessageElementConverter::convertXmlMultiMsgElem,
|
||||||
|
// //MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElementConverter::convertXmlLongMsgElem,
|
||||||
|
// MsgConstant.KELEMTYPEFACEBUBBLE to MessageElementConverter::convertBubbleFaceElem,
|
||||||
|
// MsgConstant.KELEMTYPEINLINEKEYBOARD to MessageElementConverter::convertInlineKeyboardElem,
|
||||||
|
)
|
||||||
|
|
||||||
|
operator fun get(type: Int): IMessageElementConverter? = convertMap[type]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本 / 艾特 消息转换消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertTextElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MessageElement
|
||||||
|
): MessageSegment {
|
||||||
|
val text = element.text!!
|
||||||
|
if (text.attr6Buf != null) {
|
||||||
|
val at = ByteReadPacket(text.attr6Buf!!)
|
||||||
|
at.discardExact(7)
|
||||||
|
val uin = at.readUInt()
|
||||||
|
return MessageSegment(
|
||||||
|
type = "at",
|
||||||
|
data = hashMapOf(
|
||||||
|
"qq" to uin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (text.pbReserve != null) {
|
||||||
|
val resv = text.pbReserve!!
|
||||||
|
return MessageSegment(
|
||||||
|
type = "at",
|
||||||
|
data = hashMapOf(
|
||||||
|
"qq" to when (resv.atType) {
|
||||||
|
2 -> resv.atMemberTinyid!!
|
||||||
|
4 -> resv.atChannelInfo!!.channelId!!
|
||||||
|
else -> throw UnsupportedOperationException("Unknown at type: ${resv.atType}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return MessageSegment(
|
||||||
|
type = "text",
|
||||||
|
data = hashMapOf(
|
||||||
|
"text" to text.text!!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * 小表情 / 戳一戳 消息转换消息段
|
||||||
|
// */
|
||||||
|
// private suspend fun convertFaceElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val face = element.faceElement
|
||||||
|
//
|
||||||
|
// if (face.faceType == 5) {
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "poke",
|
||||||
|
// data = hashMapOf(
|
||||||
|
// "type" to face.pokeType,
|
||||||
|
// "id" to face.vaspokeId,
|
||||||
|
// "strength" to face.pokeStrength
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// when (face.faceIndex) {
|
||||||
|
// 114 -> {
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "basketball",
|
||||||
|
// data = hashMapOf(
|
||||||
|
// "id" to face.resultId.ifEmpty { "0" }.toInt(),
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 358 -> {
|
||||||
|
// if (face.sourceType == 1) return MessageSegment("new_dice")
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "new_dice",
|
||||||
|
// data = hashMapOf(
|
||||||
|
// "id" to face.resultId.ifEmpty { "0" }.toInt()
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 359 -> {
|
||||||
|
// if (face.resultId.isEmpty()) return MessageSegment("new_rps")
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "new_rps",
|
||||||
|
// data = hashMapOf(
|
||||||
|
// "id" to face.resultId.ifEmpty { "0" }.toInt()
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 394 -> {
|
||||||
|
// //LogCenter.log(face.toString())
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "face",
|
||||||
|
// data = hashMapOf(
|
||||||
|
// "id" to face.faceIndex,
|
||||||
|
// "big" to (face.faceType == 3),
|
||||||
|
// "result" to (face.resultId ?: "1")
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// else -> return MessageSegment(
|
||||||
|
// type = "face",
|
||||||
|
// data = hashMapOf(
|
||||||
|
// "id" to face.faceIndex,
|
||||||
|
// "big" to (face.faceType == 3)
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 图片消息转换消息段
|
||||||
|
// */
|
||||||
|
// private suspend fun convertImageElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val image = element.picElement
|
||||||
|
// val md5 = image.md5HexStr ?: image.fileName
|
||||||
|
// .replace("{", "")
|
||||||
|
// .replace("}", "")
|
||||||
|
// .replace("-", "").split(".")[0]
|
||||||
|
//
|
||||||
|
// ImageDB.getInstance().imageMappingDao().insert(
|
||||||
|
// ImageMapping(md5.uppercase(), chatType, image.fileSize)
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// //LogCenter.log(image.toString())
|
||||||
|
//
|
||||||
|
// val originalUrl = image.originImageUrl ?: ""
|
||||||
|
// //LogCenter.log({ "receive image: $image" }, Level.DEBUG)
|
||||||
|
//
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "image",
|
||||||
|
// data = hashMapOf(
|
||||||
|
// "file" to md5,
|
||||||
|
// "url" to when (chatType) {
|
||||||
|
// MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
||||||
|
// originalUrl,
|
||||||
|
// md5
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(originalUrl, md5)
|
||||||
|
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(originalUrl, md5)
|
||||||
|
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||||
|
// },
|
||||||
|
// "subType" to image.picSubType,
|
||||||
|
// "type" to if (image.isFlashPic == true) "flash" else if (image.original) "original" else "show"
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 语音消息转换消息段
|
||||||
|
// */
|
||||||
|
// private suspend fun convertVoiceElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val record = element.pttElement
|
||||||
|
//
|
||||||
|
// val md5 = if (record.fileName.startsWith("silk"))
|
||||||
|
// record.fileName.substring(5)
|
||||||
|
// else record.md5HexStr
|
||||||
|
//
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "record",
|
||||||
|
// data = hashMapOf(
|
||||||
|
// "file" to md5,
|
||||||
|
// "url" to when (chatType) {
|
||||||
|
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl(
|
||||||
|
// "0",
|
||||||
|
// record.md5HexStr,
|
||||||
|
// record.fileUuid
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
|
||||||
|
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
|
||||||
|
// "0",
|
||||||
|
// record.md5HexStr,
|
||||||
|
// record.fileUuid
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||||
|
// }
|
||||||
|
// ).also {
|
||||||
|
// if (record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) {
|
||||||
|
// it["magic"] = "1"
|
||||||
|
// }
|
||||||
|
// if ((it["url"] as String).isBlank()) {
|
||||||
|
// it.remove("url")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 视频消息转换消息段
|
||||||
|
// */
|
||||||
|
// private suspend fun convertVideoElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val video = element.videoElement
|
||||||
|
// val md5 = if (video.fileName.contains("/")) {
|
||||||
|
// video.videoMd5.takeIf {
|
||||||
|
// !it.isNullOrEmpty()
|
||||||
|
// }?.hex2ByteArray() ?: video.fileName.split("/").let {
|
||||||
|
// it[it.size - 2].hex2ByteArray()
|
||||||
|
// }
|
||||||
|
// } else video.fileName.split(".")[0].hex2ByteArray()
|
||||||
|
//
|
||||||
|
// //LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
|
||||||
|
//
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "video",
|
||||||
|
// data = hashMapOf(
|
||||||
|
// "file" to video.fileName,
|
||||||
|
// "url" to when (chatType) {
|
||||||
|
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||||
|
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
|
||||||
|
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||||
|
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||||
|
// }
|
||||||
|
// ).also {
|
||||||
|
// if ((it["url"] as String).isBlank())
|
||||||
|
// it.remove("url")
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 商城大表情消息转换消息段
|
||||||
|
// */
|
||||||
|
// private suspend fun convertMarketFaceElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val face = element.marketFaceElement
|
||||||
|
// return when (face.emojiId.lowercase()) {
|
||||||
|
// "4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
|
||||||
|
// "83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
|
||||||
|
// else -> MessageSegment(
|
||||||
|
// type = "mface",
|
||||||
|
// data = hashMapOf(
|
||||||
|
// "id" to face.emojiId
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
/**
|
||||||
|
* JSON消息转消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertStructJsonElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MessageElement
|
||||||
|
): MessageSegment {
|
||||||
|
val data = element.json!!.data!!
|
||||||
|
val jsonStr =
|
||||||
|
(if (data[0].toInt() == 1) DeflateTools.uncompress(data.sliceArray(1 until data.size)) else data.sliceArray(1 until data.size)).toString()
|
||||||
|
val json = jsonStr.asJsonObject
|
||||||
|
return when (json["app"].asString) {
|
||||||
|
"com.tencent.multimsg" -> {
|
||||||
|
val info = json["meta"].asJsonObject["detail"].asJsonObject
|
||||||
|
MessageSegment(
|
||||||
|
type = "forward",
|
||||||
|
data = mapOf(
|
||||||
|
"id" to info["resid"].asString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"com.tencent.troopsharecard" -> {
|
||||||
|
val info = json["meta"].asJsonObject["contact"].asJsonObject
|
||||||
|
MessageSegment(
|
||||||
|
type = "contact",
|
||||||
|
data = hashMapOf(
|
||||||
|
"type" to "group",
|
||||||
|
"id" to info["jumpUrl"].asString.split("group_code=")[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"com.tencent.contact.lua" -> {
|
||||||
|
val info = json["meta"].asJsonObject["contact"].asJsonObject
|
||||||
|
MessageSegment(
|
||||||
|
type = "contact",
|
||||||
|
data = hashMapOf(
|
||||||
|
"type" to "private",
|
||||||
|
"id" to info["jumpUrl"].asString.split("uin=")[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"com.tencent.map" -> {
|
||||||
|
val info = json["meta"].asJsonObject["Location.Search"].asJsonObject
|
||||||
|
MessageSegment(
|
||||||
|
type = "location",
|
||||||
|
data = hashMapOf(
|
||||||
|
"lat" to info["lat"].asString,
|
||||||
|
"lon" to info["lng"].asString,
|
||||||
|
"content" to info["address"].asString,
|
||||||
|
"title" to info["name"].asString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> MessageSegment(
|
||||||
|
type = "json",
|
||||||
|
data = mapOf(
|
||||||
|
"data" to jsonStr
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * 回复消息转消息段
|
||||||
|
// */
|
||||||
|
// private suspend fun convertReplyElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val reply = element.replyElement
|
||||||
|
// val msgId = reply.replayMsgId
|
||||||
|
// val msgHash = if (msgId != 0L) {
|
||||||
|
// MessageHelper.generateMsgIdHash(chatType, msgId)
|
||||||
|
// } else {
|
||||||
|
// MessageDB.getInstance().messageMappingDao()
|
||||||
|
// .queryByMsgSeq(chatType, peerId, reply.replayMsgSeq?.toInt() ?: 0)?.msgHashId
|
||||||
|
// ?: kotlin.run {
|
||||||
|
// LogCenter.log("消息映射关系未找到: Message($reply)", Level.WARN)
|
||||||
|
// MessageHelper.generateMsgIdHash(chatType, reply.sourceMsgIdInRecords)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "reply",
|
||||||
|
// data = mapOf(
|
||||||
|
// "id" to msgHash
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 灰色提示条消息过滤
|
||||||
|
// */
|
||||||
|
// private suspend fun convertGrayTipsElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val tip = element.grayTipElement
|
||||||
|
// when (tip.subElementType) {
|
||||||
|
// MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
|
||||||
|
// val notify = tip.jsonGrayTipElement
|
||||||
|
// when (notify.busiId) {
|
||||||
|
// /* 新人入群 */ 17L, /* 群戳一戳 */1061L,
|
||||||
|
// /* 群撤回 */1014L, /* 群设精消息 */2401L,
|
||||||
|
// /* 群头衔 */2407L -> {
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
|
||||||
|
// val notify = tip.xmlElement
|
||||||
|
// when (notify.busiId) {
|
||||||
|
// /* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
|
||||||
|
// else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
|
||||||
|
// }
|
||||||
|
// // 提示类消息,这里提供的是一个xml,不具备解析通用性
|
||||||
|
// // 在这里不推送
|
||||||
|
// throw UnknownError()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 文件消息转换消息段
|
||||||
|
// */
|
||||||
|
// private suspend fun convertFileElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val fileMsg = element.fileElement
|
||||||
|
// val fileName = fileMsg.fileName
|
||||||
|
// val fileSize = fileMsg.fileSize
|
||||||
|
// val expireTime = fileMsg.expireTime ?: 0
|
||||||
|
// val fileId = fileMsg.fileUuid
|
||||||
|
// val bizId = fileMsg.fileBizId ?: 0
|
||||||
|
// val fileSubId = fileMsg.fileSubId ?: ""
|
||||||
|
// val url = when (chatType) {
|
||||||
|
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
||||||
|
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
|
||||||
|
// else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "file",
|
||||||
|
// data = mapOf(
|
||||||
|
// "name" to fileName,
|
||||||
|
// "size" to fileSize,
|
||||||
|
// "expire" to expireTime,
|
||||||
|
// "id" to fileId,
|
||||||
|
// "url" to url,
|
||||||
|
// "biz" to bizId,
|
||||||
|
// "sub" to fileSubId
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 老板QQ的合并转发信息
|
||||||
|
// */
|
||||||
|
// private suspend fun convertXmlMultiMsgElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val multiMsg = element.multiForwardMessageElement
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "forward",
|
||||||
|
// data = mapOf(
|
||||||
|
// "id" to multiMsg.resId
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private suspend fun convertXmlLongMsgElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val longMsg = element.structLongMessageElement
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "forward",
|
||||||
|
// data = mapOf(
|
||||||
|
// "id" to longMsg.resId
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private suspend fun convertMarkdownElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val markdown = element.markdownElement
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "markdown",
|
||||||
|
// data = mapOf(
|
||||||
|
// "content" to markdown.content
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private suspend fun convertBubbleFaceElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val bubbleElement = element.faceBubbleElement
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "bubble_face",
|
||||||
|
// data = mapOf(
|
||||||
|
// "id" to bubbleElement.yellowFaceInfo.index,
|
||||||
|
// "count" to (bubbleElement.faceCount ?: 1),
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private suspend fun convertInlineKeyboardElem(
|
||||||
|
// chatType: Int,
|
||||||
|
// peerId: String,
|
||||||
|
// subPeer: String,
|
||||||
|
// element: MessageElement
|
||||||
|
// ): MessageSegment {
|
||||||
|
// val keyboard = element.inlineKeyboardElement
|
||||||
|
// return MessageSegment(
|
||||||
|
// type = "inline_keyboard",
|
||||||
|
// data = mapOf(
|
||||||
|
// "data" to buildJsonObject {
|
||||||
|
// putJsonArray("rows") {
|
||||||
|
// keyboard.rows.forEach { row ->
|
||||||
|
// add(buildJsonObject row@{
|
||||||
|
// putJsonArray("buttons") {
|
||||||
|
// row.buttons.forEach { button ->
|
||||||
|
// add(buildJsonObject {
|
||||||
|
// put("id", button.id ?: "")
|
||||||
|
// put("label", button.label ?: "")
|
||||||
|
// put("visited_label", button.visitedLabel ?: "")
|
||||||
|
// put("style", button.style)
|
||||||
|
// put("type", button.type)
|
||||||
|
// put("click_limit", button.clickLimit)
|
||||||
|
// put("unsupport_tips", button.unsupportTips ?: "")
|
||||||
|
// put("data", button.data)
|
||||||
|
// put("at_bot_show_channel_list", button.atBotShowChannelList)
|
||||||
|
// put("permission_type", button.permissionType)
|
||||||
|
// putJsonArray("specify_role_ids") {
|
||||||
|
// button.specifyRoleIds?.forEach { add(it) }
|
||||||
|
// }
|
||||||
|
// putJsonArray("specify_tinyids") {
|
||||||
|
// button.specifyTinyids?.forEach { add(it) }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// put("bot_appid", keyboard.botAppid)
|
||||||
|
// }.toString()
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
@ -0,0 +1,183 @@
|
|||||||
|
package moe.fuqiuluo.qqinterface.servlet.msg.messageelement
|
||||||
|
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.GProSvc
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
||||||
|
import moe.fuqiuluo.shamrock.helper.*
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.helper.ParamsException
|
||||||
|
import moe.fuqiuluo.shamrock.tools.*
|
||||||
|
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||||
|
import protobuf.message.MessageElement
|
||||||
|
import protobuf.message.element.FaceElement
|
||||||
|
import protobuf.message.element.JsonElement
|
||||||
|
import protobuf.message.element.TextElement
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
internal typealias IMessageElementMaker = suspend (Int, Long, String, JsonObject) -> Result<MessageElement>
|
||||||
|
|
||||||
|
internal object MessageElementMaker {
|
||||||
|
private val makerArray = hashMapOf(
|
||||||
|
"text" to MessageElementMaker::createTextElem,
|
||||||
|
"face" to MessageElementMaker::createFaceElem,
|
||||||
|
// "pic" to MessageElementMaker::createImageElem,
|
||||||
|
// "image" to MessageElementMaker::createImageElem,
|
||||||
|
// "voice" to MessageElementMaker::createRecordElem,
|
||||||
|
// "record" to MessageElementMaker::createRecordElem,
|
||||||
|
"at" to MessageElementMaker::createAtElem,
|
||||||
|
// "video" to MessageElementMaker::createVideoElem,
|
||||||
|
// "markdown" to MessageElementMaker::createMarkdownElem,
|
||||||
|
// "dice" to MessageElementMaker::createDiceElem,
|
||||||
|
// "rps" to MessageElementMaker::createRpsElem,
|
||||||
|
// "poke" to MessageElementMaker::createPokeElem,
|
||||||
|
// "anonymous" to MessageElementMaker::createAnonymousElem,
|
||||||
|
// "share" to MessageElementMaker::createShareElem,
|
||||||
|
// "contact" to MessageElementMaker::createContactElem,
|
||||||
|
// "location" to MessageElementMaker::createLocationElem,
|
||||||
|
// "music" to MessageElementMaker::createMusicElem,
|
||||||
|
// "reply" to MessageElementMaker::createReplyElem,
|
||||||
|
// "touch" to MessageElementMaker::createTouchElem,
|
||||||
|
// "weather" to MessageElementMaker::createWeatherElem,
|
||||||
|
"json" to MessageElementMaker::createJsonElem,
|
||||||
|
//"new_dice" to MessageElementMaker::createNewDiceElem,
|
||||||
|
//"new_rps" to MessageElementMaker::createNewRpsElem,
|
||||||
|
//"basketball" to MessageElementMaker::createBasketballElem,
|
||||||
|
//"node" to MessageMaker::createNodeElem,
|
||||||
|
//"multi_msg" to MessageMaker::createLongMsgStruct,
|
||||||
|
//"bubble_face" to MessageElementMaker::createBubbleFaceElem,
|
||||||
|
)
|
||||||
|
|
||||||
|
operator fun get(type: String): IMessageElementMaker? = makerArray[type]
|
||||||
|
|
||||||
|
private suspend fun createTextElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MessageElement> {
|
||||||
|
data.checkAndThrow("text")
|
||||||
|
val elem = MessageElement(
|
||||||
|
text = TextElement(data["text"].asString)
|
||||||
|
)
|
||||||
|
return Result.success(elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createFaceElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MessageElement> {
|
||||||
|
data.checkAndThrow("id")
|
||||||
|
val elem = MessageElement(
|
||||||
|
face = FaceElement(data["id"].asInt)
|
||||||
|
)
|
||||||
|
return Result.success(elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createAtElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MessageElement> {
|
||||||
|
return if (chatType == MsgConstant.KCHATTYPEGROUP) {
|
||||||
|
data.checkAndThrow("qq")
|
||||||
|
|
||||||
|
val qq: Long
|
||||||
|
val type: Int
|
||||||
|
lateinit var display: String
|
||||||
|
when (val qqStr = data["qq"].asString) {
|
||||||
|
"0", "all" -> {
|
||||||
|
qq = 0
|
||||||
|
type = 1
|
||||||
|
display = "@全体成员"
|
||||||
|
}
|
||||||
|
|
||||||
|
"online" -> {
|
||||||
|
qq = 0
|
||||||
|
type = 64
|
||||||
|
display = "@在线成员"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
qq = qqStr.toLong()
|
||||||
|
type = 0
|
||||||
|
display =
|
||||||
|
"@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(peerId, qqStr, true)
|
||||||
|
.onSuccess {
|
||||||
|
it.troopnick
|
||||||
|
.ifEmpty { it.friendnick }
|
||||||
|
.ifEmpty { qqStr }
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val attr6: ByteBuffer = ByteBuffer.allocate(6)
|
||||||
|
attr6.put(byteArrayOf(0, 1, 0, 0, 0))
|
||||||
|
attr6.putChar(display.length.toChar())
|
||||||
|
attr6.putChar(type.toChar())
|
||||||
|
attr6.putBuf32Long(qq)
|
||||||
|
attr6.put(byteArrayOf(0, 0))
|
||||||
|
val elem = MessageElement(
|
||||||
|
text = TextElement(text = display, attr6Buf = attr6.array())
|
||||||
|
)
|
||||||
|
Result.success(elem)
|
||||||
|
} else if (chatType == MsgConstant.KCHATTYPEGUILD) {
|
||||||
|
data.checkAndThrow("qq")
|
||||||
|
|
||||||
|
val qq: Long
|
||||||
|
val type: Int
|
||||||
|
lateinit var display: String
|
||||||
|
when (val qqStr = data["qq"].asString) {
|
||||||
|
"0", "all" -> {
|
||||||
|
type = 2
|
||||||
|
display = "@全体成员"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
qq = qqStr.toLong()
|
||||||
|
type = 2
|
||||||
|
display =
|
||||||
|
"@" + (data["name"].asStringOrNull ?: GProSvc.getUserGuildInfo(0UL, 0UL)
|
||||||
|
.onSuccess {
|
||||||
|
it.nickName.ifNullOrEmpty(qqStr)
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("无法获取频道组成员信息: $qqStr", Level.ERROR)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val elem = MessageElement(
|
||||||
|
text = TextElement(text = display, pbReserve = TextElement.Companion.TextResvAttr(atType = type))
|
||||||
|
)
|
||||||
|
Result.success(elem)
|
||||||
|
} else Result.failure(ActionMsgException)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createJsonElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MessageElement> {
|
||||||
|
data.checkAndThrow("data")
|
||||||
|
|
||||||
|
val elem = MessageElement(
|
||||||
|
json = JsonElement(
|
||||||
|
data = DeflateTools.compress(data.toString().toByteArray())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return Result.success(elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonObject.checkAndThrow(vararg key: String) {
|
||||||
|
key.forEach {
|
||||||
|
if (!containsKey(it)) throw ParamsException(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,603 @@
|
|||||||
|
package moe.fuqiuluo.qqinterface.servlet.msg.msgelement
|
||||||
|
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
||||||
|
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||||
|
import moe.fuqiuluo.shamrock.helper.db.ImageDB
|
||||||
|
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
|
||||||
|
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
||||||
|
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||||
|
import moe.fuqiuluo.shamrock.tools.asString
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||||||
|
|
||||||
|
|
||||||
|
internal suspend fun List<MsgElement>.toSegments(chatType: Int, peerId: String, subPeer: String): List<MessageSegment> {
|
||||||
|
val messageData = arrayListOf<MessageSegment>()
|
||||||
|
this.forEach { msg ->
|
||||||
|
kotlin.runCatching {
|
||||||
|
val converter = MsgElementConverter[msg.elementType]
|
||||||
|
converter?.invoke(chatType, peerId, subPeer, msg)
|
||||||
|
?: throw UnsupportedOperationException("不支持的消息element类型:${msg.elementType}")
|
||||||
|
}.onSuccess {
|
||||||
|
messageData.add(it)
|
||||||
|
}.onFailure {
|
||||||
|
if (it is UnknownError) {
|
||||||
|
// 不处理的消息类型,抛出unknown error
|
||||||
|
} else {
|
||||||
|
LogCenter.log("消息element转换错误:$it, elementType: ${msg.elementType}", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messageData
|
||||||
|
}
|
||||||
|
|
||||||
|
internal suspend fun List<MsgElement>.toCQCode(chatType: Int, peerId: String, subPeer: String): String {
|
||||||
|
if (this.isEmpty()) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return MessageHelper.nativeEncodeCQCode(this.toSegments(chatType, peerId, subPeer).map {
|
||||||
|
val params = hashMapOf<String, String>()
|
||||||
|
params["_type"] = it.type
|
||||||
|
it.data.forEach { (key, value) ->
|
||||||
|
params[key] = value.toString()
|
||||||
|
}
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
internal typealias IMsgElementConverter = suspend (Int, String, String, MsgElement) -> MessageSegment
|
||||||
|
|
||||||
|
internal object MsgElementConverter {
|
||||||
|
private val convertMap = hashMapOf(
|
||||||
|
MsgConstant.KELEMTYPETEXT to MsgElementConverter::convertTextElem,
|
||||||
|
MsgConstant.KELEMTYPEFACE to MsgElementConverter::convertFaceElem,
|
||||||
|
MsgConstant.KELEMTYPEPIC to MsgElementConverter::convertImageElem,
|
||||||
|
MsgConstant.KELEMTYPEPTT to MsgElementConverter::convertVoiceElem,
|
||||||
|
MsgConstant.KELEMTYPEVIDEO to MsgElementConverter::convertVideoElem,
|
||||||
|
MsgConstant.KELEMTYPEMARKETFACE to MsgElementConverter::convertMarketFaceElem,
|
||||||
|
MsgConstant.KELEMTYPEARKSTRUCT to MsgElementConverter::convertStructJsonElem,
|
||||||
|
MsgConstant.KELEMTYPEREPLY to MsgElementConverter::convertReplyElem,
|
||||||
|
MsgConstant.KELEMTYPEGRAYTIP to MsgElementConverter::convertGrayTipsElem,
|
||||||
|
MsgConstant.KELEMTYPEFILE to MsgElementConverter::convertFileElem,
|
||||||
|
MsgConstant.KELEMTYPEMARKDOWN to MsgElementConverter::convertMarkdownElem,
|
||||||
|
//MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem,
|
||||||
|
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem,
|
||||||
|
MsgConstant.KELEMTYPEFACEBUBBLE to MsgElementConverter::convertBubbleFaceElem,
|
||||||
|
MsgConstant.KELEMTYPEINLINEKEYBOARD to MsgElementConverter::convertInlineKeyboardElem
|
||||||
|
)
|
||||||
|
|
||||||
|
operator fun get(type: Int): IMsgElementConverter? = convertMap[type]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本 / 艾特 消息转换消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertTextElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val text = element.textElement
|
||||||
|
return if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
|
||||||
|
MessageSegment(
|
||||||
|
type = "at",
|
||||||
|
data = hashMapOf(
|
||||||
|
"qq" to ContactHelper.getUinByUidAsync(text.atNtUid),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
MessageSegment(
|
||||||
|
type = "text",
|
||||||
|
data = hashMapOf(
|
||||||
|
"text" to text.content
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小表情 / 戳一戳 消息转换消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertFaceElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val face = element.faceElement
|
||||||
|
|
||||||
|
if (face.faceType == 5) {
|
||||||
|
return MessageSegment(
|
||||||
|
type = "poke",
|
||||||
|
data = hashMapOf(
|
||||||
|
"type" to face.pokeType,
|
||||||
|
"id" to face.vaspokeId,
|
||||||
|
"strength" to face.pokeStrength
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
when (face.faceIndex) {
|
||||||
|
114 -> {
|
||||||
|
return MessageSegment(
|
||||||
|
type = "basketball",
|
||||||
|
data = hashMapOf(
|
||||||
|
"id" to face.resultId.ifEmpty { "0" }.toInt(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
358 -> {
|
||||||
|
if (face.sourceType == 1) return MessageSegment("new_dice")
|
||||||
|
return MessageSegment(
|
||||||
|
type = "new_dice",
|
||||||
|
data = hashMapOf(
|
||||||
|
"id" to face.resultId.ifEmpty { "0" }.toInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
359 -> {
|
||||||
|
if (face.resultId.isEmpty()) return MessageSegment("new_rps")
|
||||||
|
return MessageSegment(
|
||||||
|
type = "new_rps",
|
||||||
|
data = hashMapOf(
|
||||||
|
"id" to face.resultId.ifEmpty { "0" }.toInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
394 -> {
|
||||||
|
//LogCenter.log(face.toString())
|
||||||
|
return MessageSegment(
|
||||||
|
type = "face",
|
||||||
|
data = hashMapOf(
|
||||||
|
"id" to face.faceIndex,
|
||||||
|
"big" to (face.faceType == 3),
|
||||||
|
"result" to (face.resultId ?: "1")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> return MessageSegment(
|
||||||
|
type = "face",
|
||||||
|
data = hashMapOf(
|
||||||
|
"id" to face.faceIndex,
|
||||||
|
"big" to (face.faceType == 3)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片消息转换消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertImageElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val image = element.picElement
|
||||||
|
val md5 = image.md5HexStr ?: image.fileName
|
||||||
|
.replace("{", "")
|
||||||
|
.replace("}", "")
|
||||||
|
.replace("-", "").split(".")[0]
|
||||||
|
|
||||||
|
ImageDB.getInstance().imageMappingDao().insert(
|
||||||
|
ImageMapping(md5.uppercase(), chatType, image.fileSize)
|
||||||
|
)
|
||||||
|
|
||||||
|
//LogCenter.log(image.toString())
|
||||||
|
|
||||||
|
val originalUrl = image.originImageUrl ?: ""
|
||||||
|
//LogCenter.log({ "receive image: $image" }, Level.DEBUG)
|
||||||
|
|
||||||
|
return MessageSegment(
|
||||||
|
type = "image",
|
||||||
|
data = hashMapOf(
|
||||||
|
"file" to md5,
|
||||||
|
"url" to when (chatType) {
|
||||||
|
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
||||||
|
originalUrl,
|
||||||
|
md5
|
||||||
|
)
|
||||||
|
|
||||||
|
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(originalUrl, md5)
|
||||||
|
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(originalUrl, md5)
|
||||||
|
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||||
|
},
|
||||||
|
"subType" to image.picSubType,
|
||||||
|
"type" to if (image.isFlashPic == true) "flash" else if (image.original) "original" else "show"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语音消息转换消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertVoiceElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val record = element.pttElement
|
||||||
|
|
||||||
|
val md5 = if (record.fileName.startsWith("silk"))
|
||||||
|
record.fileName.substring(5)
|
||||||
|
else record.md5HexStr
|
||||||
|
|
||||||
|
return MessageSegment(
|
||||||
|
type = "record",
|
||||||
|
data = hashMapOf(
|
||||||
|
"file" to md5,
|
||||||
|
"url" to when (chatType) {
|
||||||
|
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl(
|
||||||
|
"0",
|
||||||
|
record.md5HexStr,
|
||||||
|
record.fileUuid
|
||||||
|
)
|
||||||
|
|
||||||
|
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
|
||||||
|
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
|
||||||
|
"0",
|
||||||
|
record.md5HexStr,
|
||||||
|
record.fileUuid
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||||
|
}
|
||||||
|
).also {
|
||||||
|
if (record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) {
|
||||||
|
it["magic"] = "1"
|
||||||
|
}
|
||||||
|
if ((it["url"] as String).isBlank()) {
|
||||||
|
it.remove("url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频消息转换消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertVideoElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val video = element.videoElement
|
||||||
|
val md5 = if (video.fileName.contains("/")) {
|
||||||
|
video.videoMd5.takeIf {
|
||||||
|
!it.isNullOrEmpty()
|
||||||
|
}?.hex2ByteArray() ?: video.fileName.split("/").let {
|
||||||
|
it[it.size - 2].hex2ByteArray()
|
||||||
|
}
|
||||||
|
} else video.fileName.split(".")[0].hex2ByteArray()
|
||||||
|
|
||||||
|
//LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
|
||||||
|
|
||||||
|
return MessageSegment(
|
||||||
|
type = "video",
|
||||||
|
data = hashMapOf(
|
||||||
|
"file" to video.fileName,
|
||||||
|
"url" to when (chatType) {
|
||||||
|
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||||
|
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
|
||||||
|
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||||
|
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||||
|
}
|
||||||
|
).also {
|
||||||
|
if ((it["url"] as String).isBlank())
|
||||||
|
it.remove("url")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商城大表情消息转换消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertMarketFaceElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val face = element.marketFaceElement
|
||||||
|
return when (face.emojiId.lowercase()) {
|
||||||
|
"4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
|
||||||
|
"83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
|
||||||
|
else -> MessageSegment(
|
||||||
|
type = "mface",
|
||||||
|
data = hashMapOf(
|
||||||
|
"id" to face.emojiId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON消息转消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertStructJsonElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val data = element.arkElement.bytesData.asJsonObject
|
||||||
|
return when (data["app"].asString) {
|
||||||
|
"com.tencent.multimsg" -> {
|
||||||
|
val info = data["meta"].asJsonObject["detail"].asJsonObject
|
||||||
|
MessageSegment(
|
||||||
|
type = "forward",
|
||||||
|
data = mapOf(
|
||||||
|
"id" to info["resid"].asString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"com.tencent.troopsharecard" -> {
|
||||||
|
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||||||
|
MessageSegment(
|
||||||
|
type = "contact",
|
||||||
|
data = hashMapOf(
|
||||||
|
"type" to "group",
|
||||||
|
"id" to info["jumpUrl"].asString.split("group_code=")[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"com.tencent.contact.lua" -> {
|
||||||
|
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||||||
|
MessageSegment(
|
||||||
|
type = "contact",
|
||||||
|
data = hashMapOf(
|
||||||
|
"type" to "private",
|
||||||
|
"id" to info["jumpUrl"].asString.split("uin=")[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"com.tencent.map" -> {
|
||||||
|
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject
|
||||||
|
MessageSegment(
|
||||||
|
type = "location",
|
||||||
|
data = hashMapOf(
|
||||||
|
"lat" to info["lat"].asString,
|
||||||
|
"lon" to info["lng"].asString,
|
||||||
|
"content" to info["address"].asString,
|
||||||
|
"title" to info["name"].asString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> MessageSegment(
|
||||||
|
type = "json",
|
||||||
|
data = mapOf(
|
||||||
|
"data" to element.arkElement.bytesData.asJsonObject.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回复消息转消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertReplyElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val reply = element.replyElement
|
||||||
|
val msgId = reply.replayMsgId
|
||||||
|
val msgHash = if (msgId != 0L) {
|
||||||
|
MessageHelper.generateMsgIdHash(chatType, msgId)
|
||||||
|
} else {
|
||||||
|
MessageDB.getInstance().messageMappingDao()
|
||||||
|
.queryByMsgSeq(chatType, peerId, reply.replayMsgSeq?.toInt() ?: 0)?.msgHashId
|
||||||
|
?: kotlin.run {
|
||||||
|
LogCenter.log("消息映射关系未找到: Message($reply)", Level.WARN)
|
||||||
|
MessageHelper.generateMsgIdHash(chatType, reply.sourceMsgIdInRecords)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MessageSegment(
|
||||||
|
type = "reply",
|
||||||
|
data = mapOf(
|
||||||
|
"id" to msgHash
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 灰色提示条消息过滤
|
||||||
|
*/
|
||||||
|
private suspend fun convertGrayTipsElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val tip = element.grayTipElement
|
||||||
|
when (tip.subElementType) {
|
||||||
|
MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
|
||||||
|
val notify = tip.jsonGrayTipElement
|
||||||
|
when (notify.busiId) {
|
||||||
|
/* 新人入群 */ 17L, /* 群戳一戳 */1061L,
|
||||||
|
/* 群撤回 */1014L, /* 群设精消息 */2401L,
|
||||||
|
/* 群头衔 */2407L -> {
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
|
||||||
|
val notify = tip.xmlElement
|
||||||
|
when (notify.busiId) {
|
||||||
|
/* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
|
||||||
|
else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
|
||||||
|
}
|
||||||
|
// 提示类消息,这里提供的是一个xml,不具备解析通用性
|
||||||
|
// 在这里不推送
|
||||||
|
throw UnknownError()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件消息转换消息段
|
||||||
|
*/
|
||||||
|
private suspend fun convertFileElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val fileMsg = element.fileElement
|
||||||
|
val fileName = fileMsg.fileName
|
||||||
|
val fileSize = fileMsg.fileSize
|
||||||
|
val expireTime = fileMsg.expireTime ?: 0
|
||||||
|
val fileId = fileMsg.fileUuid
|
||||||
|
val bizId = fileMsg.fileBizId ?: 0
|
||||||
|
val fileSubId = fileMsg.fileSubId ?: ""
|
||||||
|
val url = when (chatType) {
|
||||||
|
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
||||||
|
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
|
||||||
|
else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MessageSegment(
|
||||||
|
type = "file",
|
||||||
|
data = mapOf(
|
||||||
|
"name" to fileName,
|
||||||
|
"size" to fileSize,
|
||||||
|
"expire" to expireTime,
|
||||||
|
"id" to fileId,
|
||||||
|
"url" to url,
|
||||||
|
"biz" to bizId,
|
||||||
|
"sub" to fileSubId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 老板QQ的合并转发信息
|
||||||
|
*/
|
||||||
|
private suspend fun convertXmlMultiMsgElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val multiMsg = element.multiForwardMsgElement
|
||||||
|
return MessageSegment(
|
||||||
|
type = "forward",
|
||||||
|
data = mapOf(
|
||||||
|
"id" to multiMsg.resId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun convertXmlLongMsgElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val longMsg = element.structLongMsgElement
|
||||||
|
return MessageSegment(
|
||||||
|
type = "forward",
|
||||||
|
data = mapOf(
|
||||||
|
"id" to longMsg.resId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun convertMarkdownElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val markdown = element.markdownElement
|
||||||
|
return MessageSegment(
|
||||||
|
type = "markdown",
|
||||||
|
data = mapOf(
|
||||||
|
"content" to markdown.content
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun convertBubbleFaceElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val bubbleElement = element.faceBubbleElement
|
||||||
|
return MessageSegment(
|
||||||
|
type = "bubble_face",
|
||||||
|
data = mapOf(
|
||||||
|
"id" to bubbleElement.yellowFaceInfo.index,
|
||||||
|
"count" to (bubbleElement.faceCount ?: 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun convertInlineKeyboardElem(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeer: String,
|
||||||
|
element: MsgElement
|
||||||
|
): MessageSegment {
|
||||||
|
val keyboard = element.inlineKeyboardElement
|
||||||
|
return MessageSegment(
|
||||||
|
type = "inline_keyboard",
|
||||||
|
data = mapOf(
|
||||||
|
"data" to buildJsonObject {
|
||||||
|
putJsonArray("rows") {
|
||||||
|
keyboard.rows.forEach { row ->
|
||||||
|
add(buildJsonObject row@{
|
||||||
|
putJsonArray("buttons") {
|
||||||
|
row.buttons.forEach { button ->
|
||||||
|
add(buildJsonObject {
|
||||||
|
put("id", button.id ?: "")
|
||||||
|
put("label", button.label ?: "")
|
||||||
|
put("visited_label", button.visitedLabel ?: "")
|
||||||
|
put("style", button.style)
|
||||||
|
put("type", button.type)
|
||||||
|
put("click_limit", button.clickLimit)
|
||||||
|
put("unsupport_tips", button.unsupportTips ?: "")
|
||||||
|
put("data", button.data)
|
||||||
|
put("at_bot_show_channel_list", button.atBotShowChannelList)
|
||||||
|
put("permission_type", button.permissionType)
|
||||||
|
putJsonArray("specify_role_ids") {
|
||||||
|
button.specifyRoleIds?.forEach { add(it) }
|
||||||
|
}
|
||||||
|
putJsonArray("specify_tinyids") {
|
||||||
|
button.specifyTinyids?.forEach { add(it) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put("bot_appid", keyboard.botAppid)
|
||||||
|
}.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.msg
|
package moe.fuqiuluo.qqinterface.servlet.msg.msgelement
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
@ -8,22 +8,7 @@ import com.tencent.mobileqq.pb.ByteStringMicro
|
|||||||
import com.tencent.mobileqq.qroute.QRoute
|
import com.tencent.mobileqq.qroute.QRoute
|
||||||
import com.tencent.qphone.base.remote.ToServiceMsg
|
import com.tencent.qphone.base.remote.ToServiceMsg
|
||||||
import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
|
import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.ArkElement
|
import com.tencent.qqnt.kernel.nativeinterface.*
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.FaceBubbleElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.FaceElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MarkdownElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceSupportSize
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.PicElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.PttElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.ReplyElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.SmallYellowFaceInfo
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.TextElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.VideoElement
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@ -34,6 +19,7 @@ import moe.fuqiuluo.qqinterface.servlet.LbsSvc
|
|||||||
import moe.fuqiuluo.qqinterface.servlet.ark.ArkAppInfo
|
import moe.fuqiuluo.qqinterface.servlet.ark.ArkAppInfo
|
||||||
import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
|
import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc
|
import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.transfile.*
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.FileTransfer
|
import moe.fuqiuluo.qqinterface.servlet.transfile.FileTransfer
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource
|
import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.Private
|
import moe.fuqiuluo.qqinterface.servlet.transfile.Private
|
||||||
@ -41,8 +27,6 @@ import moe.fuqiuluo.qqinterface.servlet.transfile.Transfer
|
|||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.Troop
|
import moe.fuqiuluo.qqinterface.servlet.transfile.Troop
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.VideoResource
|
import moe.fuqiuluo.qqinterface.servlet.transfile.VideoResource
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.VoiceResource
|
import moe.fuqiuluo.qqinterface.servlet.transfile.VoiceResource
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.trans
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.with
|
|
||||||
import moe.fuqiuluo.shamrock.helper.ActionMsgException
|
import moe.fuqiuluo.shamrock.helper.ActionMsgException
|
||||||
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
||||||
import moe.fuqiuluo.shamrock.helper.IllegalParamsException
|
import moe.fuqiuluo.shamrock.helper.IllegalParamsException
|
||||||
@ -53,14 +37,7 @@ import moe.fuqiuluo.shamrock.helper.LogicException
|
|||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||||
import moe.fuqiuluo.shamrock.helper.MusicHelper
|
import moe.fuqiuluo.shamrock.helper.MusicHelper
|
||||||
import moe.fuqiuluo.shamrock.helper.ParamsException
|
import moe.fuqiuluo.shamrock.helper.ParamsException
|
||||||
import moe.fuqiuluo.shamrock.tools.asBooleanOrNull
|
import moe.fuqiuluo.shamrock.tools.*
|
||||||
import moe.fuqiuluo.shamrock.tools.asInt
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asIntOrNull
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asLong
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asString
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asStringOrNull
|
|
||||||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
|
||||||
import moe.fuqiuluo.shamrock.utils.AudioUtils
|
import moe.fuqiuluo.shamrock.utils.AudioUtils
|
||||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||||
import moe.fuqiuluo.shamrock.utils.MediaType
|
import moe.fuqiuluo.shamrock.utils.MediaType
|
||||||
@ -77,40 +54,101 @@ import kotlin.math.roundToInt
|
|||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlin.random.nextInt
|
import kotlin.random.nextInt
|
||||||
|
|
||||||
internal typealias IMaker = suspend (Int, Long, String, JsonObject) -> Result<MsgElement>
|
internal typealias IMsgElementMaker = suspend (Int, Long, String, JsonObject) -> Result<MsgElement>
|
||||||
|
|
||||||
internal object MessageMaker {
|
internal object MsgElementMaker {
|
||||||
private val makerArray = mutableMapOf(
|
private val makerMap = hashMapOf(
|
||||||
"text" to MessageMaker::createTextElem,
|
"text" to MsgElementMaker::createTextElem,
|
||||||
"face" to MessageMaker::createFaceElem,
|
"face" to MsgElementMaker::createFaceElem,
|
||||||
"pic" to MessageMaker::createImageElem,
|
"pic" to MsgElementMaker::createImageElem,
|
||||||
"image" to MessageMaker::createImageElem,
|
"image" to MsgElementMaker::createImageElem,
|
||||||
"voice" to MessageMaker::createRecordElem,
|
"voice" to MsgElementMaker::createRecordElem,
|
||||||
"record" to MessageMaker::createRecordElem,
|
"record" to MsgElementMaker::createRecordElem,
|
||||||
"at" to MessageMaker::createAtElem,
|
"at" to MsgElementMaker::createAtElem,
|
||||||
"video" to MessageMaker::createVideoElem,
|
"video" to MsgElementMaker::createVideoElem,
|
||||||
"markdown" to MessageMaker::createMarkdownElem,
|
"markdown" to MsgElementMaker::createMarkdownElem,
|
||||||
"dice" to MessageMaker::createDiceElem,
|
"dice" to MsgElementMaker::createDiceElem,
|
||||||
"rps" to MessageMaker::createRpsElem,
|
"rps" to MsgElementMaker::createRpsElem,
|
||||||
"poke" to MessageMaker::createPokeElem,
|
"poke" to MsgElementMaker::createPokeElem,
|
||||||
"anonymous" to MessageMaker::createAnonymousElem,
|
"anonymous" to MsgElementMaker::createAnonymousElem,
|
||||||
"share" to MessageMaker::createShareElem,
|
"share" to MsgElementMaker::createShareElem,
|
||||||
"contact" to MessageMaker::createContactElem,
|
"contact" to MsgElementMaker::createContactElem,
|
||||||
"location" to MessageMaker::createLocationElem,
|
"location" to MsgElementMaker::createLocationElem,
|
||||||
"music" to MessageMaker::createMusicElem,
|
"music" to MsgElementMaker::createMusicElem,
|
||||||
"reply" to MessageMaker::createReplyElem,
|
"reply" to MsgElementMaker::createReplyElem,
|
||||||
"touch" to MessageMaker::createTouchElem,
|
"touch" to MsgElementMaker::createTouchElem,
|
||||||
"weather" to MessageMaker::createWeatherElem,
|
"weather" to MsgElementMaker::createWeatherElem,
|
||||||
"json" to MessageMaker::createJsonElem,
|
"json" to MsgElementMaker::createJsonElem,
|
||||||
"new_dice" to MessageMaker::createNewDiceElem,
|
"new_dice" to MsgElementMaker::createNewDiceElem,
|
||||||
"new_rps" to MessageMaker::createNewRpsElem,
|
"new_rps" to MsgElementMaker::createNewRpsElem,
|
||||||
"basketball" to MessageMaker::createBasketballElem,
|
"basketball" to MsgElementMaker::createBasketballElem,
|
||||||
//"node" to MessageMaker::createNodeElem,
|
//"node" to MessageMaker::createNodeElem,
|
||||||
//"multi_msg" to MessageMaker::createLongMsgStruct,
|
//"multi_msg" to MessageMaker::createLongMsgStruct,
|
||||||
"bubble_face" to MessageMaker::createBubbleFaceElem,
|
"bubble_face" to MsgElementMaker::createBubbleFaceElem,
|
||||||
|
"inline_keyboard" to MsgElementMaker::createInlineKeywordElem
|
||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun createBubbleFaceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
operator fun get(type: String): IMsgElementMaker? = makerMap[type]
|
||||||
|
|
||||||
|
private suspend fun createInlineKeywordElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
||||||
|
fun tryNewKeyboardButton(btn: JsonObject): InlineKeyboardButton {
|
||||||
|
return runCatching {
|
||||||
|
InlineKeyboardButton(
|
||||||
|
btn["id"].asString,
|
||||||
|
btn["label"].asString,
|
||||||
|
btn["visited_label"].asString,
|
||||||
|
btn["style"].asInt,
|
||||||
|
btn["type"].asInt,
|
||||||
|
btn["click_limit"].asInt,
|
||||||
|
btn["unsupport_tips"].asString,
|
||||||
|
btn["data"].asString,
|
||||||
|
btn["at_bot_show_channel_list"].asBoolean,
|
||||||
|
btn["permission_type"].asInt,
|
||||||
|
ArrayList(btn["specify_role_ids"].asJsonArray.map { it.asString }),
|
||||||
|
ArrayList(btn["specify_tinyids"].asJsonArray.map { it.asString }),
|
||||||
|
false, 0, false, arrayListOf()
|
||||||
|
)
|
||||||
|
}.getOrElse {
|
||||||
|
InlineKeyboardButton(
|
||||||
|
btn["id"].asString,
|
||||||
|
btn["label"].asString,
|
||||||
|
btn["visited_label"].asString,
|
||||||
|
btn["style"].asInt,
|
||||||
|
btn["type"].asInt,
|
||||||
|
btn["click_limit"].asInt,
|
||||||
|
btn["unsupport_tips"].asString,
|
||||||
|
btn["data"].asString,
|
||||||
|
btn["at_bot_show_channel_list"].asBoolean,
|
||||||
|
btn["permission_type"].asInt,
|
||||||
|
ArrayList(btn["specify_role_ids"].asJsonArray.map { it.asString }),
|
||||||
|
ArrayList(btn["specify_tinyids"].asJsonArray.map { it.asString }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val elem = MsgElement()
|
||||||
|
elem.elementType = MsgConstant.KELEMTYPEINLINEKEYBOARD
|
||||||
|
val rows = arrayListOf<InlineKeyboardRow>()
|
||||||
|
|
||||||
|
val keyboard = Json.parseToJsonElement(data["data"].asString).asJsonObject
|
||||||
|
keyboard["rows"].asJsonArray.forEach {
|
||||||
|
val row = it.asJsonObject
|
||||||
|
val buttons = arrayListOf<InlineKeyboardButton>()
|
||||||
|
row["buttons"].asJsonArray.forEach { button ->
|
||||||
|
val btn = button.asJsonObject
|
||||||
|
buttons.add(tryNewKeyboardButton(btn))
|
||||||
|
}
|
||||||
|
rows.add(InlineKeyboardRow(buttons))
|
||||||
|
}
|
||||||
|
elem.inlineKeyboardElement = InlineKeyboardElement(rows, keyboard["bot_appid"].asLong)
|
||||||
|
return Result.success(elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createBubbleFaceElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
data.checkAndThrow("id", "count")
|
data.checkAndThrow("id", "count")
|
||||||
val faceId = data["id"].asInt
|
val faceId = data["id"].asInt
|
||||||
val local = QQSysFaceUtil.convertToLocal(faceId)
|
val local = QQSysFaceUtil.convertToLocal(faceId)
|
||||||
@ -143,21 +181,13 @@ internal object MessageMaker {
|
|||||||
// SendForwardMessage(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin(), data["content"].asJsonArray)
|
// SendForwardMessage(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin(), data["content"].asJsonArray)
|
||||||
//
|
//
|
||||||
// }
|
// }
|
||||||
/**\
|
|
||||||
* msgElement.setFaceElement(new FaceElement());
|
private suspend fun createBasketballElem(
|
||||||
* msgElement.getFaceElement().setFaceIndex(114);
|
chatType: Int,
|
||||||
* msgElement.getFaceElement().setFaceText("/篮球");
|
msgId: Long,
|
||||||
* msgElement.getFaceElement().setFaceType(3);
|
peerId: String,
|
||||||
* msgElement.getFaceElement().setPackId("1");
|
data: JsonObject
|
||||||
* msgElement.getFaceElement().setStickerId("13");
|
): Result<MsgElement> {
|
||||||
* msgElement.getFaceElement().setRandomType(1);
|
|
||||||
* msgElement.getFaceElement().setImageType(1);
|
|
||||||
* msgElement.getFaceElement().setStickerType(2);
|
|
||||||
* msgElement.getFaceElement().setSourceType(1);
|
|
||||||
* msgElement.getFaceElement().setSurpriseId("");
|
|
||||||
* msgElement.getFaceElement().setResultId(String.valueOf(new Random().nextInt(5) + 1));
|
|
||||||
*/
|
|
||||||
private suspend fun createBasketballElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
|
||||||
val elem = MsgElement()
|
val elem = MsgElement()
|
||||||
elem.elementType = MsgConstant.KELEMTYPEFACE
|
elem.elementType = MsgConstant.KELEMTYPEFACE
|
||||||
val face = FaceElement()
|
val face = FaceElement()
|
||||||
@ -168,14 +198,19 @@ internal object MessageMaker {
|
|||||||
face.stickerId = "13"
|
face.stickerId = "13"
|
||||||
face.sourceType = 1
|
face.sourceType = 1
|
||||||
face.stickerType = 2
|
face.stickerType = 2
|
||||||
face.resultId = Random.nextInt(1 .. 5).toString()
|
face.resultId = Random.nextInt(1..5).toString()
|
||||||
face.surpriseId = ""
|
face.surpriseId = ""
|
||||||
face.randomType = 1
|
face.randomType = 1
|
||||||
elem.faceElement = face
|
elem.faceElement = face
|
||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createNewRpsElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createNewRpsElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
val elem = MsgElement()
|
val elem = MsgElement()
|
||||||
elem.elementType = MsgConstant.KELEMTYPEFACE
|
elem.elementType = MsgConstant.KELEMTYPEFACE
|
||||||
val face = FaceElement()
|
val face = FaceElement()
|
||||||
@ -193,7 +228,12 @@ internal object MessageMaker {
|
|||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createNewDiceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createNewDiceElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
val elem = MsgElement()
|
val elem = MsgElement()
|
||||||
elem.elementType = MsgConstant.KELEMTYPEFACE
|
elem.elementType = MsgConstant.KELEMTYPEFACE
|
||||||
val face = FaceElement()
|
val face = FaceElement()
|
||||||
@ -311,7 +351,7 @@ internal object MessageMaker {
|
|||||||
LogCenter.log("无法获取被回复消息", Level.ERROR)
|
LogCenter.log("无法获取被回复消息", Level.ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(data.containsKey("text")) {
|
if (data.containsKey("text")) {
|
||||||
data.checkAndThrow("qq", "time", "seq")
|
data.checkAndThrow("qq", "time", "seq")
|
||||||
reply.replayMsgSeq = data["seq"].asLong
|
reply.replayMsgSeq = data["seq"].asLong
|
||||||
reply.sourceMsgText = data["text"].asString
|
reply.sourceMsgText = data["text"].asString
|
||||||
@ -330,21 +370,23 @@ internal object MessageMaker {
|
|||||||
): Result<MsgElement> {
|
): Result<MsgElement> {
|
||||||
data.checkAndThrow("type")
|
data.checkAndThrow("type")
|
||||||
|
|
||||||
when(val type = data["type"].asString) {
|
when (val type = data["type"].asString) {
|
||||||
"qq" -> {
|
"qq" -> {
|
||||||
data.checkAndThrow("id")
|
data.checkAndThrow("id")
|
||||||
val id = data["id"].asString
|
val id = data["id"].asString
|
||||||
if(!MusicHelper.tryShareQQMusicById(chatType, peerId.toLong(), msgId, id)) {
|
if (!MusicHelper.tryShareQQMusicById(chatType, peerId.toLong(), msgId, id)) {
|
||||||
LogCenter.log("无法发送QQ音乐分享", Level.ERROR)
|
LogCenter.log("无法发送QQ音乐分享", Level.ERROR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"163" -> {
|
"163" -> {
|
||||||
data.checkAndThrow("id")
|
data.checkAndThrow("id")
|
||||||
val id = data["id"].asString
|
val id = data["id"].asString
|
||||||
if(!MusicHelper.tryShare163MusicById(chatType, peerId.toLong(), msgId, id)) {
|
if (!MusicHelper.tryShare163MusicById(chatType, peerId.toLong(), msgId, id)) {
|
||||||
LogCenter.log("无法发送网易云音乐分享", Level.ERROR)
|
LogCenter.log("无法发送网易云音乐分享", Level.ERROR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"custom" -> {
|
"custom" -> {
|
||||||
data.checkAndThrow("url", "audio", "title")
|
data.checkAndThrow("url", "audio", "title")
|
||||||
ArkMsgSvc.tryShareMusic(
|
ArkMsgSvc.tryShareMusic(
|
||||||
@ -359,13 +401,19 @@ internal object MessageMaker {
|
|||||||
data["audio"].asString
|
data["audio"].asString
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> LogCenter.log("不支持的音乐分享类型: $type", Level.ERROR)
|
else -> LogCenter.log("不支持的音乐分享类型: $type", Level.ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.failure(ActionMsgException)
|
return Result.failure(ActionMsgException)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createLocationElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createLocationElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
data.checkAndThrow("lat", "lon")
|
data.checkAndThrow("lat", "lon")
|
||||||
|
|
||||||
val lat = data["lat"].asString.toDouble()
|
val lat = data["lat"].asString.toDouble()
|
||||||
@ -378,7 +426,12 @@ internal object MessageMaker {
|
|||||||
return Result.failure(ActionMsgException)
|
return Result.failure(ActionMsgException)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createContactElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createContactElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
data.checkAndThrow("id")
|
data.checkAndThrow("id")
|
||||||
val type = data["type"].asStringOrNull ?: data["kind"].asStringOrNull
|
val type = data["type"].asStringOrNull ?: data["kind"].asStringOrNull
|
||||||
val id = data["id"].asString
|
val id = data["id"].asString
|
||||||
@ -389,10 +442,12 @@ internal object MessageMaker {
|
|||||||
val ark = ArkElement(CardSvc.getSharePrivateArkMsg(id.toLong()), null, null)
|
val ark = ArkElement(CardSvc.getSharePrivateArkMsg(id.toLong()), null, null)
|
||||||
elem.arkElement = ark
|
elem.arkElement = ark
|
||||||
}
|
}
|
||||||
|
|
||||||
"group" -> {
|
"group" -> {
|
||||||
val ark = ArkElement(GroupSvc.getShareTroopArkMsg(id.toLong()), null, null)
|
val ark = ArkElement(GroupSvc.getShareTroopArkMsg(id.toLong()), null, null)
|
||||||
elem.arkElement = ark
|
elem.arkElement = ark
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> throw IllegalParamsException("type")
|
else -> throw IllegalParamsException("type")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -401,7 +456,12 @@ internal object MessageMaker {
|
|||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createShareElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createShareElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
data.checkAndThrow("title", "url")
|
data.checkAndThrow("title", "url")
|
||||||
|
|
||||||
val url = data["url"].asString
|
val url = data["url"].asString
|
||||||
@ -466,11 +526,21 @@ internal object MessageMaker {
|
|||||||
return Result.failure(ActionMsgException)
|
return Result.failure(ActionMsgException)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createAnonymousElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createAnonymousElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
return Result.failure(ActionMsgException)
|
return Result.failure(ActionMsgException)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createPokeElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createPokeElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
data.checkAndThrow("type", "id")
|
data.checkAndThrow("type", "id")
|
||||||
val elem = MsgElement()
|
val elem = MsgElement()
|
||||||
val face = FaceElement()
|
val face = FaceElement()
|
||||||
@ -485,8 +555,8 @@ internal object MessageMaker {
|
|||||||
face.vaspokeName = ""
|
face.vaspokeName = ""
|
||||||
face.vaspokeMinver = ""
|
face.vaspokeMinver = ""
|
||||||
face.pokeStrength = (data["strength"].asIntOrNull ?: data["cnt"].asIntOrNull
|
face.pokeStrength = (data["strength"].asIntOrNull ?: data["cnt"].asIntOrNull
|
||||||
?: data["count"].asIntOrNull ?: data["time"].asIntOrNull ?: 0).also {
|
?: data["count"].asIntOrNull ?: data["time"].asIntOrNull ?: 0).also {
|
||||||
if(it < 0 || it > 3) throw IllegalParamsException("strength")
|
if (it < 0 || it > 3) throw IllegalParamsException("strength")
|
||||||
}
|
}
|
||||||
face.msgType = 0
|
face.msgType = 0
|
||||||
face.faceBubbleCount = 0
|
face.faceBubbleCount = 0
|
||||||
@ -497,7 +567,12 @@ internal object MessageMaker {
|
|||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createFaceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createFaceElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
data.checkAndThrow("id")
|
data.checkAndThrow("id")
|
||||||
|
|
||||||
val serverId = data["id"].asInt
|
val serverId = data["id"].asInt
|
||||||
@ -516,15 +591,15 @@ internal object MessageMaker {
|
|||||||
face.faceIndex = serverId
|
face.faceIndex = serverId
|
||||||
face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId))
|
face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId))
|
||||||
if (serverId == 394) {
|
if (serverId == 394) {
|
||||||
face.stickerId = 40.toString()
|
face.stickerId = "40"
|
||||||
face.packId = "1"
|
face.packId = "1"
|
||||||
face.sourceType = 1
|
face.sourceType = 1
|
||||||
face.stickerType = 3
|
face.stickerType = 3
|
||||||
face.randomType = 1
|
face.randomType = 1
|
||||||
face.resultId = data["result"].asStringOrNull ?: Random.nextInt(1 .. 5).toString()
|
face.resultId = data["result"].asStringOrNull ?: Random.nextInt(1..5).toString()
|
||||||
} else if (big) {
|
} else if (big) {
|
||||||
face.imageType = 0
|
face.imageType = 0
|
||||||
face.stickerId = 30.toString()
|
face.stickerId = "30"
|
||||||
face.packId = "1"
|
face.packId = "1"
|
||||||
face.sourceType = 1
|
face.sourceType = 1
|
||||||
face.stickerType = 1
|
face.stickerType = 1
|
||||||
@ -538,7 +613,12 @@ internal object MessageMaker {
|
|||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createRpsElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createRpsElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
val elem = MsgElement()
|
val elem = MsgElement()
|
||||||
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
|
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
|
||||||
val market = MarketFaceElement(
|
val market = MarketFaceElement(
|
||||||
@ -553,7 +633,12 @@ internal object MessageMaker {
|
|||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createDiceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createDiceElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
val elem = MsgElement()
|
val elem = MsgElement()
|
||||||
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
|
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
|
||||||
val market = MarketFaceElement(
|
val market = MarketFaceElement(
|
||||||
@ -568,22 +653,32 @@ internal object MessageMaker {
|
|||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createMarkdownElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createMarkdownElem(
|
||||||
data.checkAndThrow("text")
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
|
data.checkAndThrow("content")
|
||||||
val elem = MsgElement()
|
val elem = MsgElement()
|
||||||
elem.elementType = MsgConstant.KELEMTYPEMARKDOWN
|
elem.elementType = MsgConstant.KELEMTYPEMARKDOWN
|
||||||
val markdown = MarkdownElement(data["text"].asString)
|
val markdown = MarkdownElement(data["content"].asString)
|
||||||
elem.markdownElement = markdown
|
elem.markdownElement = markdown
|
||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createVideoElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createVideoElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
data.checkAndThrow("file")
|
data.checkAndThrow("file")
|
||||||
|
|
||||||
val file = data["file"].asString.let {
|
val file = data["file"].asString.let {
|
||||||
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
|
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
|
||||||
var file = if (md5.length == 32) {
|
var file = if (md5.length == 32) {
|
||||||
FileUtils.getFile(it)
|
FileUtils.getFileByMd5(it)
|
||||||
} else {
|
} else {
|
||||||
FileUtils.parseAndSave(it)
|
FileUtils.parseAndSave(it)
|
||||||
}
|
}
|
||||||
@ -613,7 +708,8 @@ internal object MessageMaker {
|
|||||||
)
|
)
|
||||||
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
|
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
|
||||||
originalPath
|
originalPath
|
||||||
) != file.length()) {
|
) != file.length()
|
||||||
|
) {
|
||||||
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
||||||
AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!)
|
AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!)
|
||||||
}
|
}
|
||||||
@ -652,35 +748,43 @@ internal object MessageMaker {
|
|||||||
val qq = data["qq"].asString
|
val qq = data["qq"].asString
|
||||||
|
|
||||||
val at = TextElement()
|
val at = TextElement()
|
||||||
when(qq) {
|
when (qq) {
|
||||||
"0", "all" -> {
|
"0", "all" -> {
|
||||||
at.content = "@全体成员"
|
at.content = "@全体成员"
|
||||||
at.atType = MsgConstant.ATTYPEALL
|
at.atType = MsgConstant.ATTYPEALL
|
||||||
at.atNtUid = "0"
|
at.atNtUid = "0"
|
||||||
}
|
}
|
||||||
"online" -> {
|
|
||||||
at.content = "@在线成员"
|
|
||||||
at.atType = MsgConstant.ATTYPEONLINE
|
|
||||||
at.atNtUid = "0"
|
|
||||||
}
|
|
||||||
"admin" -> {
|
"admin" -> {
|
||||||
at.content = "@管理员"
|
at.content = "@管理员"
|
||||||
at.atRoleId = 1
|
at.atRoleId = 1
|
||||||
at.atType = MsgConstant.ATTYPEROLE
|
at.atType = MsgConstant.ATTYPEROLE
|
||||||
at.atNtUid = "0"
|
at.atNtUid = "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"online" -> {
|
||||||
|
at.content = "@在线成员"
|
||||||
|
at.atType = MsgConstant.ATTYPEONLINE
|
||||||
|
at.atNtUid = "0"
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val info = GroupSvc.getTroopMemberInfoByUin(peerId, qq, true).onFailure {
|
val name = data["name"].asStringOrNull
|
||||||
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
|
if (name == null) {
|
||||||
}.getOrNull()
|
val info = GroupSvc.getTroopMemberInfoByUinV2(peerId, qq, true).onFailure {
|
||||||
if (info != null) {
|
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
|
||||||
at.content = "@${
|
}.getOrNull()
|
||||||
info.troopnick
|
if (info != null) {
|
||||||
.ifNullOrEmpty(info.friendnick)
|
at.content = "@${
|
||||||
.ifNullOrEmpty(qq)
|
info.troopnick
|
||||||
}"
|
.ifNullOrEmpty(info.friendnick)
|
||||||
|
.ifNullOrEmpty(qq)
|
||||||
|
}"
|
||||||
|
} else {
|
||||||
|
at.content = "@$qq"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
at.content = "@${data["name"].asStringOrNull.ifNullOrEmpty(qq)}"
|
at.content = "@$name"
|
||||||
}
|
}
|
||||||
at.atType = MsgConstant.ATTYPEONE
|
at.atType = MsgConstant.ATTYPEONE
|
||||||
at.atNtUid = ContactHelper.getUidByUinAsync(qq.toLong())
|
at.atNtUid = ContactHelper.getUidByUinAsync(qq.toLong())
|
||||||
@ -693,7 +797,12 @@ internal object MessageMaker {
|
|||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createRecordElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createRecordElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
var file = data["file"].asStringOrNull?.let {
|
var file = data["file"].asStringOrNull?.let {
|
||||||
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "")
|
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "")
|
||||||
.replace(" ", "")
|
.replace(" ", "")
|
||||||
@ -721,11 +830,13 @@ internal object MessageMaker {
|
|||||||
ptt.duration = QRoute.api(IAIOPttApi::class.java)
|
ptt.duration = QRoute.api(IAIOPttApi::class.java)
|
||||||
.getPttFileDuration(file.absolutePath)
|
.getPttFileDuration(file.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaType.Amr -> {
|
MediaType.Amr -> {
|
||||||
LogCenter.log({ "Amr: $file" }, Level.DEBUG)
|
LogCenter.log({ "Amr: $file" }, Level.DEBUG)
|
||||||
ptt.duration = AudioUtils.getDurationSec(file)
|
ptt.duration = AudioUtils.getDurationSec(file)
|
||||||
ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
|
ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaType.Pcm -> {
|
MediaType.Pcm -> {
|
||||||
LogCenter.log({ "Pcm To Silk: $file" }, Level.DEBUG)
|
LogCenter.log({ "Pcm To Silk: $file" }, Level.DEBUG)
|
||||||
val result = AudioUtils.pcmToSilk(file)
|
val result = AudioUtils.pcmToSilk(file)
|
||||||
@ -733,6 +844,7 @@ internal object MessageMaker {
|
|||||||
file = result.first
|
file = result.first
|
||||||
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LogCenter.log({ "Audio To SILK: $file" }, Level.DEBUG)
|
LogCenter.log({ "Audio To SILK: $file" }, Level.DEBUG)
|
||||||
val result = AudioUtils.audioToSilk(file)
|
val result = AudioUtils.audioToSilk(file)
|
||||||
@ -749,12 +861,13 @@ internal object MessageMaker {
|
|||||||
// QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
// QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
||||||
//}
|
//}
|
||||||
|
|
||||||
if(!(Transfer with when (chatType) {
|
if (!(Transfer with when (chatType) {
|
||||||
MsgConstant.KCHATTYPEGROUP -> Troop(peerId)
|
MsgConstant.KCHATTYPEGROUP -> Troop(peerId)
|
||||||
MsgConstant.KCHATTYPEC2C -> Private(peerId)
|
MsgConstant.KCHATTYPEC2C -> Private(peerId)
|
||||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Private(peerId)
|
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Private(peerId)
|
||||||
else -> error("Not supported chatType($chatType) for RecordMsg")
|
else -> error("Not supported chatType($chatType) for RecordMsg")
|
||||||
} trans VoiceResource(file))) {
|
} trans VoiceResource(file))
|
||||||
|
) {
|
||||||
return Result.failure(RuntimeException("上传语音失败: $file"))
|
return Result.failure(RuntimeException("上传语音失败: $file"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -784,16 +897,23 @@ internal object MessageMaker {
|
|||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createImageElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createImageElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
val isOriginal = data["original"].asBooleanOrNull ?: true
|
val isOriginal = data["original"].asBooleanOrNull ?: true
|
||||||
val isFlash = data["flash"].asBooleanOrNull ?: false
|
val isFlash = data["flash"].asBooleanOrNull ?: false
|
||||||
val filePath = data["file"].asStringOrNull
|
val filePath = data["file"].asStringOrNull
|
||||||
val url = data["url"].asStringOrNull
|
val url = data["url"].asStringOrNull
|
||||||
var file: File? = null
|
var file: File? = null
|
||||||
if (filePath != null) {
|
if (filePath != null) {
|
||||||
val md5 = filePath.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
|
val md5 = filePath
|
||||||
|
.replace(regex = "[{}\\-]".toRegex(), replacement = "")
|
||||||
|
.split(".")[0].lowercase()
|
||||||
file = if (md5.length == 32) {
|
file = if (md5.length == 32) {
|
||||||
FileUtils.getFile(md5)
|
FileUtils.getFileByMd5(md5)
|
||||||
} else {
|
} else {
|
||||||
FileUtils.parseAndSave(filePath)
|
FileUtils.parseAndSave(filePath)
|
||||||
}
|
}
|
||||||
@ -825,7 +945,8 @@ internal object MessageMaker {
|
|||||||
)
|
)
|
||||||
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
|
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
|
||||||
originalPath
|
originalPath
|
||||||
) != file.length()) {
|
) != file.length()
|
||||||
|
) {
|
||||||
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
|
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
|
||||||
RichMediaFilePathInfo(
|
RichMediaFilePathInfo(
|
||||||
2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true
|
2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true
|
||||||
@ -863,7 +984,12 @@ internal object MessageMaker {
|
|||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createTextElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
private suspend fun createTextElem(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
peerId: String,
|
||||||
|
data: JsonObject
|
||||||
|
): Result<MsgElement> {
|
||||||
data.checkAndThrow("text")
|
data.checkAndThrow("text")
|
||||||
val elem = MsgElement()
|
val elem = MsgElement()
|
||||||
elem.elementType = MsgConstant.KELEMTYPETEXT
|
elem.elementType = MsgConstant.KELEMTYPETEXT
|
||||||
@ -878,6 +1004,4 @@ internal object MessageMaker {
|
|||||||
if (!containsKey(it)) throw ParamsException(it)
|
if (!containsKey(it)) throw ParamsException(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun get(type: String): IMaker? = makerArray[type]
|
|
||||||
}
|
}
|
@ -19,18 +19,35 @@ import moe.fuqiuluo.shamrock.tools.slice
|
|||||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
||||||
|
import mqq.app.MobileQQ
|
||||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
|
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
|
||||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
|
import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
|
||||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
|
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
|
||||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody
|
import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody
|
||||||
import mqq.app.MobileQQ
|
|
||||||
import tencent.im.cs.cmd0x346.cmd0x346
|
import tencent.im.cs.cmd0x346.cmd0x346
|
||||||
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
|
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
|
||||||
import tencent.im.oidb.cmd0xe37.cmd0xe37
|
import tencent.im.oidb.cmd0xe37.cmd0xe37
|
||||||
import tencent.im.oidb.oidb_sso
|
import tencent.im.oidb.oidb_sso
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
private const val GPRO_PIC = "gchat.qpic.cn"
|
||||||
|
private const val GPRO_PIC_NT = "multimedia.nt.qq.com.cn"
|
||||||
|
private const val C2C_PIC = "c2cpicdw.qpic.cn"
|
||||||
|
|
||||||
internal object RichProtoSvc: BaseSvc() {
|
internal object RichProtoSvc: BaseSvc() {
|
||||||
|
var multiMediaRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
|
||||||
|
/*@Deprecated("Use RichProtoSvc.getQQDns instead", ReplaceWith("getQQDns(domain)"))
|
||||||
|
fun getQQDns(domain: String) {
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putString("domain", "xxx")
|
||||||
|
bundle.putInt("businessType", 1)
|
||||||
|
val result = BinderMethodProxy
|
||||||
|
.callServer(QIPCClientHelper.getInstance().client, "InnerDnsModule", "reqDomain2IpList", bundle)
|
||||||
|
if (result.isSuccess) {
|
||||||
|
val ipList: ArrayList<IpData> = result.data.getParcelableArrayList("ip")!!
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
|
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
|
||||||
val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, ProtoBuf.encodeToByteArray(
|
val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, ProtoBuf.encodeToByteArray(
|
||||||
Oidb0xfc2ReqBody(
|
Oidb0xfc2ReqBody(
|
||||||
@ -142,24 +159,53 @@ internal object RichProtoSvc: BaseSvc() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getGroupPicDownUrl(
|
fun getGroupPicDownUrl(
|
||||||
md5: String
|
originalUrl: String,
|
||||||
|
md5: String,
|
||||||
): String {
|
): String {
|
||||||
return "http://gchat.qpic.cn/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2"
|
val isNtServer = originalUrl.startsWith("/download")
|
||||||
|
val domain = if (isNtServer) GPRO_PIC_NT else GPRO_PIC
|
||||||
|
if (originalUrl.isNotEmpty()) {
|
||||||
|
if (isNtServer && !originalUrl.contains("rkey=")) {
|
||||||
|
return "https://$domain$originalUrl&rkey=$multiMediaRKey"
|
||||||
|
}
|
||||||
|
return "https://$domain$originalUrl"
|
||||||
|
}
|
||||||
|
return "https://$domain/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getC2CPicDownUrl(
|
fun getC2CPicDownUrl(
|
||||||
|
originalUrl: String,
|
||||||
md5: String
|
md5: String
|
||||||
): String {
|
): String {
|
||||||
return "https://c2cpicdw.qpic.cn/offpic_new/0/123-0-${md5.uppercase()}/0?term=2"
|
val isNtServer = originalUrl.startsWith("/download")
|
||||||
|
val domain = if (isNtServer) GPRO_PIC_NT else C2C_PIC
|
||||||
|
if (originalUrl.isNotEmpty()) {
|
||||||
|
if (isNtServer && !originalUrl.contains("rkey=")) {
|
||||||
|
return "https://$domain$originalUrl&rkey=$multiMediaRKey"
|
||||||
|
}
|
||||||
|
return "https://$domain$originalUrl"
|
||||||
|
}
|
||||||
|
return "https://$$domain/offpic_new/0/123-0-${md5.uppercase()}/0?term=2"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGuildPicDownUrl(md5: String): String {
|
fun getGuildPicDownUrl(
|
||||||
return "https://gchat.qpic.cn/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
|
originalUrl: String,
|
||||||
|
md5: String
|
||||||
|
): String {
|
||||||
|
val isNtServer = originalUrl.startsWith("/download")
|
||||||
|
val domain = if (isNtServer) GPRO_PIC_NT else GPRO_PIC
|
||||||
|
if (originalUrl.isNotEmpty()) {
|
||||||
|
if (isNtServer && !originalUrl.contains("rkey=")) {
|
||||||
|
return "https://$domain$originalUrl&rkey=$multiMediaRKey"
|
||||||
|
}
|
||||||
|
return "https://$domain$originalUrl"
|
||||||
|
}
|
||||||
|
return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getC2CVideoDownUrl(
|
suspend fun getC2CVideoDownUrl(
|
||||||
peerId: String,
|
peerId: String,
|
||||||
md5Hex: String,
|
md5: ByteArray,
|
||||||
fileUUId: String
|
fileUUId: String
|
||||||
): String {
|
): String {
|
||||||
return suspendCancellableCoroutine {
|
return suspendCancellableCoroutine {
|
||||||
@ -175,7 +221,7 @@ internal object RichProtoSvc: BaseSvc() {
|
|||||||
downReq.troopUin = peerId
|
downReq.troopUin = peerId
|
||||||
downReq.clientType = 2
|
downReq.clientType = 2
|
||||||
downReq.fileId = fileUUId
|
downReq.fileId = fileUUId
|
||||||
downReq.md5 = md5Hex.hex2ByteArray()
|
downReq.md5 = md5
|
||||||
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
|
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
|
||||||
downReq.subBusiType = 0
|
downReq.subBusiType = 0
|
||||||
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
|
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
|
||||||
@ -202,7 +248,7 @@ internal object RichProtoSvc: BaseSvc() {
|
|||||||
|
|
||||||
suspend fun getGroupVideoDownUrl(
|
suspend fun getGroupVideoDownUrl(
|
||||||
peerId: String,
|
peerId: String,
|
||||||
md5Hex: String,
|
md5: ByteArray,
|
||||||
fileUUId: String
|
fileUUId: String
|
||||||
): String {
|
): String {
|
||||||
return suspendCancellableCoroutine {
|
return suspendCancellableCoroutine {
|
||||||
@ -218,7 +264,7 @@ internal object RichProtoSvc: BaseSvc() {
|
|||||||
downReq.troopUin = peerId
|
downReq.troopUin = peerId
|
||||||
downReq.clientType = 2
|
downReq.clientType = 2
|
||||||
downReq.fileId = fileUUId
|
downReq.fileId = fileUUId
|
||||||
downReq.md5 = md5Hex.hex2ByteArray()
|
downReq.md5 = md5
|
||||||
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
|
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
|
||||||
downReq.subBusiType = 0
|
downReq.subBusiType = 0
|
||||||
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
|
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
|
||||||
@ -321,12 +367,4 @@ internal object RichProtoSvc: BaseSvc() {
|
|||||||
RichProtoProc.procRichProtoReq(richProtoReq)
|
RichProtoProc.procRichProtoReq(richProtoReq)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getGuildPttDownUrl(
|
|
||||||
peerId: String,
|
|
||||||
md5Hex: String,
|
|
||||||
fileUUId: String
|
|
||||||
): String {
|
|
||||||
return "unsupported"
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -6,6 +6,7 @@ import com.tencent.mobileqq.transfile.TransferRequest
|
|||||||
import moe.fuqiuluo.shamrock.utils.MD5
|
import moe.fuqiuluo.shamrock.utils.MD5
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.ResourceType.*
|
import moe.fuqiuluo.qqinterface.servlet.transfile.ResourceType.*
|
||||||
|
import moe.fuqiuluo.shamrock.helper.TransfileHelper
|
||||||
|
|
||||||
internal object Transfer: FileTransfer() {
|
internal object Transfer: FileTransfer() {
|
||||||
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
|
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
|
||||||
@ -84,11 +85,14 @@ internal object Transfer: FileTransfer() {
|
|||||||
file: File,
|
file: File,
|
||||||
wait: Boolean = true
|
wait: Boolean = true
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_SHARE, wait) {
|
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
|
||||||
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
|
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
|
||||||
picUpExtraInfo.mIsRaw = true
|
picUpExtraInfo.mIsRaw = false
|
||||||
|
picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY
|
||||||
it.mPicSendSource = 8
|
it.mPicSendSource = 8
|
||||||
it.mExtraObj = picUpExtraInfo
|
it.mExtraObj = picUpExtraInfo
|
||||||
|
it.mIsPresend = true
|
||||||
|
it.delayShowProgressTimeInMs = 2000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,10 +101,13 @@ internal object Transfer: FileTransfer() {
|
|||||||
file: File,
|
file: File,
|
||||||
wait: Boolean = true
|
wait: Boolean = true
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_SHARE, wait) {
|
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
|
||||||
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
|
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
|
||||||
picUpExtraInfo.mIsRaw = true
|
//picUpExtraInfo.mIsRaw = !TransfileHelper.isGifFile(file)
|
||||||
|
picUpExtraInfo.mIsRaw = false
|
||||||
|
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
|
||||||
it.mPicSendSource = 8
|
it.mPicSendSource = 8
|
||||||
|
it.delayShowProgressTimeInMs = 2000
|
||||||
it.mExtraObj = picUpExtraInfo
|
it.mExtraObj = picUpExtraInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ package moe.fuqiuluo.shamrock.helper
|
|||||||
|
|
||||||
internal abstract class InternalMessageMakerError(why: String): RuntimeException(why)
|
internal abstract class InternalMessageMakerError(why: String): RuntimeException(why)
|
||||||
|
|
||||||
internal class ParamsException(key: String): InternalMessageMakerError("Lack of param $key")
|
internal class ParamsException(key: String): InternalMessageMakerError("Lack of param `$key`")
|
||||||
|
|
||||||
internal class IllegalParamsException(key: String): InternalMessageMakerError("Illegal param $key")
|
internal class IllegalParamsException(key: String): InternalMessageMakerError("Illegal param $key")
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ internal object LocalCacheHelper: BaseSvc() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getCachePttFile(md5: String): File {
|
fun getCachePttFile(md5: String): File {
|
||||||
val file = FileUtils.getFile(md5)
|
val file = FileUtils.getFileByMd5(md5)
|
||||||
return if (file.exists()) file else getCurrentPttPath().resolve("$md5.amr")
|
return if (file.exists()) file else getCurrentPttPath().resolve("$md5.amr")
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,18 +16,18 @@ import kotlinx.serialization.json.JsonElement
|
|||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.MessageMaker
|
import moe.fuqiuluo.qqinterface.servlet.msg.messageelement.MessageElementMaker
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.msg.msgelement.MsgElementMaker
|
||||||
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
||||||
import moe.fuqiuluo.shamrock.helper.db.MessageMapping
|
import moe.fuqiuluo.shamrock.helper.db.MessageMapping
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
|
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
|
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull
|
import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull
|
||||||
import moe.fuqiuluo.shamrock.tools.asString
|
import moe.fuqiuluo.shamrock.tools.asString
|
||||||
import moe.fuqiuluo.shamrock.tools.json
|
import moe.fuqiuluo.shamrock.tools.json
|
||||||
import moe.fuqiuluo.shamrock.tools.jsonArray
|
import moe.fuqiuluo.shamrock.tools.jsonArray
|
||||||
|
import protobuf.message.MessageElement
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
internal object MessageHelper {
|
internal object MessageHelper {
|
||||||
@ -39,7 +39,7 @@ internal object MessageHelper {
|
|||||||
fromId: String = peerId
|
fromId: String = peerId
|
||||||
): SendMsgResult {
|
): SendMsgResult {
|
||||||
val uniseq = generateMsgId(chatType)
|
val uniseq = generateMsgId(chatType)
|
||||||
val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, decodeCQCode(message)).also {
|
val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, decodeCQCode(message)).also {
|
||||||
if (it.second.isEmpty() && !it.first) {
|
if (it.second.isEmpty() && !it.first) {
|
||||||
error("消息合成失败,请查看日志或者检查输入。")
|
error("消息合成失败,请查看日志或者检查输入。")
|
||||||
} else if (it.second.isEmpty()) {
|
} else if (it.second.isEmpty()) {
|
||||||
@ -51,7 +51,14 @@ internal object MessageHelper {
|
|||||||
return sendMessageWithoutMsgId(chatType, peerId, msg, fromId, callback)
|
return sendMessageWithoutMsgId(chatType, peerId, msg, fromId, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun resendMsg(chatType: Int, peerId: String, fromId: String, msgId: Long, retryCnt: Int, msgHashId: Int): Result<SendMsgResult> {
|
suspend fun resendMsg(
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
fromId: String,
|
||||||
|
msgId: Long,
|
||||||
|
retryCnt: Int,
|
||||||
|
msgHashId: Int
|
||||||
|
): Result<SendMsgResult> {
|
||||||
val contact = generateContact(chatType, peerId, fromId)
|
val contact = generateContact(chatType, peerId, fromId)
|
||||||
return resendMsg(contact, msgId, retryCnt, msgHashId)
|
return resendMsg(contact, msgId, retryCnt, msgHashId)
|
||||||
}
|
}
|
||||||
@ -60,11 +67,11 @@ internal object MessageHelper {
|
|||||||
if (retryCnt < 0) return Result.failure(SendMsgException("消息发送超时次数过多"))
|
if (retryCnt < 0) return Result.failure(SendMsgException("消息发送超时次数过多"))
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
val service = QRoute.api(IMsgService::class.java)
|
||||||
val result = withTimeoutOrNull(15000) {
|
val result = withTimeoutOrNull(15000) {
|
||||||
if(suspendCancellableCoroutine {
|
if (suspendCancellableCoroutine {
|
||||||
service.resendMsg(contact, msgId) { result, _ ->
|
service.resendMsg(contact, msgId) { result, _ ->
|
||||||
it.resume(result)
|
it.resume(result)
|
||||||
}
|
}
|
||||||
} != 0) {
|
} != 0) {
|
||||||
resendMsg(contact, msgId, retryCnt - 1, msgHashId)
|
resendMsg(contact, msgId, retryCnt - 1, msgHashId)
|
||||||
} else {
|
} else {
|
||||||
Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis()))
|
Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis()))
|
||||||
@ -82,7 +89,7 @@ internal object MessageHelper {
|
|||||||
callback: IOperateCallback
|
callback: IOperateCallback
|
||||||
): Result<SendMsgResult> {
|
): Result<SendMsgResult> {
|
||||||
val uniseq = generateMsgId(chatType)
|
val uniseq = generateMsgId(chatType)
|
||||||
val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, message).also {
|
val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also {
|
||||||
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
||||||
}.second.filter {
|
}.second.filter {
|
||||||
it.elementType != -1
|
it.elementType != -1
|
||||||
@ -166,7 +173,7 @@ internal object MessageHelper {
|
|||||||
fromId: String = peerId
|
fromId: String = peerId
|
||||||
): SendMsgResult {
|
): SendMsgResult {
|
||||||
val uniseq = generateMsgId(chatType)
|
val uniseq = generateMsgId(chatType)
|
||||||
val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, message).also {
|
val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also {
|
||||||
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
||||||
}.second.filter {
|
}.second.filter {
|
||||||
it.elementType != -1
|
it.elementType != -1
|
||||||
@ -224,7 +231,7 @@ internal object MessageHelper {
|
|||||||
fromId: String = peerId
|
fromId: String = peerId
|
||||||
): SendMsgResult {
|
): SendMsgResult {
|
||||||
val uniseq = generateMsgId(chatType)
|
val uniseq = generateMsgId(chatType)
|
||||||
val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, message).also {
|
val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also {
|
||||||
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
||||||
}.second.filter {
|
}.second.filter {
|
||||||
it.elementType != -1
|
it.elementType != -1
|
||||||
@ -233,7 +240,7 @@ internal object MessageHelper {
|
|||||||
return if (!message.isEmpty()) {
|
return if (!message.isEmpty()) {
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
val service = QRoute.api(IMsgService::class.java)
|
||||||
return suspendCancellableCoroutine {
|
return suspendCancellableCoroutine {
|
||||||
service.sendMsg(contact, uniseq.qqMsgId, msg) { code, why ->
|
service.sendMsg(contact, uniseq.qqMsgId, msg) { _, _ ->
|
||||||
it.resume(uniseq.copy(msgTime = System.currentTimeMillis()))
|
it.resume(uniseq.copy(msgTime = System.currentTimeMillis()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,10 +250,11 @@ internal object MessageHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact {
|
suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact {
|
||||||
val peerId = when(chatType) {
|
val peerId = when (chatType) {
|
||||||
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
|
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
|
||||||
ContactHelper.getUidByUinAsync(id.toLong())
|
ContactHelper.getUidByUinAsync(id.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> id
|
else -> id
|
||||||
}
|
}
|
||||||
return if (chatType == MsgConstant.KCHATTYPEGUILD) {
|
return if (chatType == MsgConstant.KCHATTYPEGUILD) {
|
||||||
@ -276,12 +284,51 @@ internal object MessageHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun messageArrayToMessageElements(chatType: Int, msgId: Long, targetUin: String, messageList: JsonArray): Pair<Boolean, ArrayList<MsgElement>> {
|
suspend fun messageArrayToMsgElements(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
targetUin: String,
|
||||||
|
messageList: JsonArray
|
||||||
|
): Pair<Boolean, ArrayList<MsgElement>> {
|
||||||
val msgList = arrayListOf<MsgElement>()
|
val msgList = arrayListOf<MsgElement>()
|
||||||
var hasActionMsg = false
|
var hasActionMsg = false
|
||||||
messageList.forEach {
|
messageList.forEach {
|
||||||
val msg = it.jsonObject
|
val msg = it.jsonObject
|
||||||
val maker = MessageMaker[msg["type"].asString]
|
val maker = MsgElementMaker[msg["type"].asString]
|
||||||
|
if (maker != null) {
|
||||||
|
try {
|
||||||
|
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
|
||||||
|
maker(chatType, msgId, targetUin, data).onSuccess { msgElem ->
|
||||||
|
msgList.add(msgElem)
|
||||||
|
}.onFailure {
|
||||||
|
if (it.javaClass != ActionMsgException::class.java) {
|
||||||
|
throw it
|
||||||
|
} else {
|
||||||
|
hasActionMsg = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
LogCenter.log(e.stackTraceToString(), Level.ERROR)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR)
|
||||||
|
return false to arrayListOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasActionMsg to msgList
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun messageArrayToMessageElements(
|
||||||
|
chatType: Int,
|
||||||
|
msgId: Long,
|
||||||
|
targetUin: String,
|
||||||
|
messageList: JsonArray
|
||||||
|
): Pair<Boolean, ArrayList<MessageElement>> {
|
||||||
|
val msgList = arrayListOf<MessageElement>()
|
||||||
|
var hasActionMsg = false
|
||||||
|
messageList.forEach {
|
||||||
|
val msg = it.jsonObject
|
||||||
|
val maker = MessageElementMaker[msg["type"].asString]
|
||||||
if (maker != null) {
|
if (maker != null) {
|
||||||
try {
|
try {
|
||||||
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
|
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
|
||||||
@ -353,6 +400,21 @@ internal object MessageHelper {
|
|||||||
database.messageMappingDao().insert(mapping)
|
database.messageMappingDao().insert(mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveMsgMappingNotExist(
|
||||||
|
hash: Int,
|
||||||
|
qqMsgId: Long,
|
||||||
|
time: Long,
|
||||||
|
chatType: Int,
|
||||||
|
peerId: String,
|
||||||
|
subPeerId: String,
|
||||||
|
msgSeq: Int,
|
||||||
|
subChatType: Int = chatType
|
||||||
|
) {
|
||||||
|
val database = MessageDB.getInstance()
|
||||||
|
val mapping = MessageMapping(hash, qqMsgId, chatType, subChatType, peerId, time, msgSeq, subPeerId)
|
||||||
|
database.messageMappingDao().insertNotExist(mapping)
|
||||||
|
}
|
||||||
|
|
||||||
external fun createMessageUniseq(chatType: Int, time: Long): Long
|
external fun createMessageUniseq(chatType: Int, time: Long): Long
|
||||||
|
|
||||||
fun decodeCQCode(code: String): JsonArray {
|
fun decodeCQCode(code: String): JsonArray {
|
||||||
@ -374,22 +436,6 @@ internal object MessageHelper {
|
|||||||
return arrayList.jsonArray
|
return arrayList.jsonArray
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encodeCQCode(msg: List<Map<String, JsonElement>>): String {
|
|
||||||
return nativeEncodeCQCode(msg.map {
|
|
||||||
val params = hashMapOf<String, String>()
|
|
||||||
it.forEach { (key, value) ->
|
|
||||||
if (key != "type") {
|
|
||||||
value.asJsonObject.forEach { param, element ->
|
|
||||||
params[param] = element.asString
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
params["_type"] = value.asString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun nativeDecodeCQCode(code: String): List<Map<String, String>>
|
private external fun nativeDecodeCQCode(code: String): List<Map<String, String>>
|
||||||
private external fun nativeEncodeCQCode(segment: List<Map<String, String>>): String
|
external fun nativeEncodeCQCode(segment: List<Map<String, String>>): String
|
||||||
}
|
}
|
@ -29,7 +29,7 @@ internal object MusicHelper {
|
|||||||
chatType,
|
chatType,
|
||||||
peerId,
|
peerId,
|
||||||
msgId,
|
msgId,
|
||||||
ArkAppInfo.NeteaseMusic,
|
ArkAppInfo.NetEaseMusic,
|
||||||
title.ifBlank { name },
|
title.ifBlank { name },
|
||||||
singerName,
|
singerName,
|
||||||
jumpUrl,
|
jumpUrl,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package moe.fuqiuluo.shamrock.helper
|
package moe.fuqiuluo.shamrock.helper
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.RandomAccessFile
|
||||||
|
|
||||||
internal object TransfileHelper {
|
internal object TransfileHelper {
|
||||||
private val extensionMap = mapOf(
|
private val extensionMap = mapOf(
|
||||||
@ -94,4 +96,15 @@ internal object TransfileHelper {
|
|||||||
val extension = name.substring(index)
|
val extension = name.substring(index)
|
||||||
return extensionMap[extension] ?: -1
|
return extensionMap[extension] ?: -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isGifFile(picFile: File): Boolean {
|
||||||
|
if (picFile.exists() && picFile.length() > 3) {
|
||||||
|
return RandomAccessFile(picFile, "r").use {
|
||||||
|
val bArr = ByteArray(3)
|
||||||
|
it.read(bArr)
|
||||||
|
if (bArr[0].toInt() == 71 && bArr[1].toInt() == 73 && bArr[2].toInt() == 70) { return true } else false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
@ -30,6 +30,9 @@ interface MessageMappingDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insert(mapping: MessageMapping)
|
fun insert(mapping: MessageMapping)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
|
fun insertNotExist(mapping: MessageMapping)
|
||||||
|
|
||||||
@Query("UPDATE message_mapping_v2 SET msgSeq = :msgSeq WHERE msgHashId = :hash")
|
@Query("UPDATE message_mapping_v2 SET msgSeq = :msgSeq WHERE msgHashId = :hash")
|
||||||
fun updateMsgSeqByMsgHash(hash: Int, msgSeq: Int)
|
fun updateMsgSeqByMsgHash(hash: Int, msgSeq: Int)
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ internal abstract class IActionHandler {
|
|||||||
return resultToString(true, Status.Ok, EmptyObject, msg, echo = echo)
|
return resultToString(true, Status.Ok, EmptyObject, msg, echo = echo)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected inline fun <reified T> ok(data: T, echo: JsonElement = EmptyJsonString, msg: String = ""): String {
|
protected inline fun <reified T> ok(data: T, echo: JsonElement, msg: String = ""): String {
|
||||||
return resultToString(true, Status.Ok, data!!, msg, echo = echo)
|
return resultToString(true, Status.Ok, data!!, msg, echo = echo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +149,14 @@ internal class ActionSession {
|
|||||||
return params[key].asBooleanOrNull ?: default as Boolean
|
return params[key].asBooleanOrNull ?: default as Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getJsonElement(key: String): JsonElement {
|
||||||
|
return params[key]!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getJsonElementOrNull(key: String): JsonElement? {
|
||||||
|
return params[key]
|
||||||
|
}
|
||||||
|
|
||||||
fun getObject(key: String): JsonObject {
|
fun getObject(key: String): JsonObject {
|
||||||
return params[key].asJsonObject
|
return params[key].asJsonObject
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ internal object FavAddImageMsg: IActionHandler() {
|
|||||||
val image = fileText.let {
|
val image = fileText.let {
|
||||||
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
|
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
|
||||||
if (md5.length == 32) {
|
if (md5.length == 32) {
|
||||||
FileUtils.getFile(it)
|
FileUtils.getFileByMd5(it)
|
||||||
} else {
|
} else {
|
||||||
FileUtils.parseAndSave(it)
|
FileUtils.parseAndSave(it)
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ internal object FavGetItemContent: IActionHandler() {
|
|||||||
resp.getFavContentResp!!.content!!.joinToString("") {
|
resp.getFavContentResp!!.content!!.joinToString("") {
|
||||||
String(it.richMedia!!.rawData!!)
|
String(it.richMedia!!.rawData!!)
|
||||||
}
|
}
|
||||||
))
|
), echo)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("id")
|
override val requiredParams: Array<String> = arrayOf("id")
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||||
|
|
||||||
|
import com.tencent.guild.api.transfile.IGuildTransFileApi
|
||||||
|
import com.tencent.mobileqq.qroute.QRoute
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.data.BigDataTicket
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.Credentials
|
import moe.fuqiuluo.shamrock.remote.service.data.Credentials
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
import moe.fuqiuluo.symbols.OneBotHandler
|
||||||
@ -17,10 +20,20 @@ internal object GetCookies: IActionHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
operator fun invoke(echo: JsonElement = EmptyJsonString): String {
|
operator fun invoke(echo: JsonElement = EmptyJsonString): String {
|
||||||
return ok(Credentials(cookie = TicketSvc.getCookie()), echo)
|
return ok(Credentials(
|
||||||
|
cookie = TicketSvc.getCookie(),
|
||||||
|
bigDataTicket = QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let {
|
||||||
|
BigDataTicket(it.sessionKey, it.sessionSig)
|
||||||
|
}
|
||||||
|
), echo)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String {
|
suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String {
|
||||||
return ok(Credentials(cookie = TicketSvc.getCookie(domain)), echo)
|
return ok(Credentials(
|
||||||
|
cookie = TicketSvc.getCookie(domain),
|
||||||
|
bigDataTicket = QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let {
|
||||||
|
BigDataTicket(it.sessionKey, it.sessionSig)
|
||||||
|
}
|
||||||
|
), echo)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,21 +1,17 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.MessageConvert
|
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail
|
import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.MessageSender
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
import moe.fuqiuluo.symbols.OneBotHandler
|
||||||
|
|
||||||
@OneBotHandler("get_forward_msg")
|
@OneBotHandler("get_forward_msg")
|
||||||
internal object GetForwardMsg: IActionHandler() {
|
internal object GetForwardMsg : IActionHandler() {
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
override suspend fun internalHandle(session: ActionSession): String {
|
||||||
val id = session.getString("id")
|
val id = session.getString("id")
|
||||||
return invoke(id, session.echo)
|
return invoke(id, session.echo)
|
||||||
@ -25,32 +21,8 @@ internal object GetForwardMsg: IActionHandler() {
|
|||||||
resId: String,
|
resId: String,
|
||||||
echo: JsonElement = EmptyJsonString
|
echo: JsonElement = EmptyJsonString
|
||||||
): String {
|
): String {
|
||||||
val result = MsgSvc.getMultiMsg(resId)
|
val result = MsgSvc.getMultiMsg(resId).getOrElse { return logic(it.toString(), echo) }
|
||||||
if (result.isFailure) {
|
return ok(data = GetForwardMsgResult(result), echo = echo)
|
||||||
return logic(result.exceptionOrNull().toString(), echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok(data = GetForwardMsgResult(result.getOrThrow().map { msg ->
|
|
||||||
val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId)
|
|
||||||
MessageDetail(
|
|
||||||
time = msg.msgTime.toInt(),
|
|
||||||
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
|
|
||||||
msgId = msgHash,
|
|
||||||
realId = msg.msgSeq.toInt(),
|
|
||||||
sender = MessageSender(
|
|
||||||
msg.senderUin, msg.sendNickName
|
|
||||||
.ifEmpty { msg.sendMemberName }
|
|
||||||
.ifEmpty { msg.sendRemarkName }
|
|
||||||
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid, msg.senderUid
|
|
||||||
),
|
|
||||||
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
|
|
||||||
it.toJson()
|
|
||||||
},
|
|
||||||
peerId = msg.peerUin,
|
|
||||||
groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0,
|
|
||||||
targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0
|
|
||||||
)
|
|
||||||
}), echo = echo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||||
|
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
@ -18,8 +19,10 @@ internal object GetGProChannelList: IActionHandler() {
|
|||||||
return invoke(guildId.toULong(), refresh, echo = session.echo)
|
return invoke(guildId.toULong(), refresh, echo = session.echo)
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String {
|
suspend operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String {
|
||||||
val result = GProSvc.getChannelList(guildId, refresh)
|
val result = withTimeoutOrNull(5000) {
|
||||||
|
GProSvc.getChannelList(guildId, refresh)
|
||||||
|
} ?: return error("timeout", echo)
|
||||||
result.onFailure {
|
result.onFailure {
|
||||||
return error(it.message ?: "unable to fetch channel list", echo)
|
return error(it.message ?: "unable to fetch channel list", echo)
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ internal object GetGuildMemberList: IActionHandler() {
|
|||||||
members = members,
|
members = members,
|
||||||
finish = nextToken.finish,
|
finish = nextToken.finish,
|
||||||
nextToken = ProtoBuf.encodeToByteArray(nextToken).toHexString(),
|
nextToken = ProtoBuf.encodeToByteArray(nextToken).toHexString(),
|
||||||
))
|
), echo)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("guild_id")
|
override val requiredParams: Array<String> = arrayOf("guild_id")
|
||||||
|
@ -6,7 +6,8 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.MessageConvert
|
import moe.fuqiuluo.qqinterface.servlet.msg.msgelement.toSegments
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.msg.toListMap
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||||
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||||
@ -21,7 +22,7 @@ import kotlin.coroutines.resume
|
|||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@OneBotHandler("get_history_msg")
|
@OneBotHandler("get_history_msg")
|
||||||
internal object GetHistoryMsg: IActionHandler() {
|
internal object GetHistoryMsg : IActionHandler() {
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
override suspend fun internalHandle(session: ActionSession): String {
|
||||||
val msgType = session.getString("message_type")
|
val msgType = session.getString("message_type")
|
||||||
val peerId = session.getString(if (msgType == "group") "group_id" else "user_id")
|
val peerId = session.getString(if (msgType == "group") "group_id" else "user_id")
|
||||||
@ -59,6 +60,16 @@ internal object GetHistoryMsg: IActionHandler() {
|
|||||||
val msgList = ArrayList<MessageDetail>().apply {
|
val msgList = ArrayList<MessageDetail>().apply {
|
||||||
addAll(result.data!!.map { msg ->
|
addAll(result.data!!.map { msg ->
|
||||||
val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId)
|
val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId)
|
||||||
|
MessageHelper.saveMsgMappingNotExist(
|
||||||
|
hash = msgHash,
|
||||||
|
qqMsgId = msg.msgId,
|
||||||
|
chatType = msg.chatType,
|
||||||
|
subChatType = msg.chatType,
|
||||||
|
peerId = msg.peerUin.toString(),
|
||||||
|
msgSeq = msg.msgSeq.toInt(),
|
||||||
|
time = msg.msgTime,
|
||||||
|
subPeerId = msg.channelId ?: msg.peerUin.toString()
|
||||||
|
)
|
||||||
MessageDetail(
|
MessageDetail(
|
||||||
time = msg.msgTime.toInt(),
|
time = msg.msgTime.toInt(),
|
||||||
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
|
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
|
||||||
@ -67,9 +78,11 @@ internal object GetHistoryMsg: IActionHandler() {
|
|||||||
sender = MessageSender(
|
sender = MessageSender(
|
||||||
msg.senderUin, msg.sendNickName, "unknown", 0, msg.senderUid, msg.senderUid
|
msg.senderUin, msg.sendNickName, "unknown", 0, msg.senderUid, msg.senderUid
|
||||||
),
|
),
|
||||||
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
|
message = msg.elements.toSegments(
|
||||||
it.toJson()
|
msg.chatType,
|
||||||
},
|
if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(),
|
||||||
|
msg.channelId ?: msg.peerUin.toString()
|
||||||
|
).toListMap(),
|
||||||
peerId = msg.peerUin,
|
peerId = msg.peerUin,
|
||||||
groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0,
|
groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0,
|
||||||
targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0
|
targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0
|
||||||
@ -91,13 +104,15 @@ internal object GetHistoryMsg: IActionHandler() {
|
|||||||
.ifEmpty { msg.sendRemarkName }
|
.ifEmpty { msg.sendRemarkName }
|
||||||
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid, msg.senderUid
|
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid, msg.senderUid
|
||||||
),
|
),
|
||||||
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
|
message = msg.elements.toSegments(
|
||||||
it.toJson()
|
chatType,
|
||||||
},
|
if (chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(),
|
||||||
|
msg.channelId ?: peerId).toListMap(),
|
||||||
peerId = msg.peerUin,
|
peerId = msg.peerUin,
|
||||||
groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0,
|
groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0,
|
||||||
targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0
|
targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +35,8 @@ internal object GetImage: IActionHandler() {
|
|||||||
image.size,
|
image.size,
|
||||||
image.fileName,
|
image.fileName,
|
||||||
when(image.chatType) {
|
when(image.chatType) {
|
||||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(fileMd5)
|
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl("", fileMd5)
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(fileMd5)
|
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl("", fileMd5)
|
||||||
else -> error("Not supported chat type: ${image.chatType}, convertMsgElementsToMsgSegment::Pic")
|
else -> error("Not supported chat type: ${image.chatType}, convertMsgElementsToMsgSegment::Pic")
|
||||||
}
|
}
|
||||||
), echo = echo)
|
), echo = echo)
|
||||||
|
@ -8,7 +8,8 @@ import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|||||||
import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail
|
import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.MessageSender
|
import moe.fuqiuluo.shamrock.remote.service.data.MessageSender
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.MessageConvert
|
import moe.fuqiuluo.qqinterface.servlet.msg.msgelement.toSegments
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.msg.toListMap
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
import moe.fuqiuluo.symbols.OneBotHandler
|
||||||
|
|
||||||
@ -39,9 +40,11 @@ internal object GetMsg: IActionHandler() {
|
|||||||
msg.senderUid,
|
msg.senderUid,
|
||||||
msg.senderUid
|
msg.senderUid
|
||||||
),
|
),
|
||||||
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
|
message = msg.elements.toSegments(
|
||||||
it.toJson()
|
msg.chatType,
|
||||||
},
|
if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(),
|
||||||
|
msg.channelId ?: msg.peerUin.toString()
|
||||||
|
).toListMap(),
|
||||||
peerId = msg.peerUin,
|
peerId = msg.peerUin,
|
||||||
groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0,
|
groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0,
|
||||||
targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0
|
targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0
|
||||||
|
@ -52,11 +52,7 @@ internal object GetTroopMemberInfo : IActionHandler() {
|
|||||||
area = info.alias ?: "",
|
area = info.alias ?: "",
|
||||||
lastSentTime = info.last_active_time,
|
lastSentTime = info.last_active_time,
|
||||||
level = info.level,
|
level = info.level,
|
||||||
role = when {
|
role = GroupSvc.getMemberRole(groupId.toLong(), uin.toLong()),
|
||||||
GroupSvc.getOwner(groupId).toString() == uin -> MemberRole.Owner
|
|
||||||
uin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin
|
|
||||||
else -> MemberRole.Member
|
|
||||||
},
|
|
||||||
unfriendly = false,
|
unfriendly = false,
|
||||||
title = info.mUniqueTitle ?: "",
|
title = info.mUniqueTitle ?: "",
|
||||||
titleExpireTime = info.mUniqueTitleExpire,
|
titleExpireTime = info.mUniqueTitleExpire,
|
||||||
|
@ -54,12 +54,13 @@ internal object GetTroopMemberList : IActionHandler() {
|
|||||||
area = info.alias ?: "",
|
area = info.alias ?: "",
|
||||||
lastSentTime = info.last_active_time,
|
lastSentTime = info.last_active_time,
|
||||||
level = info.level,
|
level = info.level,
|
||||||
role = when {
|
role = GroupSvc.getMemberRole(groupId.toLong(), info.memberuin.toLong())
|
||||||
|
/*when {
|
||||||
GroupSvc.getOwner(groupId)
|
GroupSvc.getOwner(groupId)
|
||||||
.toString() == info.memberuin -> MemberRole.Owner
|
.toString() == info.memberuin -> MemberRole.Owner
|
||||||
info.memberuin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin
|
info.memberuin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin
|
||||||
else -> MemberRole.Member
|
else -> MemberRole.Member
|
||||||
},
|
}*/,
|
||||||
unfriendly = false,
|
unfriendly = false,
|
||||||
title = info.mUniqueTitle ?: "",
|
title = info.mUniqueTitle ?: "",
|
||||||
titleExpireTime = info.mUniqueTitleExpire,
|
titleExpireTime = info.mUniqueTitleExpire,
|
||||||
|
@ -2,6 +2,7 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
|
|||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
||||||
@ -17,7 +18,7 @@ internal object ModifyTroopMemberName: IActionHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
operator fun invoke(groupId: String, userId: String, card: String, echo: JsonElement = EmptyJsonString): String {
|
operator fun invoke(groupId: String, userId: String, card: String, echo: JsonElement = EmptyJsonString): String {
|
||||||
if (!GroupSvc.isAdmin(groupId)) {
|
if (!GroupSvc.isAdmin(groupId) && userId != TicketSvc.getUin()) {
|
||||||
return logic("you are not admin", echo)
|
return logic("you are not admin", echo)
|
||||||
}
|
}
|
||||||
return if(GroupSvc.modifyGroupMemberCard(groupId.toLong(), userId.toLong(), card))
|
return if(GroupSvc.modifyGroupMemberCard(groupId.toLong(), userId.toLong(), card))
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo
|
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
|
import moe.fuqiuluo.qqinterface.servlet.msg.msgelement.toSegments
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||||
@ -14,8 +13,11 @@ import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult
|
import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult
|
||||||
import moe.fuqiuluo.shamrock.tools.*
|
import moe.fuqiuluo.shamrock.tools.*
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
import moe.fuqiuluo.symbols.OneBotHandler
|
||||||
|
import protobuf.message.*
|
||||||
|
import protobuf.message.longmsg.PushMsgBody
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
@OneBotHandler("send_forward_msg")
|
@OneBotHandler("send_forward_msg")
|
||||||
internal object SendForwardMessage : IActionHandler() {
|
internal object SendForwardMessage : IActionHandler() {
|
||||||
@ -37,14 +39,26 @@ internal object SendForwardMessage : IActionHandler() {
|
|||||||
return noParam("detail_type/message_type", session.echo)
|
return noParam("detail_type/message_type", session.echo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val peerId = when(chatType) {
|
val peerId = when (chatType) {
|
||||||
MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam("group_id", session.echo)
|
MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam(
|
||||||
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id") ?: return noParam("user_id", session.echo)
|
"group_id",
|
||||||
|
session.echo
|
||||||
|
)
|
||||||
|
|
||||||
|
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id")
|
||||||
|
?: return noParam("user_id", session.echo)
|
||||||
|
|
||||||
else -> error("unknown chat type: $chatType")
|
else -> error("unknown chat type: $chatType")
|
||||||
}
|
}
|
||||||
val fromId = when(chatType) {
|
val fromId = when (chatType) {
|
||||||
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("group_id") ?: return noParam("group_id", session.echo)
|
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("group_id")
|
||||||
MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam("user_id", session.echo)
|
?: return noParam("group_id", session.echo)
|
||||||
|
|
||||||
|
MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam(
|
||||||
|
"user_id",
|
||||||
|
session.echo
|
||||||
|
)
|
||||||
|
|
||||||
else -> error("unknown chat type: $chatType")
|
else -> error("unknown chat type: $chatType")
|
||||||
}
|
}
|
||||||
return if (session.isArray("messages")) {
|
return if (session.isArray("messages")) {
|
||||||
@ -68,103 +82,208 @@ internal object SendForwardMessage : IActionHandler() {
|
|||||||
echo: JsonElement = EmptyJsonString
|
echo: JsonElement = EmptyJsonString
|
||||||
): String {
|
): String {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val kernelService = NTServiceFetcher.kernelService
|
var uid: String? = null
|
||||||
val sessionService = kernelService.wrapperSession
|
var groupUin: String? = null
|
||||||
val msgService = sessionService.msgService
|
|
||||||
val selfUin = TicketSvc.getUin()
|
|
||||||
|
|
||||||
val multiNodes = messages.map {
|
var i = -1
|
||||||
if (it.asJsonObject["type"].asStringOrNull != "node") {
|
val desc = MutableList(messages.size) { "" }
|
||||||
LogCenter.log("包含非node类型节点", Level.WARN)
|
|
||||||
return@map null
|
val msgs = messages.map { msg ->
|
||||||
}
|
val data = msg.asJsonObject["data"].asJsonObject
|
||||||
if (it.asJsonObject["data"] !is JsonObject) {
|
if (data.containsKey("id")) {
|
||||||
LogCenter.log("data字段错误", Level.WARN)
|
val record = MsgSvc.getMsg(data["id"].asInt).getOrElse {
|
||||||
return@map null
|
LogCenter.log("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it", Level.WARN)
|
||||||
}
|
|
||||||
it.asJsonObject["data"].asJsonObject.let { data ->
|
|
||||||
if (data.containsKey("id")) {
|
|
||||||
val record = MsgSvc.getMsg(data["id"].asInt).getOrNull()
|
|
||||||
if (record == null) {
|
|
||||||
LogCenter.log("合并转发消息节点消息获取失败:${data["id"]}", Level.WARN)
|
|
||||||
return@map null
|
|
||||||
} else {
|
|
||||||
record.peerName to record.toSegments().map { segment ->
|
|
||||||
segment.toJson()
|
|
||||||
}.json
|
|
||||||
}
|
|
||||||
} else if (data.containsKey("content")) {
|
|
||||||
(data["name"].asStringOrNull ?: "Anno") to when (val raw = data["content"]) {
|
|
||||||
is JsonObject -> raw.asJsonArray
|
|
||||||
is JsonArray -> raw.asJsonArray
|
|
||||||
else -> MessageHelper.decodeCQCode(raw.asString)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LogCenter.log("消息节点缺少id或content字段", Level.WARN)
|
|
||||||
return@map null
|
return@map null
|
||||||
}
|
}
|
||||||
}.let { node ->
|
if (record.chatType == MsgConstant.KCHATTYPEGROUP) groupUin = record.peerUin.toString()
|
||||||
val content = node.second.map { msg ->
|
PushMsgBody(
|
||||||
when (msg.asJsonObject["type"].asStringOrNull ?: "text") {
|
head = MessageHead(
|
||||||
"at" -> {
|
peerUid = record.senderUid,
|
||||||
buildJsonObject {
|
receiverUid = record.peerUid,
|
||||||
put("type", "text")
|
forward = MessageForward(
|
||||||
putJsonObject("data") {
|
friendName = record.sendNickName
|
||||||
put(
|
),
|
||||||
"text", "@${
|
groupInfo = if (record.chatType == MsgConstant.KCHATTYPEGROUP) GroupInfo(
|
||||||
msg.asJsonObject["data"].asJsonObject["name"].asStringOrNull.ifNullOrEmpty(
|
groupCode = record.peerUin.toULong(),
|
||||||
msg.asJsonObject["data"].asJsonObject["qq"].asString
|
memberCard = record.sendMemberName,
|
||||||
)
|
u1 = 2
|
||||||
}"
|
) else null
|
||||||
)
|
),
|
||||||
|
content = MessageContent(
|
||||||
|
msgType = when (record.chatType) {
|
||||||
|
MsgConstant.KCHATTYPEC2C -> 9
|
||||||
|
MsgConstant.KCHATTYPEGROUP -> 82
|
||||||
|
else -> throw UnsupportedOperationException(
|
||||||
|
"Unsupported chatType: $chatType"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
|
||||||
|
u1 = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
|
||||||
|
msgViaRandom = record.msgId,
|
||||||
|
msgSeq_ = record.msgSeq, // idk what this is(i++)
|
||||||
|
msgTime = record.msgTime,
|
||||||
|
u2 = 1,
|
||||||
|
u6 = 0,
|
||||||
|
u7 = 0,
|
||||||
|
msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm
|
||||||
|
forwardHead = ForwardHead(
|
||||||
|
u1 = 0,
|
||||||
|
u2 = 0,
|
||||||
|
u3 = 0,
|
||||||
|
ub641 = "",
|
||||||
|
Avatar = ""
|
||||||
|
)
|
||||||
|
),
|
||||||
|
body = MessageBody(
|
||||||
|
rich = RichMessage(
|
||||||
|
elements = MessageHelper.messageArrayToMessageElements(
|
||||||
|
record.chatType,
|
||||||
|
record.msgId,
|
||||||
|
record.peerUin.toString(),
|
||||||
|
record.elements.toSegments(
|
||||||
|
record.chatType,
|
||||||
|
record.peerUin.toString(),
|
||||||
|
"0"
|
||||||
|
).also {
|
||||||
|
desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": "
|
||||||
|
}.map {
|
||||||
|
when (it.type) {
|
||||||
|
"text" -> desc[i] += it.data["text"] as String
|
||||||
|
|
||||||
|
"at" -> desc[i] += "@${it.data["name"] as String? ?: it.data["qq"] as String}"
|
||||||
|
|
||||||
|
"face" -> desc[i] += "[表情]"
|
||||||
|
|
||||||
|
"voice" -> desc[i] += "[语音]"
|
||||||
|
|
||||||
|
"node" -> desc[i] += "[合并转发消息]"
|
||||||
|
}
|
||||||
|
it.toJson()
|
||||||
|
}.json
|
||||||
|
).also {
|
||||||
|
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
||||||
|
}.second
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (data.containsKey("content")) {
|
||||||
|
PushMsgBody(
|
||||||
|
head = MessageHead(
|
||||||
|
peer = data["uin"]?.asLong ?: TicketSvc.getUin().toLong(),
|
||||||
|
peerUid = data["uid"]?.asString ?: TicketSvc.getUid(),
|
||||||
|
receiverUid = TicketSvc.getUid(),
|
||||||
|
forward = MessageForward(
|
||||||
|
friendName = data["name"]?.asStringOrNull ?: TicketSvc.getNickname()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
content = MessageContent(
|
||||||
|
msgType = 9,
|
||||||
|
msgSubType = 175,
|
||||||
|
u1 = 175,
|
||||||
|
msgViaRandom = Random.nextLong(),
|
||||||
|
msgSeq_ = data["seq"]?.asLong ?: Random.nextLong(),
|
||||||
|
msgTime = data["time"]?.asLong ?: (System.currentTimeMillis() / 1000),
|
||||||
|
u2 = 1,
|
||||||
|
u6 = 0,
|
||||||
|
u7 = 0,
|
||||||
|
msgSeq = data["seq"]?.asLong ?: Random.nextLong(),
|
||||||
|
forwardHead = ForwardHead(
|
||||||
|
u1 = 0,
|
||||||
|
u2 = 0,
|
||||||
|
u3 = 2,
|
||||||
|
ub641 = "",
|
||||||
|
Avatar = ""
|
||||||
|
)
|
||||||
|
),
|
||||||
|
body = MessageBody(
|
||||||
|
rich = RichMessage(
|
||||||
|
elements = MessageHelper.messageArrayToMessageElements(
|
||||||
|
1,
|
||||||
|
Random.nextLong(),
|
||||||
|
data["uin"]?.asString ?: TicketSvc.getUin(),
|
||||||
|
when (data["content"]) {
|
||||||
|
is JsonObject -> listOf(data["content"] as JsonObject).json
|
||||||
|
is JsonArray -> data["content"] as JsonArray
|
||||||
|
else -> MessageHelper.decodeCQCode(data["content"].asString)
|
||||||
|
}.also {
|
||||||
|
desc[++i] = "${
|
||||||
|
data["name"].asStringOrNull ?: data["uin"].asStringOrNull
|
||||||
|
?: TicketSvc.getNickname()
|
||||||
|
}: "
|
||||||
|
}.onEach {
|
||||||
|
when (it.asJsonObject["type"].asString) {
|
||||||
|
"text" -> desc[i] += it.asJsonObject["data"].asJsonObject["text"].asString
|
||||||
|
|
||||||
|
"at" -> desc[i] += "@${it.asJsonObject["data"].asJsonObject["name"].asStringOrNull ?: it.asJsonObject["data"].asJsonObject["qq"].asString}"
|
||||||
|
|
||||||
|
"face" -> desc[i] += "[表情]"
|
||||||
|
|
||||||
|
"voice" -> desc[i] += "[语音]"
|
||||||
|
|
||||||
|
"node" -> desc[i] += "[合并转发消息]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
).also {
|
||||||
}
|
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
||||||
|
}.second
|
||||||
"voice" -> {
|
)
|
||||||
buildJsonObject {
|
)
|
||||||
put("type", "text")
|
)
|
||||||
putJsonObject("data") {
|
} else {
|
||||||
put("text", "[语音]")
|
LogCenter.log("消息节点缺少id或content字段", Level.WARN)
|
||||||
}
|
null
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"node" -> {
|
|
||||||
LogCenter.log("合并转发消息暂时不支持嵌套", Level.WARN)
|
|
||||||
buildJsonObject {
|
|
||||||
put("type", "text")
|
|
||||||
putJsonObject("data") {
|
|
||||||
put("text", "[合并转发消息]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> msg
|
|
||||||
}
|
|
||||||
}.json
|
|
||||||
|
|
||||||
val result = MessageHelper.sendMessageNoCb(MsgConstant.KCHATTYPEC2C, selfUin, content)
|
|
||||||
if (result.qqMsgId == 0L) {
|
|
||||||
LogCenter.log("合并转发消息节点消息发送失败", Level.WARN)
|
|
||||||
return@map null
|
|
||||||
}
|
|
||||||
result.qqMsgId to node.first
|
|
||||||
}
|
}
|
||||||
}.filterNotNull()
|
}.filterNotNull().ifEmpty { return logic("消息节点为空", echo) }
|
||||||
|
|
||||||
val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
|
|
||||||
val to = MessageHelper.generateContact(chatType, peerId, fromId)
|
|
||||||
|
|
||||||
val uniseq = MessageHelper.generateMsgId(chatType)
|
val resid = MsgSvc.sendMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs)
|
||||||
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply {
|
.getOrElse { return logic(it.message ?: "", echo) }
|
||||||
multiNodes.forEach { add(MultiMsgInfo(it.first, it.second)) }
|
val uniseq = UUID.randomUUID().toString().uppercase()
|
||||||
}.also { it.reverse() }, from, to, MsgSvc.MessageCallback(peerId, uniseq.msgHashId))
|
|
||||||
|
val result = MsgSvc.sendToAio(
|
||||||
|
chatType, peerId,
|
||||||
|
listOf(
|
||||||
|
hashMapOf(
|
||||||
|
"type" to "json",
|
||||||
|
"data" to hashMapOf(
|
||||||
|
"data" to hashMapOf(
|
||||||
|
"app" to "com.tencent.multimsg",
|
||||||
|
"config" to hashMapOf(
|
||||||
|
"autosize" to 1,
|
||||||
|
"forward" to 1,
|
||||||
|
"round" to 1,
|
||||||
|
"type" to "normal",
|
||||||
|
"width" to 300
|
||||||
|
).json,
|
||||||
|
"desc" to "[聊天记录]",
|
||||||
|
"extra" to hashMapOf(
|
||||||
|
"filename" to uniseq,
|
||||||
|
"tsum" to 2
|
||||||
|
).json.toString(),
|
||||||
|
"meta" to hashMapOf(
|
||||||
|
"detail" to hashMapOf(
|
||||||
|
"news" to desc.slice(0..if (i < 3) i else 3)
|
||||||
|
.map { hashMapOf("text" to it).json }.json,
|
||||||
|
"resid" to resid,
|
||||||
|
"source" to "群聊的聊天记录",
|
||||||
|
"summary" to "查看${msgs.size}条转发消息",
|
||||||
|
"uniseq" to uniseq
|
||||||
|
).json
|
||||||
|
).json,
|
||||||
|
"prompt" to "[聊天记录]",
|
||||||
|
"ver" to "0.0.0.5",
|
||||||
|
"view" to "contact"
|
||||||
|
).json,
|
||||||
|
"resid" to resid
|
||||||
|
).json
|
||||||
|
).json
|
||||||
|
).json, fromId, 3
|
||||||
|
).getOrElse { return logic(it.message ?: "", echo) }
|
||||||
|
|
||||||
return ok(
|
return ok(
|
||||||
ForwardMessageResult(
|
ForwardMessageResult(
|
||||||
msgId = uniseq.msgHashId,
|
msgId = result.msgHashId,
|
||||||
forwardId = ""
|
forwardId = resid
|
||||||
), echo = echo
|
), echo = echo
|
||||||
)
|
)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
@ -120,18 +120,14 @@ internal object SendMessage: IActionHandler() {
|
|||||||
//if (!ContactHelper.checkContactAvailable(chatType, peerId)) {
|
//if (!ContactHelper.checkContactAvailable(chatType, peerId)) {
|
||||||
// return logic("contact is not found", echo = echo)
|
// return logic("contact is not found", echo = echo)
|
||||||
//}
|
//}
|
||||||
val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt)
|
val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt).getOrElse { return logic(it.message ?: "", echo) }
|
||||||
if (result.isFailure) {
|
if (result.msgHashId <= 0) {
|
||||||
return logic(result.exceptionOrNull()?.message ?: "", echo)
|
|
||||||
}
|
|
||||||
val sendMsgResult = result.getOrThrow()
|
|
||||||
if (sendMsgResult.msgHashId <= 0) {
|
|
||||||
return logic("send message failed", echo = echo)
|
return logic("send message failed", echo = echo)
|
||||||
}
|
}
|
||||||
recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) }
|
recallDuration?.let { autoRecall(result.msgHashId, it) }
|
||||||
return ok(MessageResult(
|
return ok(MessageResult(
|
||||||
msgId = sendMsgResult.msgHashId,
|
msgId = result.msgHashId,
|
||||||
time = (sendMsgResult.msgTime * 0.001).toLong()
|
time = (result.msgTime * 0.001).toLong()
|
||||||
), echo)
|
), echo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||||
|
|
||||||
|
import kotlinx.atomicfu.atomic
|
||||||
|
import kotlinx.serialization.encodeToByteArray
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
||||||
|
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||||
|
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||||
|
import moe.fuqiuluo.symbols.OneBotHandler
|
||||||
|
import protobuf.msg.C2C
|
||||||
|
import protobuf.msg.ContentHead
|
||||||
|
import protobuf.msg.Elem
|
||||||
|
import protobuf.msg.GeneralFlags
|
||||||
|
import protobuf.msg.Grp
|
||||||
|
import protobuf.msg.MsgBody
|
||||||
|
import protobuf.msg.PbSendMsgReq
|
||||||
|
import protobuf.msg.RichText
|
||||||
|
import protobuf.msg.RoutingHead
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.random.nextUInt
|
||||||
|
|
||||||
|
@OneBotHandler("send_msg_by_resid")
|
||||||
|
internal object SendMsgByResid: IActionHandler() {
|
||||||
|
private val msgSeq = atomic(1000)
|
||||||
|
|
||||||
|
override suspend fun internalHandle(session: ActionSession): String {
|
||||||
|
val resid = session.getString("resid")
|
||||||
|
val peerId = session.getString("peer")
|
||||||
|
val req = PbSendMsgReq(
|
||||||
|
routingHead = RoutingHead().apply {
|
||||||
|
when(session.getStringOrNull("message_type")) {
|
||||||
|
"group" -> grp = Grp(peerId.toULong())
|
||||||
|
"private" -> c2c = C2C(peerId.toULong())
|
||||||
|
else -> grp = Grp(peerId.toULong())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentHead = ContentHead(1u, 0u, 0u, 0u),
|
||||||
|
msgBody = MsgBody(
|
||||||
|
richText = RichText(arrayListOf(Elem(
|
||||||
|
generalFlags = GeneralFlags(
|
||||||
|
long_text_flag = 1u,
|
||||||
|
long_text_resid = resid.toByteArray()
|
||||||
|
)
|
||||||
|
)))
|
||||||
|
),
|
||||||
|
msgSeq = msgSeq.incrementAndGet().toULong(),
|
||||||
|
msgRand = Random.nextUInt(),
|
||||||
|
msgVia = 0u
|
||||||
|
)
|
||||||
|
BaseSvc.sendBufferAW("MessageSvc.PbSendMsg", true, ProtoBuf.encodeToByteArray(req))
|
||||||
|
return ok("ok", session.echo)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
|
||||||
|
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||||
|
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||||
|
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
||||||
|
import moe.fuqiuluo.symbols.OneBotHandler
|
||||||
|
|
||||||
|
@OneBotHandler("sign_ark_message")
|
||||||
|
internal object SignArkMessage: IActionHandler() {
|
||||||
|
override suspend fun internalHandle(session: ActionSession): String {
|
||||||
|
val json = if(session.isString("json")) session.getString("json")
|
||||||
|
else session.getJsonElement("json").toString()
|
||||||
|
return invoke(json, session.echo)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend operator fun invoke(json: String, echo: JsonElement = EmptyJsonString): String {
|
||||||
|
/*
|
||||||
|
ArkMsgSvc.tryShareJsonMessage(json).onSuccess {
|
||||||
|
return ok(SignArkMessageResult(it), echo = echo)
|
||||||
|
}.onFailure {
|
||||||
|
return error(it.message ?: it.toString(), echo)
|
||||||
|
}*/
|
||||||
|
return logic("logic error", echo)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SignArkMessageResult(
|
||||||
|
val result: String
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||||
|
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||||
|
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||||||
|
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||||
|
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||||
|
import moe.fuqiuluo.symbols.OneBotHandler
|
||||||
|
import java.io.RandomAccessFile
|
||||||
|
|
||||||
|
@OneBotHandler("upload_file_to_shamrock")
|
||||||
|
internal object UploadFileToShamrock: IActionHandler() {
|
||||||
|
override suspend fun internalHandle(session: ActionSession): String {
|
||||||
|
val md5 = session.getString("md5").hex2ByteArray()
|
||||||
|
val offset = session.getStringOrNull("offset")?.toULong() ?: 0uL
|
||||||
|
val chunk = Base64.decode(session.getString("chunk"), Base64.DEFAULT)
|
||||||
|
val fileSize = session.getStringOrNull("file_size")?.toULong() ?: chunk.size.toULong()
|
||||||
|
return invoke(md5, fileSize, offset, chunk, session.echo)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun invoke(
|
||||||
|
md5: ByteArray,
|
||||||
|
fileSize: ULong,
|
||||||
|
offset: ULong,
|
||||||
|
chunk: ByteArray,
|
||||||
|
echo: JsonElement = EmptyJsonString
|
||||||
|
): String {
|
||||||
|
val file = FileUtils.getFileByMd5(md5.toHexString())
|
||||||
|
runCatching {
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.createNewFile()
|
||||||
|
}
|
||||||
|
val rd = RandomAccessFile(file, "rw")
|
||||||
|
rd.setLength(fileSize.toLong())
|
||||||
|
rd.seek(offset.toLong())
|
||||||
|
rd.write(chunk, 0, chunk.size)
|
||||||
|
rd.close()
|
||||||
|
}.onFailure {
|
||||||
|
return error(it.message ?: it.toString(), echo)
|
||||||
|
}
|
||||||
|
return ok(UploadFileResult(
|
||||||
|
fileSize = fileSize,
|
||||||
|
isFinish = fileSize <= offset + chunk.size.toULong(),
|
||||||
|
filePath = file.absolutePath
|
||||||
|
), echo = echo)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UploadFileResult(
|
||||||
|
@SerialName("file_size") val fileSize: ULong,
|
||||||
|
@SerialName("finish") val isFinish: Boolean,
|
||||||
|
@SerialName("path") val filePath: String
|
||||||
|
)
|
||||||
|
}
|
@ -81,7 +81,7 @@ internal object UploadGroupFile : IActionHandler() {
|
|||||||
fileElement.picThumbPath[750] = srcFile.absolutePath
|
fileElement.picThumbPath[750] = srcFile.absolutePath
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
val thumbPic = FileUtils.getFile(MD5.genFileMd5Hex(srcFile.absolutePath))
|
val thumbPic = FileUtils.getFileByMd5(MD5.genFileMd5Hex(srcFile.absolutePath))
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val fileOutputStream = FileOutputStream(thumbPic)
|
val fileOutputStream = FileOutputStream(thumbPic)
|
||||||
val retriever = MediaMetadataRetriever()
|
val retriever = MediaMetadataRetriever()
|
||||||
@ -103,7 +103,7 @@ internal object UploadGroupFile : IActionHandler() {
|
|||||||
|
|
||||||
// 根据文件大小调整超时时间
|
// 根据文件大小调整超时时间
|
||||||
val msgIdPair = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEGROUP)
|
val msgIdPair = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEGROUP)
|
||||||
val info = (withTimeoutOrNull((srcFile.length() / (300 * 1024)) * 1000 + 5000) {
|
val info = (withTimeoutOrNull((srcFile.length() / (125 * 1024)) * 1000 + 5000) {
|
||||||
val msgService = QRoute.api(IMsgService::class.java)
|
val msgService = QRoute.api(IMsgService::class.java)
|
||||||
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId)
|
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId)
|
||||||
suspendCancellableCoroutine<FileTransNotifyInfo?> {
|
suspendCancellableCoroutine<FileTransNotifyInfo?> {
|
||||||
|
@ -82,7 +82,7 @@ internal object UploadPrivateFile : IActionHandler() {
|
|||||||
fileElement.picThumbPath[750] = srcFile.absolutePath
|
fileElement.picThumbPath[750] = srcFile.absolutePath
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
val thumbPic = FileUtils.getFile(MD5.genFileMd5Hex(srcFile.absolutePath))
|
val thumbPic = FileUtils.getFileByMd5(MD5.genFileMd5Hex(srcFile.absolutePath))
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val fileOutputStream = FileOutputStream(thumbPic)
|
val fileOutputStream = FileOutputStream(thumbPic)
|
||||||
val retriever = MediaMetadataRetriever()
|
val retriever = MediaMetadataRetriever()
|
||||||
|
@ -5,6 +5,7 @@ import com.tencent.mobileqq.transfile.TransferRequest
|
|||||||
import com.tencent.mobileqq.transfile.api.ITransFileController
|
import com.tencent.mobileqq.transfile.api.ITransFileController
|
||||||
import io.ktor.server.routing.Routing
|
import io.ktor.server.routing.Routing
|
||||||
import io.ktor.server.routing.post
|
import io.ktor.server.routing.post
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.Status
|
import moe.fuqiuluo.shamrock.remote.structures.Status
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchPost
|
import moe.fuqiuluo.shamrock.tools.fetchPost
|
||||||
import moe.fuqiuluo.shamrock.tools.respond
|
import moe.fuqiuluo.shamrock.tools.respond
|
||||||
@ -15,7 +16,7 @@ import kotlin.random.Random
|
|||||||
import kotlin.random.nextLong
|
import kotlin.random.nextLong
|
||||||
|
|
||||||
fun Routing.registerBDH() {
|
fun Routing.registerBDH() {
|
||||||
post("/upload_group_image") {
|
if(ShamrockConfig.isDev()) post("/upload_group_image") {
|
||||||
val troop = fetchPost("troop")
|
val troop = fetchPost("troop")
|
||||||
val picBytes = Base64.decode(fetchPost("pic"), Base64.DEFAULT)
|
val picBytes = Base64.decode(fetchPost("pic"), Base64.DEFAULT)
|
||||||
val md5Str = MD5.getMd5Hex(picBytes)
|
val md5Str = MD5.getMd5Hex(picBytes)
|
||||||
@ -46,5 +47,4 @@ fun Routing.registerBDH() {
|
|||||||
.transferAsync(transferRequest)
|
.transferAsync(transferRequest)
|
||||||
respond(isOk = true, Status.Ok, "$md5Str.jpg")
|
respond(isOk = true, Status.Ok, "$md5Str.jpg")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -17,11 +17,13 @@ import moe.fuqiuluo.shamrock.tools.fetchGetOrThrow
|
|||||||
import moe.fuqiuluo.shamrock.tools.fetchOrNull
|
import moe.fuqiuluo.shamrock.tools.fetchOrNull
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
|
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchPostJsonArray
|
import moe.fuqiuluo.shamrock.tools.fetchPostJsonArray
|
||||||
|
import moe.fuqiuluo.shamrock.tools.fetchPostJsonElement
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject
|
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchPostJsonString
|
import moe.fuqiuluo.shamrock.tools.fetchPostJsonString
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchPostOrNull
|
import moe.fuqiuluo.shamrock.tools.fetchPostOrNull
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchPostOrThrow
|
import moe.fuqiuluo.shamrock.tools.fetchPostOrThrow
|
||||||
import moe.fuqiuluo.shamrock.tools.getOrPost
|
import moe.fuqiuluo.shamrock.tools.getOrPost
|
||||||
|
import moe.fuqiuluo.shamrock.tools.isJsonArray
|
||||||
import moe.fuqiuluo.shamrock.tools.isJsonData
|
import moe.fuqiuluo.shamrock.tools.isJsonData
|
||||||
import moe.fuqiuluo.shamrock.tools.isJsonObject
|
import moe.fuqiuluo.shamrock.tools.isJsonObject
|
||||||
import moe.fuqiuluo.shamrock.tools.isJsonString
|
import moe.fuqiuluo.shamrock.tools.isJsonString
|
||||||
@ -29,6 +31,19 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
|
|||||||
import moe.fuqiuluo.shamrock.tools.respond
|
import moe.fuqiuluo.shamrock.tools.respond
|
||||||
|
|
||||||
fun Routing.messageAction() {
|
fun Routing.messageAction() {
|
||||||
|
route("/sign_ark_message") {
|
||||||
|
get {
|
||||||
|
val json = fetchGetOrThrow("json")
|
||||||
|
call.respondText(SignArkMessage(json), ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
val json = if (isJsonData() && (isJsonObject("json") || isJsonArray("json")))
|
||||||
|
fetchPostJsonElement("json").toString()
|
||||||
|
else fetchPostOrThrow("json")
|
||||||
|
call.respondText(SignArkMessage(json), ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
route("/send_group_forward_(msg|message)".toRegex()) {
|
route("/send_group_forward_(msg|message)".toRegex()) {
|
||||||
post {
|
post {
|
||||||
val groupId = fetchPostOrNull("group_id")
|
val groupId = fetchPostOrNull("group_id")
|
||||||
|
@ -18,6 +18,7 @@ import moe.fuqiuluo.shamrock.remote.action.handlers.DownloadFile
|
|||||||
import moe.fuqiuluo.shamrock.remote.action.handlers.GetDeviceBattery
|
import moe.fuqiuluo.shamrock.remote.action.handlers.GetDeviceBattery
|
||||||
import moe.fuqiuluo.shamrock.remote.action.handlers.GetVersionInfo
|
import moe.fuqiuluo.shamrock.remote.action.handlers.GetVersionInfo
|
||||||
import moe.fuqiuluo.shamrock.remote.action.handlers.RestartMe
|
import moe.fuqiuluo.shamrock.remote.action.handlers.RestartMe
|
||||||
|
import moe.fuqiuluo.shamrock.remote.action.handlers.UploadFileToShamrock
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.Status
|
import moe.fuqiuluo.shamrock.remote.structures.Status
|
||||||
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
||||||
import moe.fuqiuluo.shamrock.tools.asString
|
import moe.fuqiuluo.shamrock.tools.asString
|
||||||
@ -25,6 +26,7 @@ import moe.fuqiuluo.shamrock.tools.fetchOrNull
|
|||||||
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
|
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchPostJsonArray
|
import moe.fuqiuluo.shamrock.tools.fetchPostJsonArray
|
||||||
import moe.fuqiuluo.shamrock.tools.getOrPost
|
import moe.fuqiuluo.shamrock.tools.getOrPost
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||||||
import moe.fuqiuluo.shamrock.tools.isJsonArray
|
import moe.fuqiuluo.shamrock.tools.isJsonArray
|
||||||
import moe.fuqiuluo.shamrock.tools.json
|
import moe.fuqiuluo.shamrock.tools.json
|
||||||
import moe.fuqiuluo.shamrock.tools.respond
|
import moe.fuqiuluo.shamrock.tools.respond
|
||||||
@ -113,6 +115,14 @@ fun Routing.otherAction() {
|
|||||||
respond(false, Status.BadRequest, "没有上传文件信息")
|
respond(false, Status.BadRequest, "没有上传文件信息")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOrPost("/upload_file_to_shamrock") {
|
||||||
|
val md5 = fetchOrThrow("md5").hex2ByteArray()
|
||||||
|
val offset = fetchOrNull("offset")?.toULong() ?: 0uL
|
||||||
|
val chunk = fetchOrThrow("chunk").toByteArray()
|
||||||
|
val fileSize = fetchOrNull("file_size")?.toULong() ?: chunk.size.toULong()
|
||||||
|
call.respondText(UploadFileToShamrock(md5, fileSize, offset, chunk), ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
|
||||||
getOrPost("/config/set_boolean") {
|
getOrPost("/config/set_boolean") {
|
||||||
val key = fetchOrThrow("key")
|
val key = fetchOrThrow("key")
|
||||||
val value = fetchOrThrow("value").toBooleanStrict()
|
val value = fetchOrThrow("value").toBooleanStrict()
|
||||||
|
@ -1,27 +1,9 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.api
|
package moe.fuqiuluo.shamrock.remote.api
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.IMsgOperateCallback
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.TextElement
|
|
||||||
import io.ktor.server.application.call
|
|
||||||
import io.ktor.server.response.respondText
|
|
||||||
import io.ktor.server.routing.Routing
|
import io.ktor.server.routing.Routing
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toCQCode
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
|
||||||
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
|
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
|
||||||
import moe.fuqiuluo.shamrock.tools.fetch
|
|
||||||
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
|
||||||
import java.util.ArrayList
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
fun Routing.testAction() {
|
fun Routing.testAction() {
|
||||||
if(ShamrockVersion.contains("dev")) {
|
if(ShamrockVersion.contains("dev")) {
|
||||||
|
@ -5,7 +5,9 @@ import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
|||||||
import io.ktor.server.application.call
|
import io.ktor.server.application.call
|
||||||
import io.ktor.server.response.respondText
|
import io.ktor.server.response.respondText
|
||||||
import io.ktor.server.routing.Routing
|
import io.ktor.server.routing.Routing
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
import moe.fuqiuluo.shamrock.remote.action.handlers.*
|
import moe.fuqiuluo.shamrock.remote.action.handlers.*
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.Status
|
import moe.fuqiuluo.shamrock.remote.structures.Status
|
||||||
import moe.fuqiuluo.shamrock.tools.*
|
import moe.fuqiuluo.shamrock.tools.*
|
||||||
|
|
||||||
@ -44,20 +46,36 @@ fun Routing.ticketActions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getTicket(uin: String, id: Int, debug: Boolean = false) = TicketSvc.getTicket(uin, id)?.let {
|
||||||
|
mutableMapOf(
|
||||||
|
"sig" to (it._sig?.toHexString() ?: "null"),
|
||||||
|
"key" to (it._sig_key?.toHexString() ?: "null")
|
||||||
|
).also { map ->
|
||||||
|
if (debug)
|
||||||
|
map["content"] = ((it._sig?.decodeToString() ?: "") + ":" + (it._sig_key?.decodeToString() ?: "null"))
|
||||||
|
}.json.asJsonObject
|
||||||
|
} ?: EmptyJsonObject
|
||||||
|
|
||||||
getOrPost("/get_ticket") {
|
getOrPost("/get_ticket") {
|
||||||
val uin = fetchOrThrow("uin")
|
val uin = fetchOrThrow("uin")
|
||||||
val ticket = when(val id = fetchOrThrow("id").toInt()) {
|
val ticket = when(val id = fetchOrThrow("id").toInt()) {
|
||||||
32 -> TicketSvc.getStWeb(uin)
|
32 -> TicketSvc.getStWeb(uin)
|
||||||
else -> {
|
else -> {
|
||||||
respond(true, Status.Ok, data = TicketSvc.getTicket(uin, id)?.let {
|
respond(true, Status.Ok, data = getTicket(uin, id))
|
||||||
mapOf(
|
|
||||||
"sig" to (it._sig?.toHexString() ?: "null"),
|
|
||||||
"key" to (it._sig_key?.toHexString() ?: "null")
|
|
||||||
).json.asJsonObject
|
|
||||||
} ?: EmptyJsonObject)
|
|
||||||
return@getOrPost
|
return@getOrPost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
respond(true, Status.Ok, data = ticket)
|
respond(true, Status.Ok, data = ticket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ShamrockConfig.isDev()) getOrPost("/get_all_ticket") {
|
||||||
|
val uin = fetchOrThrow("uin")
|
||||||
|
|
||||||
|
val ticketMap = mutableMapOf<Int, JsonElement>()
|
||||||
|
TicketSvc.SigType.ALL_TICKET.forEach {
|
||||||
|
ticketMap[it] = getTicket(uin, it, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(true, Status.Ok, data = ticketMap)
|
||||||
|
}
|
||||||
}
|
}
|
@ -26,35 +26,33 @@ import moe.fuqiuluo.shamrock.remote.action.handlers.QuickOperation.quicklyReply
|
|||||||
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
|
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
|
||||||
|
|
||||||
internal object HttpService: HttpTransmitServlet() {
|
internal object HttpService: HttpTransmitServlet() {
|
||||||
private val jobList = arrayListOf<Job>()
|
private val subscribes = arrayListOf<Job>()
|
||||||
|
|
||||||
override fun submitFlowJob(job: Job) {
|
override fun subscribe(job: Job) {
|
||||||
// HTTP 回调不会触发断连,无需释放之前的JOB
|
subscribes.add(job)
|
||||||
jobList.add(job)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelFlowJobs() {
|
override fun unsubscribe() {
|
||||||
jobList.removeIf {
|
subscribes.removeIf {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
return@removeIf true
|
return@removeIf true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initTransmitter() {
|
override fun init() {
|
||||||
if (jobList.isNotEmpty()) return
|
if (subscribes.isNotEmpty()) return
|
||||||
submitFlowJob(GlobalScope.launch {
|
subscribe(GlobalScope.launch {
|
||||||
GlobalEventTransmitter.onMessageEvent { (record, event) ->
|
GlobalEventTransmitter.onMessageEvent { (record, event) ->
|
||||||
val respond = pushTo(event) ?: return@onMessageEvent
|
val respond = pushTo(event) ?: return@onMessageEvent
|
||||||
handleQuicklyReply(record, event.messageId, respond.bodyAsText())
|
handleQuicklyReply(record, event.messageId, respond.bodyAsText())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
submitFlowJob(GlobalScope.launch {
|
subscribe(GlobalScope.launch {
|
||||||
GlobalEventTransmitter.onNoticeEvent { event ->
|
GlobalEventTransmitter.onNoticeEvent { event ->
|
||||||
pushTo(event)
|
pushTo(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
submitFlowJob(GlobalScope.launch {
|
subscribe(GlobalScope.launch {
|
||||||
GlobalEventTransmitter.onRequestEvent {
|
GlobalEventTransmitter.onRequestEvent {
|
||||||
pushTo(it)
|
pushTo(it)
|
||||||
}
|
}
|
||||||
|
@ -1,52 +1,51 @@
|
|||||||
@file:OptIn(DelicateCoroutinesApi::class)
|
|
||||||
|
|
||||||
package moe.fuqiuluo.shamrock.remote.service
|
package moe.fuqiuluo.shamrock.remote.service
|
||||||
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.fuqiuluo.shamrock.remote.service.api.WebSocketClientServlet
|
import moe.fuqiuluo.shamrock.remote.service.api.WebSocketClientServlet
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
|
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onMessageEvent
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onNoticeEvent
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onRequestEvent
|
||||||
|
|
||||||
internal class WebSocketClientService(
|
internal class WebSocketClientService(
|
||||||
override val address: String,
|
override val address: String,
|
||||||
heartbeatInterval: Long,
|
heartbeatInterval: Long,
|
||||||
wsHeaders: Map<String, String>
|
wsHeaders: Map<String, String>
|
||||||
) : WebSocketClientServlet(address, heartbeatInterval, wsHeaders) {
|
) : WebSocketClientServlet(address, heartbeatInterval, wsHeaders) {
|
||||||
private val eventJobList = mutableSetOf<Job>()
|
private val subscribes = mutableSetOf<Job>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
startHeartbeatTimer()
|
startHeartbeatTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun submitFlowJob(job: Job) {
|
override fun subscribe(job: Job) {
|
||||||
eventJobList.add(job)
|
subscribes.add(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initTransmitter() {
|
override fun init() {
|
||||||
submitFlowJob(GlobalScope.launch {
|
subscribe(launch {
|
||||||
GlobalEventTransmitter.onMessageEvent { (_, event) ->
|
onMessageEvent { (_, event) ->
|
||||||
pushTo(event)
|
pushTo(event)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
submitFlowJob(GlobalScope.launch {
|
subscribe(launch {
|
||||||
GlobalEventTransmitter.onNoticeEvent { event ->
|
onNoticeEvent { event ->
|
||||||
pushTo(event)
|
pushTo(event)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
submitFlowJob(GlobalScope.launch {
|
subscribe(launch {
|
||||||
GlobalEventTransmitter.onRequestEvent { event ->
|
onRequestEvent { event ->
|
||||||
pushTo(event)
|
pushTo(event)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
LogCenter.log("WebSocketClientService: 初始化服务", Level.WARN)
|
LogCenter.log("WebSocketClientService: 初始化服务", Level.WARN)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelFlowJobs() {
|
override fun unsubscribe() {
|
||||||
eventJobList.removeIf { job ->
|
subscribes.removeIf { job ->
|
||||||
job.cancel()
|
job.cancel()
|
||||||
return@removeIf true
|
return@removeIf true
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.service
|
package moe.fuqiuluo.shamrock.remote.service
|
||||||
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.fuqiuluo.shamrock.helper.ErrorTokenException
|
import moe.fuqiuluo.shamrock.helper.ErrorTokenException
|
||||||
@ -15,7 +14,9 @@ import moe.fuqiuluo.shamrock.remote.service.data.push.*
|
|||||||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
|
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onMessageEvent
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onNoticeEvent
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onRequestEvent
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
||||||
import org.java_websocket.WebSocket
|
import org.java_websocket.WebSocket
|
||||||
import org.java_websocket.handshake.ClientHandshake
|
import org.java_websocket.handshake.ClientHandshake
|
||||||
@ -26,33 +27,27 @@ internal class WebSocketService(
|
|||||||
port: Int,
|
port: Int,
|
||||||
heartbeatInterval: Long,
|
heartbeatInterval: Long,
|
||||||
): WebSocketTransmitServlet(host, port, heartbeatInterval) {
|
): WebSocketTransmitServlet(host, port, heartbeatInterval) {
|
||||||
private val eventJobList = mutableSetOf<Job>()
|
private val subscribes = mutableSetOf<Job>()
|
||||||
|
|
||||||
override fun submitFlowJob(job: Job) {
|
override fun subscribe(job: Job) {
|
||||||
eventJobList.add(job)
|
subscribes.add(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initTransmitter() {
|
override fun init() {
|
||||||
submitFlowJob(GlobalScope.launch {
|
subscribe(launch {
|
||||||
GlobalEventTransmitter.onMessageEvent { (_, event) ->
|
onMessageEvent { (_, event) -> pushTo(event) }
|
||||||
pushTo(event)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
submitFlowJob(GlobalScope.launch {
|
subscribe(launch {
|
||||||
GlobalEventTransmitter.onNoticeEvent { event ->
|
onNoticeEvent { event -> pushTo(event) }
|
||||||
pushTo(event)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
submitFlowJob(GlobalScope.launch {
|
subscribe(launch {
|
||||||
GlobalEventTransmitter.onRequestEvent { event ->
|
onRequestEvent { event -> pushTo(event) }
|
||||||
pushTo(event)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
LogCenter.log("WebSocketService: 初始化服务", Level.WARN)
|
LogCenter.log("WebSocketService: 初始化服务", Level.WARN)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelFlowJobs() {
|
override fun unsubscribe() {
|
||||||
eventJobList.removeIf { job ->
|
subscribes.removeIf { job ->
|
||||||
job.cancel()
|
job.cancel()
|
||||||
return@removeIf true
|
return@removeIf true
|
||||||
}
|
}
|
||||||
@ -60,8 +55,10 @@ internal class WebSocketService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpen(conn: WebSocket, handshake: ClientHandshake) {
|
override fun onOpen(conn: WebSocket, handshake: ClientHandshake) {
|
||||||
val token = ShamrockConfig.getActiveWebSocketConfig()?.token ?: ShamrockConfig.getToken()
|
val token = ShamrockConfig.getActiveWebSocketConfig()?.tokens
|
||||||
if (token.isNotBlank()) {
|
?: ShamrockConfig.getActiveWebSocketConfig()?.token?.split(",", "|", ",")
|
||||||
|
?: listOf(ShamrockConfig.getToken())
|
||||||
|
if (token.isNotEmpty()) {
|
||||||
var accessToken = handshake.getFieldValue("access_token")
|
var accessToken = handshake.getFieldValue("access_token")
|
||||||
.ifNullOrEmpty(handshake.getFieldValue("ticket"))
|
.ifNullOrEmpty(handshake.getFieldValue("ticket"))
|
||||||
.ifNullOrEmpty(handshake.getFieldValue("Authorization"))
|
.ifNullOrEmpty(handshake.getFieldValue("Authorization"))
|
||||||
@ -69,8 +66,7 @@ internal class WebSocketService(
|
|||||||
if (accessToken.startsWith("Bearer ", ignoreCase = true)) {
|
if (accessToken.startsWith("Bearer ", ignoreCase = true)) {
|
||||||
accessToken = accessToken.substring(7)
|
accessToken = accessToken.substring(7)
|
||||||
}
|
}
|
||||||
val tokenList = token.split(",", "|", ",")
|
if (!token.contains(accessToken)) {
|
||||||
if (!tokenList.contains(accessToken)) {
|
|
||||||
conn.close()
|
conn.close()
|
||||||
LogCenter.log({ "WSServer连接错误(${conn.remoteSocketAddress.address.hostAddress}:${conn.remoteSocketAddress.port}) 没有提供正确的token, $accessToken。" }, Level.ERROR)
|
LogCenter.log({ "WSServer连接错误(${conn.remoteSocketAddress.address.hostAddress}:${conn.remoteSocketAddress.port}) 没有提供正确的token, $accessToken。" }, Level.ERROR)
|
||||||
return
|
return
|
||||||
@ -85,7 +81,7 @@ internal class WebSocketService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun pushMetaLifecycle() {
|
private fun pushMetaLifecycle() {
|
||||||
GlobalScope.launch {
|
launch {
|
||||||
val runtime = AppRuntimeFetcher.appRuntime
|
val runtime = AppRuntimeFetcher.appRuntime
|
||||||
pushTo(PushMetaEvent(
|
pushTo(PushMetaEvent(
|
||||||
time = System.currentTimeMillis() / 1000,
|
time = System.currentTimeMillis() / 1000,
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.service.api
|
package moe.fuqiuluo.shamrock.remote.service.api
|
||||||
|
|
||||||
import com.tencent.mobileqq.app.QQAppInterface
|
import com.tencent.mobileqq.app.QQAppInterface
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeSubType
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeType
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
||||||
import oicq.wlogin_sdk.tools.MD5
|
import oicq.wlogin_sdk.tools.MD5
|
||||||
|
|
||||||
internal interface BaseTransmitServlet {
|
internal interface BaseTransmitServlet {
|
||||||
val address: String
|
val address: String
|
||||||
|
|
||||||
fun allowTransmit(): Boolean
|
fun transmitAccess(): Boolean
|
||||||
|
|
||||||
fun submitFlowJob(job: Job)
|
fun subscribe(job: Job)
|
||||||
|
|
||||||
fun cancelFlowJobs()
|
fun unsubscribe()
|
||||||
|
|
||||||
fun initTransmitter()
|
fun init()
|
||||||
|
|
||||||
val app: QQAppInterface
|
val app: QQAppInterface
|
||||||
get() = AppRuntimeFetcher.appRuntime as QQAppInterface
|
get() = AppRuntimeFetcher.appRuntime as QQAppInterface
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
|
@file:OptIn(DelicateCoroutinesApi::class)
|
||||||
|
|
||||||
package moe.fuqiuluo.shamrock.remote.service.api
|
package moe.fuqiuluo.shamrock.remote.service.api
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.CardSvc
|
import moe.fuqiuluo.qqinterface.servlet.CardSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
|
import moe.fuqiuluo.qqinterface.servlet.msg.msgelement.toSegments
|
||||||
|
import moe.fuqiuluo.qqinterface.servlet.msg.toJson
|
||||||
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.push.GroupFileMsg
|
import moe.fuqiuluo.shamrock.remote.service.data.push.GroupFileMsg
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.push.MemberRole
|
import moe.fuqiuluo.shamrock.remote.service.data.push.MemberRole
|
||||||
@ -48,7 +54,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
|||||||
private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message)
|
private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息 手淫器
|
* 消息
|
||||||
*/
|
*/
|
||||||
object MessageTransmitter {
|
object MessageTransmitter {
|
||||||
/**
|
/**
|
||||||
@ -74,9 +80,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
|||||||
peerId = uin,
|
peerId = uin,
|
||||||
userId = record.senderUin,
|
userId = record.senderUin,
|
||||||
message = if(ShamrockConfig.useCQ()) rawMsg.json
|
message = if(ShamrockConfig.useCQ()) rawMsg.json
|
||||||
else elements.toSegments(record.chatType, record.peerUin.toString(), "0").map {
|
else elements.toSegments(record.chatType, record.peerUin.toString(), "0").toJson(),
|
||||||
it.toJson()
|
|
||||||
}.json,
|
|
||||||
rawMessage = rawMsg,
|
rawMessage = rawMsg,
|
||||||
font = 0,
|
font = 0,
|
||||||
sender = Sender(
|
sender = Sender(
|
||||||
@ -86,11 +90,11 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
|||||||
.ifEmpty { record.sendMemberName }
|
.ifEmpty { record.sendMemberName }
|
||||||
.ifEmpty { record.peerName },
|
.ifEmpty { record.peerName },
|
||||||
card = record.sendMemberName,
|
card = record.sendMemberName,
|
||||||
role = when (record.senderUin) {
|
role = GroupSvc.getMemberRole(record.peerUin, record.senderUin)/*when (record.senderUin) {
|
||||||
GroupSvc.getOwner(record.peerUin.toString()) -> MemberRole.Owner
|
GroupSvc.getOwner(record.peerUin.toString()) -> MemberRole.Owner
|
||||||
in GroupSvc.getAdminList(record.peerUin.toString()) -> MemberRole.Admin
|
in GroupSvc.getAdminList(record.peerUin.toString()) -> MemberRole.Admin
|
||||||
else -> MemberRole.Member
|
else -> MemberRole.Member
|
||||||
},
|
}*/,
|
||||||
title = "",
|
title = "",
|
||||||
level = "",
|
level = "",
|
||||||
)
|
)
|
||||||
@ -108,7 +112,9 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
|||||||
rawMsg: String,
|
rawMsg: String,
|
||||||
msgHash: Int,
|
msgHash: Int,
|
||||||
postType: PostType,
|
postType: PostType,
|
||||||
tempSource: MessageTempSource = MessageTempSource.Unknown
|
tempSource: MessageTempSource = MessageTempSource.Unknown,
|
||||||
|
groupId: Long = Long.MIN_VALUE,
|
||||||
|
fromNick: String? = null
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val botUin = app.longAccountUin
|
val botUin = app.longAccountUin
|
||||||
var nickName = record.sendNickName
|
var nickName = record.sendNickName
|
||||||
@ -129,9 +135,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
|||||||
peerId = botUin,
|
peerId = botUin,
|
||||||
userId = record.senderUin,
|
userId = record.senderUin,
|
||||||
message = if(ShamrockConfig.useCQ()) rawMsg.json
|
message = if(ShamrockConfig.useCQ()) rawMsg.json
|
||||||
else elements.toSegments(record.chatType, record.peerUin.toString(), "0").map {
|
else elements.toSegments(record.chatType, record.peerUin.toString(), "0").toJson(),
|
||||||
it.toJson()
|
|
||||||
}.json,
|
|
||||||
rawMessage = rawMsg,
|
rawMessage = rawMsg,
|
||||||
font = 0,
|
font = 0,
|
||||||
sender = Sender(
|
sender = Sender(
|
||||||
@ -142,7 +146,9 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
|||||||
title = "",
|
title = "",
|
||||||
level = "",
|
level = "",
|
||||||
),
|
),
|
||||||
tmpSource = tempSource.id
|
tmpSource = tempSource.id,
|
||||||
|
groupId = groupId,
|
||||||
|
fromNickName = fromNick
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
@ -172,21 +178,21 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
|||||||
postType = postType,
|
postType = postType,
|
||||||
messageType = MsgType.Guild,
|
messageType = MsgType.Guild,
|
||||||
subType = MsgSubType.Channel,
|
subType = MsgSubType.Channel,
|
||||||
|
guildId = record.guildId,
|
||||||
|
channelId = record.channelId,
|
||||||
messageId = msgHash,
|
messageId = msgHash,
|
||||||
targetId = record.peerUin,
|
targetId = record.peerUin,
|
||||||
peerId = botUin,
|
peerId = botUin,
|
||||||
userId = record.senderUid.toLong(),
|
userId = record.senderUin,
|
||||||
message = if(ShamrockConfig.useCQ()) rawMsg.json
|
message = if(ShamrockConfig.useCQ()) rawMsg.json
|
||||||
else elements.toSegments(record.chatType, record.guildId, record.channelId).map {
|
else elements.toSegments(record.chatType, record.guildId, record.channelId).toJson(),
|
||||||
it.toJson()
|
|
||||||
}.json,
|
|
||||||
rawMessage = rawMsg,
|
rawMessage = rawMsg,
|
||||||
font = 0,
|
font = 0,
|
||||||
sender = Sender(
|
sender = Sender(
|
||||||
userId = record.senderUid.toLong(),
|
userId = record.senderUin,
|
||||||
nickname = nickName,
|
nickname = nickName,
|
||||||
card = record.sendMemberName,
|
card = record.sendMemberName,
|
||||||
role = MemberRole.Member,
|
role = MemberRole.Member, // TODO(GUILD ROLE)
|
||||||
title = record.sendNickName,
|
title = record.sendNickName,
|
||||||
level = record.roleId.toString(),
|
level = record.roleId.toString(),
|
||||||
tinyId = record.senderUid
|
tinyId = record.senderUid
|
||||||
@ -554,19 +560,29 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
|||||||
|
|
||||||
@ShamrockDsl
|
@ShamrockDsl
|
||||||
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) {
|
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) {
|
||||||
messageEventFlow.collect(collector)
|
messageEventFlow.collect {
|
||||||
|
GlobalScope.launch {
|
||||||
|
collector.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ShamrockDsl
|
@ShamrockDsl
|
||||||
suspend inline fun onNoticeEvent(collector: FlowCollector<NoticeEvent>) {
|
suspend inline fun onNoticeEvent(collector: FlowCollector<NoticeEvent>) {
|
||||||
noticeEventFlow
|
noticeEventFlow.collect {
|
||||||
.collect(collector)
|
GlobalScope.launch {
|
||||||
|
collector.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ShamrockDsl
|
@ShamrockDsl
|
||||||
suspend inline fun onRequestEvent(collector: FlowCollector<RequestEvent>) {
|
suspend inline fun onRequestEvent(collector: FlowCollector<RequestEvent>) {
|
||||||
requestEventFlow
|
requestEventFlow.collect {
|
||||||
.collect(collector)
|
GlobalScope.launch {
|
||||||
|
collector.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import io.ktor.client.request.setBody
|
|||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
||||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
@ -22,12 +21,12 @@ import java.net.SocketException
|
|||||||
internal abstract class HttpTransmitServlet : BaseTransmitServlet {
|
internal abstract class HttpTransmitServlet : BaseTransmitServlet {
|
||||||
override val address: String by lazy { ShamrockConfig.getWebHookAddress() }
|
override val address: String by lazy { ShamrockConfig.getWebHookAddress() }
|
||||||
|
|
||||||
override fun allowTransmit(): Boolean {
|
override fun transmitAccess(): Boolean {
|
||||||
return ShamrockConfig.allowWebHook()
|
return ShamrockConfig.allowWebHook()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend inline fun <reified T> pushTo(body: T): HttpResponse? {
|
protected suspend inline fun <reified T> pushTo(body: T): HttpResponse? {
|
||||||
if (!allowTransmit()) return null
|
if (!transmitAccess()) return null
|
||||||
try {
|
try {
|
||||||
if (address.startsWith("http://") || address.startsWith("https://")) {
|
if (address.startsWith("http://") || address.startsWith("https://")) {
|
||||||
val response = GlobalClient.post(address) {
|
val response = GlobalClient.post(address) {
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
package moe.fuqiuluo.shamrock.remote.service.api
|
package moe.fuqiuluo.shamrock.remote.service.api
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionManager
|
import moe.fuqiuluo.shamrock.remote.action.ActionManager
|
||||||
@ -30,12 +31,13 @@ import org.java_websocket.handshake.ServerHandshake
|
|||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.concurrent.timer
|
import kotlin.concurrent.timer
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
internal abstract class WebSocketClientServlet(
|
internal abstract class WebSocketClientServlet(
|
||||||
private val url: String,
|
private val url: String,
|
||||||
private val heartbeatInterval: Long,
|
private val heartbeatInterval: Long,
|
||||||
private val wsHeaders: Map<String, String>
|
private val wsHeaders: Map<String, String>
|
||||||
) : BaseTransmitServlet, WebSocketClient(URI(url), wsHeaders) {
|
) : BaseTransmitServlet, WebSocketClient(URI(url), wsHeaders), CoroutineScope {
|
||||||
init {
|
init {
|
||||||
if (connectedClients.containsKey(url)) {
|
if (connectedClients.containsKey(url)) {
|
||||||
throw RuntimeException("WebSocketClient已存在: $url")
|
throw RuntimeException("WebSocketClient已存在: $url")
|
||||||
@ -43,14 +45,13 @@ internal abstract class WebSocketClientServlet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var firstOpen = true
|
private var firstOpen = true
|
||||||
private val sendLock = Mutex()
|
|
||||||
|
|
||||||
override fun allowTransmit(): Boolean {
|
override fun transmitAccess(): Boolean {
|
||||||
return ShamrockConfig.openWebSocketClient()
|
return ShamrockConfig.openWebSocketClient()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMessage(message: String) {
|
override fun onMessage(message: String) {
|
||||||
GlobalScope.launch {
|
launch {
|
||||||
handleMessage(message)
|
handleMessage(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,14 +86,13 @@ internal abstract class WebSocketClientServlet(
|
|||||||
|
|
||||||
connectedClients[url] = this
|
connectedClients[url] = this
|
||||||
|
|
||||||
//startHeartbeatTimer()
|
|
||||||
pushMetaLifecycle()
|
pushMetaLifecycle()
|
||||||
if (firstOpen) {
|
if (firstOpen) {
|
||||||
firstOpen = false
|
firstOpen = false
|
||||||
} else {
|
} else {
|
||||||
cancelFlowJobs()
|
unsubscribe()
|
||||||
}
|
}
|
||||||
initTransmitter()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClose(code: Int, reason: String?, remote: Boolean) {
|
override fun onClose(code: Int, reason: String?, remote: Boolean) {
|
||||||
@ -105,22 +105,22 @@ internal abstract class WebSocketClientServlet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LogCenter.log("WebSocketClient onClose: $code, $reason, $remote")
|
LogCenter.log("WebSocketClient onClose: $code, $reason, $remote")
|
||||||
cancelFlowJobs()
|
unsubscribe()
|
||||||
|
coroutineContext.cancel()
|
||||||
connectedClients.remove(url)
|
connectedClients.remove(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(ex: Exception?) {
|
override fun onError(ex: Exception?) {
|
||||||
LogCenter.log("WebSocketClient onError: ${ex?.message}")
|
LogCenter.log("WebSocketClient onError: ${ex?.message}")
|
||||||
cancelFlowJobs()
|
unsubscribe()
|
||||||
|
coroutineContext.cancel()
|
||||||
connectedClients.remove(url)
|
connectedClients.remove(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend inline fun <reified T> pushTo(body: T) {
|
protected suspend inline fun <reified T> pushTo(body: T) {
|
||||||
if (!allowTransmit() || isClosed || isClosing) return
|
if (!transmitAccess() || isClosed || isClosing) return
|
||||||
try {
|
try {
|
||||||
sendLock.withLock {
|
send(GlobalJson.encodeToString(body))
|
||||||
send(GlobalJson.encodeToString(body))
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
LogCenter.log("被动WS推送失败: ${e.stackTraceToString()}", Level.ERROR)
|
LogCenter.log("被动WS推送失败: ${e.stackTraceToString()}", Level.ERROR)
|
||||||
}
|
}
|
||||||
@ -142,8 +142,7 @@ internal abstract class WebSocketClientServlet(
|
|||||||
}
|
}
|
||||||
val runtime = AppRuntimeFetcher.appRuntime
|
val runtime = AppRuntimeFetcher.appRuntime
|
||||||
LogCenter.log("WebSocketClient心跳: ${app.longAccountUin}", Level.DEBUG)
|
LogCenter.log("WebSocketClient心跳: ${app.longAccountUin}", Level.DEBUG)
|
||||||
send(
|
send(GlobalJson.encodeToString(
|
||||||
GlobalJson.encodeToString(
|
|
||||||
PushMetaEvent(
|
PushMetaEvent(
|
||||||
time = System.currentTimeMillis() / 1000,
|
time = System.currentTimeMillis() / 1000,
|
||||||
selfId = app.longAccountUin,
|
selfId = app.longAccountUin,
|
||||||
@ -164,7 +163,7 @@ internal abstract class WebSocketClientServlet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun pushMetaLifecycle() {
|
private fun pushMetaLifecycle() {
|
||||||
GlobalScope.launch {
|
launch {
|
||||||
val runtime = AppRuntimeFetcher.appRuntime
|
val runtime = AppRuntimeFetcher.appRuntime
|
||||||
val curUin = runtime.currentAccountUin
|
val curUin = runtime.currentAccountUin
|
||||||
pushTo(
|
pushTo(
|
||||||
@ -183,6 +182,10 @@ internal abstract class WebSocketClientServlet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
override val coroutineContext: CoroutineContext =
|
||||||
|
Dispatchers.IO.limitedParallelism(20)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val connectedClients = mutableMapOf<String, WebSocketClientServlet>()
|
private val connectedClients = mutableMapOf<String, WebSocketClientServlet>()
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
@file:OptIn(DelicateCoroutinesApi::class)
|
@file:OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
package moe.fuqiuluo.shamrock.remote.service.api
|
package moe.fuqiuluo.shamrock.remote.service.api
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionManager
|
import moe.fuqiuluo.shamrock.remote.action.ActionManager
|
||||||
@ -31,20 +32,25 @@ import org.java_websocket.server.WebSocketServer
|
|||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
import java.util.Timer
|
||||||
import kotlin.concurrent.timer
|
import kotlin.concurrent.timer
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
internal abstract class WebSocketTransmitServlet(
|
internal abstract class WebSocketTransmitServlet(
|
||||||
host:String,
|
host:String,
|
||||||
port: Int,
|
port: Int,
|
||||||
protected val heartbeatInterval: Long,
|
protected val heartbeatInterval: Long,
|
||||||
) : BaseTransmitServlet, WebSocketServer(InetSocketAddress(host, port)) {
|
) : BaseTransmitServlet, WebSocketServer(InetSocketAddress(host, port)), CoroutineScope {
|
||||||
private val sendLock = Mutex()
|
private lateinit var heartbeatTask: Timer
|
||||||
protected val eventReceivers: MutableList<WebSocket> = Collections.synchronizedList(mutableListOf<WebSocket>())
|
protected val eventReceivers: MutableList<WebSocket> = Collections.synchronizedList(mutableListOf<WebSocket>())
|
||||||
|
|
||||||
override val address: String
|
init {
|
||||||
get() = "-"
|
connectionLostTimeout = 0
|
||||||
|
}
|
||||||
|
|
||||||
override fun allowTransmit(): Boolean {
|
override val address: String = "-"
|
||||||
|
|
||||||
|
override fun transmitAccess(): Boolean {
|
||||||
return ShamrockConfig.openWebSocket()
|
return ShamrockConfig.openWebSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +64,7 @@ internal abstract class WebSocketTransmitServlet(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
if (heartbeatInterval > 0) {
|
if (heartbeatInterval > 0) {
|
||||||
timer("heartbeat", true, 0, heartbeatInterval) {
|
heartbeatTask = timer("heartbeat", true, 0, heartbeatInterval) {
|
||||||
val runtime = AppRuntimeFetcher.appRuntime
|
val runtime = AppRuntimeFetcher.appRuntime
|
||||||
val curUin = runtime.currentAccountUin
|
val curUin = runtime.currentAccountUin
|
||||||
LogCenter.log("WebSocket心跳: $curUin", Level.DEBUG)
|
LogCenter.log("WebSocket心跳: $curUin", Level.DEBUG)
|
||||||
@ -100,7 +106,7 @@ internal abstract class WebSocketTransmitServlet(
|
|||||||
|
|
||||||
override fun onMessage(conn: WebSocket, message: String) {
|
override fun onMessage(conn: WebSocket, message: String) {
|
||||||
val path = URI.create(conn.resourceDescriptor).path
|
val path = URI.create(conn.resourceDescriptor).path
|
||||||
GlobalScope.launch {
|
launch {
|
||||||
onHandleAction(conn, message, path)
|
onHandleAction(conn, message, path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,22 +131,27 @@ internal abstract class WebSocketTransmitServlet(
|
|||||||
|
|
||||||
override fun onError(conn: WebSocket, ex: Exception?) {
|
override fun onError(conn: WebSocket, ex: Exception?) {
|
||||||
LogCenter.log("WSServer Error: " + ex?.stackTraceToString(), Level.ERROR)
|
LogCenter.log("WSServer Error: " + ex?.stackTraceToString(), Level.ERROR)
|
||||||
cancelFlowJobs()
|
unsubscribe()
|
||||||
|
coroutineContext.cancel()
|
||||||
|
if (::heartbeatTask.isInitialized) {
|
||||||
|
heartbeatTask.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
LogCenter.log("WSServer start running on ws://${getAddress()}!")
|
LogCenter.log("WSServer start running on ws://${getAddress()}!")
|
||||||
initTransmitter()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend inline fun <reified T> pushTo(body: T) {
|
protected inline fun <reified T> pushTo(body: T) {
|
||||||
if(!allowTransmit()) return
|
if(!transmitAccess()) return
|
||||||
try {
|
try {
|
||||||
sendLock.withLock {
|
broadcastTextEvent(GlobalJson.encodeToString(body))
|
||||||
broadcastTextEvent(GlobalJson.encodeToString(body))
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
LogCenter.log("WS推送失败: ${e.stackTraceToString()}", Level.ERROR)
|
LogCenter.log("WS推送失败: ${e.stackTraceToString()}", Level.ERROR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext =
|
||||||
|
Dispatchers.IO.limitedParallelism(40)
|
||||||
}
|
}
|
@ -18,6 +18,7 @@ data class ConnectionConfig(
|
|||||||
@SerialName("address") val address: String? = null,
|
@SerialName("address") val address: String? = null,
|
||||||
@SerialName("port") var port: Int? = null,
|
@SerialName("port") var port: Int? = null,
|
||||||
@SerialName("token") val token: String? = null,
|
@SerialName("token") val token: String? = null,
|
||||||
|
@SerialName("tokens") val tokens: List<String>? = null,
|
||||||
@SerialName("heartbeat_interval") var heartbeatInterval: Long? = null,
|
@SerialName("heartbeat_interval") var heartbeatInterval: Long? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -76,6 +76,7 @@ internal object ShamrockConfig {
|
|||||||
putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息
|
putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息
|
||||||
putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口
|
putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口
|
||||||
putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息
|
putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息
|
||||||
|
putBoolean("forbid_useless_process", intent.getBooleanExtra("forbid_useless_process", false)) // 禁用QQ生成无用进程
|
||||||
}
|
}
|
||||||
Config.defaultToken = intent.getStringExtra("token")
|
Config.defaultToken = intent.getStringExtra("token")
|
||||||
Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
|
Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
|
||||||
@ -126,6 +127,10 @@ internal object ShamrockConfig {
|
|||||||
return mmkv.getBoolean("enable_self_msg", false)
|
return mmkv.getBoolean("enable_self_msg", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun forbidUselessProcess(): Boolean {
|
||||||
|
return mmkv.getBoolean("forbid_useless_process", false)
|
||||||
|
}
|
||||||
|
|
||||||
fun openWebSocketClient(): Boolean {
|
fun openWebSocketClient(): Boolean {
|
||||||
return mmkv.getBoolean("ws_client", false)
|
return mmkv.getBoolean("ws_client", false)
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,12 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
internal data class Credentials(
|
internal data class Credentials(
|
||||||
@SerialName("token") val bkn: String = "",
|
@SerialName("token") val bkn: String = "",
|
||||||
@SerialName("cookies") val cookie: String = ""
|
@SerialName("cookies") val cookie: String = "",
|
||||||
|
@SerialName("bigdata_ticket") val bigDataTicket: BigDataTicket? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BigDataTicket(
|
||||||
|
var key: String? = null,
|
||||||
|
var sig: String? = null
|
||||||
)
|
)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user