mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 05:12:17 +00:00
Compare commits
39 Commits
1.0.8
...
c70f3eabfe
Author | SHA1 | Date | |
---|---|---|---|
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 |
12
.github/workflows/build-apk.yml
vendored
12
.github/workflows/build-apk.yml
vendored
@ -22,13 +22,13 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v4.0.0
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: "adopt"
|
||||
|
||||
- name: Cache Gradle Dependencies
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@ -38,7 +38,7 @@ jobs:
|
||||
restore-keys: gradle-deps
|
||||
|
||||
- name: Cache Gradle Build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches/build-cache-*
|
||||
@ -82,19 +82,19 @@ jobs:
|
||||
echo "|x86_64|$x86_64" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload ALL APK RELEASE
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "${{ env.SHAMROCK_VERSION_ALL }}"
|
||||
path: "${{ env.APK_FILE_ALL }}"
|
||||
|
||||
- name: Upload ARM64 APK RELEASE
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "${{ env.SHAMROCK_VERSION_ARM64 }}"
|
||||
path: "${{ env.APK_FILE_ARM64 }}"
|
||||
|
||||
- name: Upload X86_64 APK RELEASE
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "${{ env.SHAMROCK_VERSION_x86_64 }}"
|
||||
path: "${{ env.APK_FILE_X86_64 }}"
|
12
SECURITY.md
12
SECURITY.md
@ -1,11 +1,19 @@
|
||||
# Security Policy
|
||||
|
||||
## Support Version
|
||||
## 支持的版本
|
||||
|
||||
| Version | Supported |
|
||||
| 版本 | 支持状态 |
|
||||
| ------- | ------------------ |
|
||||
| 9.0.15 | :white_check_mark: |
|
||||
| 8.9.75 | :white_check_mark: |
|
||||
| 8.9.73 | :white_check_mark: |
|
||||
| 8.9.98 | :white_check_mark: |
|
||||
| < 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("anti_qq_trace", v).apply()
|
||||
@ -333,6 +343,7 @@ object ShamrockConfig {
|
||||
"alive_reply" to preferences.getBoolean("alive_reply", 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),
|
||||
"forbid_useless_process" to preferences.getBoolean("forbid_useless_process", false)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -100,17 +100,16 @@ fun LabFragment() {
|
||||
thickness = 0.2.dp
|
||||
)
|
||||
|
||||
/*
|
||||
Function(
|
||||
title = "自动清理QQ垃圾",
|
||||
desc = "也许会导致奇怪的问题(无效)。",
|
||||
title = "禁止无用进程",
|
||||
desc = "禁止QQ生成无用进程浪费内存",
|
||||
descColor = color,
|
||||
isSwitch = ShamrockConfig.isAutoClean(ctx)
|
||||
isSwitch = ShamrockConfig.isForbidUselessProcess(ctx)
|
||||
) {
|
||||
ShamrockConfig.setAutoClean(ctx, it)
|
||||
ShamrockConfig.setForbidUselessProcess(ctx, it)
|
||||
ShamrockConfig.pushUpdate(ctx)
|
||||
return@Function false
|
||||
}*/
|
||||
return@Function true
|
||||
}
|
||||
|
||||
Function(
|
||||
title = "自回复测试",
|
||||
|
@ -5,53 +5,13 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
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
|
||||
data class RichMessage(
|
||||
@ProtoNumber(1) val elements: MessageElementList? = null,
|
||||
@ProtoNumber(2) val rawBuffer: ByteArray? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MessageElementList(
|
||||
@ProtoNumber(1) val font: Font? = null,
|
||||
@ProtoNumber(2) val elements: List<MessageElement>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MessageElement(
|
||||
@ProtoNumber(51) val json: JsonElement? = 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,
|
||||
data class Font(
|
||||
@ProtoNumber(9) val fontName: String? = 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
|
||||
)
|
30
protobuf/src/main/java/protobuf/message/MessageContent.kt
Normal file
30
protobuf/src/main/java/protobuf/message/MessageContent.kt
Normal file
@ -0,0 +1,30 @@
|
||||
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 = Int.MIN_VALUE,
|
||||
@ProtoNumber(4) val msgViaRandom: 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(8) val u6: Int? = null,
|
||||
@ProtoNumber(9) val u7: Int? = null,
|
||||
@ProtoNumber(11) val u3: 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 u4: 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 commElem: CommonElement? = null,
|
||||
)
|
31
protobuf/src/main/java/protobuf/message/MessageHead.kt
Normal file
31
protobuf/src/main/java/protobuf/message/MessageHead.kt
Normal file
@ -0,0 +1,31 @@
|
||||
@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,
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
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,13 @@
|
||||
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,9 @@
|
||||
package protobuf.message.element
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
|
||||
@Serializable
|
||||
data class TextElement(
|
||||
@ProtoNumber(1) val text: String? = 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 acquire: Boolean? = 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: 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ package protobuf.push
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import protobuf.message.MessageBody
|
||||
import protobuf.message.NtMessage
|
||||
|
||||
@Serializable
|
||||
data class MessagePush(
|
||||
@ProtoNumber(1) val msgBody: MessageBody? = null,
|
||||
@ProtoNumber(1) val msgBody: NtMessage? = 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;
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.IGetTempChatInfoCallback;
|
||||
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener;
|
||||
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.TempChatPrepareInfo;
|
||||
|
||||
@ -9,6 +11,10 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class MsgService {
|
||||
void getRichMediaElement(@NotNull RichMediaElementGetReq req) {
|
||||
|
||||
}
|
||||
|
||||
public void addMsgListener(IKernelMsgListener listener) {
|
||||
}
|
||||
|
||||
@ -24,4 +30,8 @@ public class MsgService {
|
||||
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 + ",}";
|
||||
}
|
||||
|
||||
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) {
|
||||
this.id = "";
|
||||
this.label = "";
|
||||
|
@ -264,6 +264,10 @@ public final class PicElement implements IKernelModel {
|
||||
this.transferStatus = num;
|
||||
}
|
||||
|
||||
public int getStoreID() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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 + ",}";
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.GroupAnnouncementMessage
|
||||
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.GlobalClient
|
||||
import moe.fuqiuluo.shamrock.tools.asInt
|
||||
@ -96,6 +97,8 @@ import java.nio.ByteBuffer
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal object GroupSvc: BaseSvc() {
|
||||
private const val GET_MEMBER_ROLE_BY_NT = false
|
||||
|
||||
private val RefreshTroopMemberInfoLock by lazy {
|
||||
Mutex()
|
||||
}
|
||||
@ -394,6 +397,27 @@ internal object GroupSvc: BaseSvc() {
|
||||
.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 {
|
||||
val groupInfo = getGroupInfo(groupId)
|
||||
return groupInfo.troopowneruin?.toLong() ?: 0
|
||||
@ -566,24 +590,53 @@ internal object GroupSvc: BaseSvc() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTroopMemberInfoByUinViaNt(groupId: String, qq: Long): Result<MemberInfo> {
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
val sessionService = kernelService.wrapperSession
|
||||
val groupService = sessionService.groupService
|
||||
val info = suspendCancellableCoroutine {
|
||||
groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data ->
|
||||
if (code != 0) {
|
||||
it.resume(null)
|
||||
return@getTransferableMemberInfo
|
||||
suspend fun getTroopMemberInfoByUinV2(
|
||||
groupId: String,
|
||||
uin: String,
|
||||
refresh: Boolean = false
|
||||
): Result<TroopMemberInfo> {
|
||||
val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
|
||||
var info = service.getTroopMember(groupId, uin)
|
||||
if (refresh || !service.isMemberInCache(groupId, uin) || info == null || info.troopnick == null) {
|
||||
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)
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
try {
|
||||
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) {
|
||||
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> {
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
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 groupIdStr = groupId.toString()
|
||||
val memberUinStr = memberUin.toString()
|
||||
@ -758,7 +845,7 @@ internal object GroupSvc: BaseSvc() {
|
||||
requestMemberInfoV2(groupId, memberUin)
|
||||
requestMemberInfo(groupId, memberUin)
|
||||
|
||||
withTimeoutOrNull(10000) {
|
||||
withTimeoutOrNull(timeout) {
|
||||
while (!service.isMemberInCache(groupIdStr, memberUinStr)) {
|
||||
delay(200)
|
||||
}
|
||||
|
@ -5,11 +5,7 @@ package moe.fuqiuluo.qqinterface.servlet
|
||||
import com.tencent.mobileqq.qroute.QRoute
|
||||
import com.tencent.mobileqq.troop.api.ITroopMemberNameService
|
||||
import com.tencent.qqnt.kernel.api.IKernelService
|
||||
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback
|
||||
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.kernel.nativeinterface.*
|
||||
import com.tencent.qqnt.msg.api.IMsgService
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@ -17,21 +13,33 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
|
||||
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.SendMsgException
|
||||
import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements
|
||||
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.msgService
|
||||
import protobuf.message.*
|
||||
import protobuf.message.longmsg.*
|
||||
import tencent.mobileim.structmsg.structmsg.SystemMsg
|
||||
import java.util.UUID
|
||||
import kotlin.collections.slice
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextLong
|
||||
|
||||
internal object MsgSvc: BaseSvc() {
|
||||
internal object MsgSvc : BaseSvc() {
|
||||
suspend fun prepareTempChatFromGroup(
|
||||
groupId: String,
|
||||
peerId: String
|
||||
@ -39,13 +47,19 @@ internal object MsgSvc: BaseSvc() {
|
||||
LogCenter.log("主动临时消息,创建临时会话。", Level.INFO)
|
||||
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
|
||||
?: return Result.failure(Exception("获取消息服务失败"))
|
||||
msgService.prepareTempChat(TempChatPrepareInfo(
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
|
||||
ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
|
||||
app.getRuntimeService(ITroopMemberNameService::class.java, "all")
|
||||
.getTroopMemberNameRemarkFirst(groupId, peerId),
|
||||
groupId, EMPTY_BYTE_ARRAY, app.currentUid, "", TempChatGameSession()
|
||||
)) { code, reason ->
|
||||
msgService.prepareTempChat(
|
||||
TempChatPrepareInfo(
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
|
||||
ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
|
||||
app.getRuntimeService(ITroopMemberNameService::class.java, "all")
|
||||
.getTroopMemberNameRemarkFirst(groupId, peerId),
|
||||
groupId,
|
||||
EMPTY_BYTE_ARRAY,
|
||||
app.currentUid,
|
||||
"",
|
||||
TempChatGameSession()
|
||||
)
|
||||
) { code, reason ->
|
||||
if (code != 0) {
|
||||
LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR)
|
||||
}
|
||||
@ -53,6 +67,24 @@ internal object MsgSvc: BaseSvc() {
|
||||
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 +93,7 @@ internal object MsgSvc: BaseSvc() {
|
||||
?: return Result.failure(Exception("没有对应消息映射,消息获取失败"))
|
||||
|
||||
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 service = QRoute.api(IMsgService::class.java)
|
||||
@ -152,7 +184,7 @@ internal object MsgSvc: BaseSvc() {
|
||||
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
|
||||
?: 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 ->
|
||||
msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why ->
|
||||
@ -182,9 +214,10 @@ internal object MsgSvc: BaseSvc() {
|
||||
}
|
||||
}
|
||||
}
|
||||
val result = MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
|
||||
result.onFailure {
|
||||
LogCenter.log("sendToAio: " + it.stackTraceToString(), Level.ERROR)
|
||||
val result =
|
||||
MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
|
||||
if (result.isFailure) {
|
||||
LogCenter.log("sendToAio: " + result.exceptionOrNull()?.stackTraceToString(), Level.ERROR)
|
||||
return result
|
||||
}
|
||||
val sendResult = result.getOrThrow()
|
||||
@ -198,13 +231,54 @@ internal object MsgSvc: BaseSvc() {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendMultiMsg(
|
||||
uid: String,
|
||||
groupUin: String?,
|
||||
messages: List<PushMsgBody>,
|
||||
): Result<String> {
|
||||
val payload = LongMsgPayload(
|
||||
action = LongMsgAction(
|
||||
command = "MultiMsg",
|
||||
data = LongMsgContent(
|
||||
messages
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val req = LongMsgReq(
|
||||
sendInfo = SendLongMsgInfo(
|
||||
type = if (groupUin == null) 1 else 3,
|
||||
uid = LongMsgUid(groupUin ?: uid),
|
||||
groupUin = groupUin?.toInt(),
|
||||
payload = DeflateTools.gzip(ProtoBuf.encodeToByteArray(payload))
|
||||
),
|
||||
setting = LongMsgSettings(
|
||||
field1 = 4,
|
||||
field2 = 2,
|
||||
field3 = 9,
|
||||
field4 = 0
|
||||
)
|
||||
)
|
||||
val buffer = sendBufferAW(
|
||||
"trpc.group.long_msg_interface.MsgService.SsoSendLongMsg",
|
||||
true,
|
||||
ProtoBuf.encodeToByteArray(req)
|
||||
) ?: 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) } ?: Result.failure(Exception("unable to upload multi message"))
|
||||
}
|
||||
|
||||
suspend fun getMultiMsg(resId: String): Result<List<MsgRecord>> {
|
||||
// trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg
|
||||
// 00 00 00 70 0A 60 0A 1A 12 18 75 5F 35 5A 5A 53 6F 38 63 4D 71 70 49 79 63 75 57 5F 78 43 4C 48 6E 77 12 40 4D 6F 61 44 38 77 2B 55 74 43 42 55 45 4C 4F 66 7A 61 72 69 43 7A 4F 5A 44 57 4B 43 6D 68 45 74 4F 65 54 6C 46 66 44 70 2F 73 61 56 77 50 2F 44 52 37 72 4A 2B 4B 4B 47 30 65 71 2B 6C 4B 58 34 18 03 7A 08 08 02 10 02 18 09 20 00
|
||||
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
val sessionService = kernelService.wrapperSession
|
||||
val msgService = sessionService.msgService
|
||||
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin())
|
||||
|
||||
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 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 msgId = PacketSvc.fakeSelfRecvJsonMsg(msgService, content)
|
||||
if (msgId < 0) {
|
||||
return Result.failure(Exception("获取合并转发消息ID失败"))
|
||||
@ -239,7 +313,7 @@ internal object MsgSvc: BaseSvc() {
|
||||
class MessageCallback(
|
||||
private val peerId: String,
|
||||
var msgHash: Int
|
||||
): IOperateCallback {
|
||||
) : IOperateCallback {
|
||||
override fun onResult(code: Int, reason: String?) {
|
||||
if (code != 0 && msgHash != 0) {
|
||||
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.tools.broadcast
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import protobuf.message.JsonElement
|
||||
import protobuf.message.MessageBody
|
||||
import protobuf.message.MessageContentHead
|
||||
import protobuf.message.element.JsonElement
|
||||
import protobuf.message.NtMessage
|
||||
import protobuf.message.MessageContent
|
||||
import protobuf.message.MessageElement
|
||||
import protobuf.message.MessageElementList
|
||||
import protobuf.message.MessageHead
|
||||
import protobuf.message.RichMessage
|
||||
import protobuf.message.MessageHead
|
||||
import protobuf.message.MessageBody
|
||||
import protobuf.push.MessagePush
|
||||
import mqq.app.MobileQQ
|
||||
import kotlin.coroutines.resume
|
||||
@ -51,7 +51,7 @@ internal object PacketSvc: BaseSvc() {
|
||||
val msgSeq = (latestMsg?.msgSeq ?: 0) + 1
|
||||
|
||||
val msgPush = MessagePush(
|
||||
msgBody = MessageBody(
|
||||
msgBody = NtMessage(
|
||||
msgHead = MessageHead(
|
||||
peer = app.longAccountUin,
|
||||
peerUid = app.currentUid,
|
||||
@ -59,11 +59,11 @@ internal object PacketSvc: BaseSvc() {
|
||||
receiver = app.longAccountUin,
|
||||
receiverUid = app.currentUid
|
||||
),
|
||||
contentHead = MessageContentHead(
|
||||
contentHead = MessageContent(
|
||||
msgType = 166,
|
||||
msgSubType = 11,
|
||||
msgSeq = msgSeq,
|
||||
u1 = msgSeq,
|
||||
msgViaRandom = msgSeq,
|
||||
msgTime = System.currentTimeMillis() / 1000,
|
||||
u2 = 1,
|
||||
u3 = msgSeq,
|
||||
@ -71,7 +71,9 @@ internal object PacketSvc: BaseSvc() {
|
||||
u4 = msgSeq - 2,
|
||||
u5 = msgSeq
|
||||
),
|
||||
richMsg = RichMessage(MessageElementList(builder()))
|
||||
body = MessageBody(RichMessage(
|
||||
elements = builder()
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -13,8 +13,11 @@ import tencent.im.oidb.oidb_sso
|
||||
|
||||
internal object TicketSvc: BaseSvc() {
|
||||
object SigType {
|
||||
const val WLOGIN_A2 = 64
|
||||
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_D2 = 262144
|
||||
const val WLOGIN_DA2 = 33554432
|
||||
@ -26,14 +29,17 @@ internal object TicketSvc: BaseSvc() {
|
||||
const val WLOGIN_PSKEY = 1048576
|
||||
const val WLOGIN_PT4Token = 134217728
|
||||
const val WLOGIN_QRPUSH = 67108864
|
||||
const val WLOGIN_RESERVED = 16
|
||||
const val WLOGIN_SID = 524288
|
||||
const val WLOGIN_SIG64 = 8192
|
||||
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_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 {
|
||||
@ -44,6 +50,14 @@ internal object TicketSvc: BaseSvc() {
|
||||
return app.longAccountUin
|
||||
}
|
||||
|
||||
fun getUid(): String {
|
||||
return app.currentUid.ifBlank { "u_" }
|
||||
}
|
||||
|
||||
fun getNickname(): String {
|
||||
return app.currentNickname
|
||||
}
|
||||
|
||||
fun getCookie(): String {
|
||||
val uin = getUin()
|
||||
val skey = getRealSkey(uin)
|
||||
|
@ -0,0 +1,96 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet.msg
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import moe.fuqiuluo.shamrock.helper.ParamsException
|
||||
import moe.fuqiuluo.shamrock.tools.asInt
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import protobuf.message.MessageElement
|
||||
import protobuf.message.element.FaceElement
|
||||
import protobuf.message.element.TextElement
|
||||
|
||||
|
||||
internal typealias IMessageMaker = 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,
|
||||
)
|
||||
|
||||
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 createJsonElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<MessageElement> {
|
||||
data.checkAndThrow("data")
|
||||
|
||||
val elem = MessageElement(
|
||||
json = protobuf.message.element.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)
|
||||
}
|
||||
}
|
||||
|
||||
operator fun get(type: String): IMessageMaker? = makerArray[type]
|
||||
|
||||
}
|
@ -11,6 +11,9 @@ import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
|
||||
import com.tencent.qqnt.kernel.nativeinterface.ArkElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FaceBubbleElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FaceElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.InlineKeyboardButton
|
||||
import com.tencent.qqnt.kernel.nativeinterface.InlineKeyboardElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.InlineKeyboardRow
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MarkdownElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceSupportSize
|
||||
@ -53,9 +56,11 @@ import moe.fuqiuluo.shamrock.helper.LogicException
|
||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||
import moe.fuqiuluo.shamrock.helper.MusicHelper
|
||||
import moe.fuqiuluo.shamrock.helper.ParamsException
|
||||
import moe.fuqiuluo.shamrock.tools.asBoolean
|
||||
import moe.fuqiuluo.shamrock.tools.asBooleanOrNull
|
||||
import moe.fuqiuluo.shamrock.tools.asInt
|
||||
import moe.fuqiuluo.shamrock.tools.asIntOrNull
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonArray
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asLong
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
@ -77,40 +82,99 @@ import kotlin.math.roundToInt
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
|
||||
internal typealias IMaker = suspend (Int, Long, String, JsonObject) -> Result<MsgElement>
|
||||
internal typealias IMsgMaker = suspend (Int, Long, String, JsonObject) -> Result<MsgElement>
|
||||
|
||||
internal object MessageMaker {
|
||||
private val makerArray = mutableMapOf(
|
||||
"text" to MessageMaker::createTextElem,
|
||||
"face" to MessageMaker::createFaceElem,
|
||||
"pic" to MessageMaker::createImageElem,
|
||||
"image" to MessageMaker::createImageElem,
|
||||
"voice" to MessageMaker::createRecordElem,
|
||||
"record" to MessageMaker::createRecordElem,
|
||||
"at" to MessageMaker::createAtElem,
|
||||
"video" to MessageMaker::createVideoElem,
|
||||
"markdown" to MessageMaker::createMarkdownElem,
|
||||
"dice" to MessageMaker::createDiceElem,
|
||||
"rps" to MessageMaker::createRpsElem,
|
||||
"poke" to MessageMaker::createPokeElem,
|
||||
"anonymous" to MessageMaker::createAnonymousElem,
|
||||
"share" to MessageMaker::createShareElem,
|
||||
"contact" to MessageMaker::createContactElem,
|
||||
"location" to MessageMaker::createLocationElem,
|
||||
"music" to MessageMaker::createMusicElem,
|
||||
"reply" to MessageMaker::createReplyElem,
|
||||
"touch" to MessageMaker::createTouchElem,
|
||||
"weather" to MessageMaker::createWeatherElem,
|
||||
"json" to MessageMaker::createJsonElem,
|
||||
"new_dice" to MessageMaker::createNewDiceElem,
|
||||
"new_rps" to MessageMaker::createNewRpsElem,
|
||||
"basketball" to MessageMaker::createBasketballElem,
|
||||
internal object MsgElementMaker {
|
||||
private val makerArray = hashMapOf(
|
||||
"text" to MsgElementMaker::createTextElem,
|
||||
"face" to MsgElementMaker::createFaceElem,
|
||||
"pic" to MsgElementMaker::createImageElem,
|
||||
"image" to MsgElementMaker::createImageElem,
|
||||
"voice" to MsgElementMaker::createRecordElem,
|
||||
"record" to MsgElementMaker::createRecordElem,
|
||||
"at" to MsgElementMaker::createAtElem,
|
||||
"video" to MsgElementMaker::createVideoElem,
|
||||
"markdown" to MsgElementMaker::createMarkdownElem,
|
||||
"dice" to MsgElementMaker::createDiceElem,
|
||||
"rps" to MsgElementMaker::createRpsElem,
|
||||
"poke" to MsgElementMaker::createPokeElem,
|
||||
"anonymous" to MsgElementMaker::createAnonymousElem,
|
||||
"share" to MsgElementMaker::createShareElem,
|
||||
"contact" to MsgElementMaker::createContactElem,
|
||||
"location" to MsgElementMaker::createLocationElem,
|
||||
"music" to MsgElementMaker::createMusicElem,
|
||||
"reply" to MsgElementMaker::createReplyElem,
|
||||
"touch" to MsgElementMaker::createTouchElem,
|
||||
"weather" to MsgElementMaker::createWeatherElem,
|
||||
"json" to MsgElementMaker::createJsonElem,
|
||||
"new_dice" to MsgElementMaker::createNewDiceElem,
|
||||
"new_rps" to MsgElementMaker::createNewRpsElem,
|
||||
"basketball" to MsgElementMaker::createBasketballElem,
|
||||
//"node" to MessageMaker::createNodeElem,
|
||||
//"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> {
|
||||
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")
|
||||
val faceId = data["id"].asInt
|
||||
val local = QQSysFaceUtil.convertToLocal(faceId)
|
||||
@ -143,21 +207,13 @@ internal object MessageMaker {
|
||||
// SendForwardMessage(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin(), data["content"].asJsonArray)
|
||||
//
|
||||
// }
|
||||
/**\
|
||||
* msgElement.setFaceElement(new FaceElement());
|
||||
* msgElement.getFaceElement().setFaceIndex(114);
|
||||
* msgElement.getFaceElement().setFaceText("/篮球");
|
||||
* msgElement.getFaceElement().setFaceType(3);
|
||||
* msgElement.getFaceElement().setPackId("1");
|
||||
* msgElement.getFaceElement().setStickerId("13");
|
||||
* 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> {
|
||||
|
||||
private suspend fun createBasketballElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<MsgElement> {
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEFACE
|
||||
val face = FaceElement()
|
||||
@ -168,14 +224,19 @@ internal object MessageMaker {
|
||||
face.stickerId = "13"
|
||||
face.sourceType = 1
|
||||
face.stickerType = 2
|
||||
face.resultId = Random.nextInt(1 .. 5).toString()
|
||||
face.resultId = Random.nextInt(1..5).toString()
|
||||
face.surpriseId = ""
|
||||
face.randomType = 1
|
||||
elem.faceElement = face
|
||||
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()
|
||||
elem.elementType = MsgConstant.KELEMTYPEFACE
|
||||
val face = FaceElement()
|
||||
@ -193,7 +254,12 @@ internal object MessageMaker {
|
||||
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()
|
||||
elem.elementType = MsgConstant.KELEMTYPEFACE
|
||||
val face = FaceElement()
|
||||
@ -311,7 +377,7 @@ internal object MessageMaker {
|
||||
LogCenter.log("无法获取被回复消息", Level.ERROR)
|
||||
}
|
||||
|
||||
if(data.containsKey("text")) {
|
||||
if (data.containsKey("text")) {
|
||||
data.checkAndThrow("qq", "time", "seq")
|
||||
reply.replayMsgSeq = data["seq"].asLong
|
||||
reply.sourceMsgText = data["text"].asString
|
||||
@ -330,21 +396,23 @@ internal object MessageMaker {
|
||||
): Result<MsgElement> {
|
||||
data.checkAndThrow("type")
|
||||
|
||||
when(val type = data["type"].asString) {
|
||||
when (val type = data["type"].asString) {
|
||||
"qq" -> {
|
||||
data.checkAndThrow("id")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
"163" -> {
|
||||
data.checkAndThrow("id")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
"custom" -> {
|
||||
data.checkAndThrow("url", "audio", "title")
|
||||
ArkMsgSvc.tryShareMusic(
|
||||
@ -359,13 +427,19 @@ internal object MessageMaker {
|
||||
data["audio"].asString
|
||||
)
|
||||
}
|
||||
|
||||
else -> LogCenter.log("不支持的音乐分享类型: $type", Level.ERROR)
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
val lat = data["lat"].asString.toDouble()
|
||||
@ -378,7 +452,12 @@ internal object MessageMaker {
|
||||
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")
|
||||
val type = data["type"].asStringOrNull ?: data["kind"].asStringOrNull
|
||||
val id = data["id"].asString
|
||||
@ -389,10 +468,12 @@ internal object MessageMaker {
|
||||
val ark = ArkElement(CardSvc.getSharePrivateArkMsg(id.toLong()), null, null)
|
||||
elem.arkElement = ark
|
||||
}
|
||||
|
||||
"group" -> {
|
||||
val ark = ArkElement(GroupSvc.getShareTroopArkMsg(id.toLong()), null, null)
|
||||
elem.arkElement = ark
|
||||
}
|
||||
|
||||
else -> throw IllegalParamsException("type")
|
||||
}
|
||||
|
||||
@ -401,7 +482,12 @@ internal object MessageMaker {
|
||||
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")
|
||||
|
||||
val url = data["url"].asString
|
||||
@ -466,11 +552,21 @@ internal object MessageMaker {
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
val elem = MsgElement()
|
||||
val face = FaceElement()
|
||||
@ -485,8 +581,8 @@ internal object MessageMaker {
|
||||
face.vaspokeName = ""
|
||||
face.vaspokeMinver = ""
|
||||
face.pokeStrength = (data["strength"].asIntOrNull ?: data["cnt"].asIntOrNull
|
||||
?: data["count"].asIntOrNull ?: data["time"].asIntOrNull ?: 0).also {
|
||||
if(it < 0 || it > 3) throw IllegalParamsException("strength")
|
||||
?: data["count"].asIntOrNull ?: data["time"].asIntOrNull ?: 0).also {
|
||||
if (it < 0 || it > 3) throw IllegalParamsException("strength")
|
||||
}
|
||||
face.msgType = 0
|
||||
face.faceBubbleCount = 0
|
||||
@ -497,7 +593,12 @@ internal object MessageMaker {
|
||||
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")
|
||||
|
||||
val serverId = data["id"].asInt
|
||||
@ -516,15 +617,15 @@ internal object MessageMaker {
|
||||
face.faceIndex = serverId
|
||||
face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId))
|
||||
if (serverId == 394) {
|
||||
face.stickerId = 40.toString()
|
||||
face.stickerId = "40"
|
||||
face.packId = "1"
|
||||
face.sourceType = 1
|
||||
face.stickerType = 3
|
||||
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) {
|
||||
face.imageType = 0
|
||||
face.stickerId = 30.toString()
|
||||
face.stickerId = "30"
|
||||
face.packId = "1"
|
||||
face.sourceType = 1
|
||||
face.stickerType = 1
|
||||
@ -538,7 +639,12 @@ internal object MessageMaker {
|
||||
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()
|
||||
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
|
||||
val market = MarketFaceElement(
|
||||
@ -553,7 +659,12 @@ internal object MessageMaker {
|
||||
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()
|
||||
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
|
||||
val market = MarketFaceElement(
|
||||
@ -568,16 +679,26 @@ internal object MessageMaker {
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun createMarkdownElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
|
||||
data.checkAndThrow("text")
|
||||
private suspend fun createMarkdownElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<MsgElement> {
|
||||
data.checkAndThrow("content")
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEMARKDOWN
|
||||
val markdown = MarkdownElement(data["text"].asString)
|
||||
val markdown = MarkdownElement(data["content"].asString)
|
||||
elem.markdownElement = markdown
|
||||
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")
|
||||
|
||||
val file = data["file"].asString.let {
|
||||
@ -613,7 +734,8 @@ internal object MessageMaker {
|
||||
)
|
||||
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
|
||||
originalPath
|
||||
) != file.length()) {
|
||||
) != file.length()
|
||||
) {
|
||||
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
||||
AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!)
|
||||
}
|
||||
@ -652,35 +774,43 @@ internal object MessageMaker {
|
||||
val qq = data["qq"].asString
|
||||
|
||||
val at = TextElement()
|
||||
when(qq) {
|
||||
when (qq) {
|
||||
"0", "all" -> {
|
||||
at.content = "@全体成员"
|
||||
at.atType = MsgConstant.ATTYPEALL
|
||||
at.atNtUid = "0"
|
||||
}
|
||||
|
||||
"online" -> {
|
||||
at.content = "@在线成员"
|
||||
at.atType = MsgConstant.ATTYPEONLINE
|
||||
at.atNtUid = "0"
|
||||
}
|
||||
|
||||
"admin" -> {
|
||||
at.content = "@管理员"
|
||||
at.atRoleId = 1
|
||||
at.atType = MsgConstant.ATTYPEROLE
|
||||
at.atNtUid = "0"
|
||||
}
|
||||
|
||||
else -> {
|
||||
val info = GroupSvc.getTroopMemberInfoByUin(peerId, qq, true).onFailure {
|
||||
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
|
||||
}.getOrNull()
|
||||
if (info != null) {
|
||||
at.content = "@${
|
||||
info.troopnick
|
||||
.ifNullOrEmpty(info.friendnick)
|
||||
.ifNullOrEmpty(qq)
|
||||
}"
|
||||
val name = data["name"].asStringOrNull
|
||||
if (name == null) {
|
||||
val info = GroupSvc.getTroopMemberInfoByUinV2(peerId, qq, true).onFailure {
|
||||
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
|
||||
}.getOrNull()
|
||||
if (info != null) {
|
||||
at.content = "@${
|
||||
info.troopnick
|
||||
.ifNullOrEmpty(info.friendnick)
|
||||
.ifNullOrEmpty(qq)
|
||||
}"
|
||||
} else {
|
||||
at.content = "@$qq"
|
||||
}
|
||||
} else {
|
||||
at.content = "@${data["name"].asStringOrNull.ifNullOrEmpty(qq)}"
|
||||
at.content = "@$name"
|
||||
}
|
||||
at.atType = MsgConstant.ATTYPEONE
|
||||
at.atNtUid = ContactHelper.getUidByUinAsync(qq.toLong())
|
||||
@ -693,7 +823,12 @@ internal object MessageMaker {
|
||||
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 {
|
||||
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "")
|
||||
.replace(" ", "")
|
||||
@ -721,11 +856,13 @@ internal object MessageMaker {
|
||||
ptt.duration = QRoute.api(IAIOPttApi::class.java)
|
||||
.getPttFileDuration(file.absolutePath)
|
||||
}
|
||||
|
||||
MediaType.Amr -> {
|
||||
LogCenter.log({ "Amr: $file" }, Level.DEBUG)
|
||||
ptt.duration = AudioUtils.getDurationSec(file)
|
||||
ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
|
||||
}
|
||||
|
||||
MediaType.Pcm -> {
|
||||
LogCenter.log({ "Pcm To Silk: $file" }, Level.DEBUG)
|
||||
val result = AudioUtils.pcmToSilk(file)
|
||||
@ -733,6 +870,7 @@ internal object MessageMaker {
|
||||
file = result.first
|
||||
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
||||
}
|
||||
|
||||
else -> {
|
||||
LogCenter.log({ "Audio To SILK: $file" }, Level.DEBUG)
|
||||
val result = AudioUtils.audioToSilk(file)
|
||||
@ -749,12 +887,13 @@ internal object MessageMaker {
|
||||
// QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
||||
//}
|
||||
|
||||
if(!(Transfer with when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> Troop(peerId)
|
||||
MsgConstant.KCHATTYPEC2C -> Private(peerId)
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Private(peerId)
|
||||
else -> error("Not supported chatType($chatType) for RecordMsg")
|
||||
} trans VoiceResource(file))) {
|
||||
if (!(Transfer with when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> Troop(peerId)
|
||||
MsgConstant.KCHATTYPEC2C -> Private(peerId)
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Private(peerId)
|
||||
else -> error("Not supported chatType($chatType) for RecordMsg")
|
||||
} trans VoiceResource(file))
|
||||
) {
|
||||
return Result.failure(RuntimeException("上传语音失败: $file"))
|
||||
}
|
||||
|
||||
@ -784,7 +923,12 @@ internal object MessageMaker {
|
||||
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 isFlash = data["flash"].asBooleanOrNull ?: false
|
||||
val filePath = data["file"].asStringOrNull
|
||||
@ -825,7 +969,8 @@ internal object MessageMaker {
|
||||
)
|
||||
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
|
||||
originalPath
|
||||
) != file.length()) {
|
||||
) != file.length()
|
||||
) {
|
||||
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
|
||||
RichMediaFilePathInfo(
|
||||
2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true
|
||||
@ -863,7 +1008,12 @@ internal object MessageMaker {
|
||||
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")
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPETEXT
|
||||
@ -879,5 +1029,5 @@ internal object MessageMaker {
|
||||
}
|
||||
}
|
||||
|
||||
operator fun get(type: String): IMaker? = makerArray[type]
|
||||
operator fun get(type: String): IMsgMaker? = makerArray[type]
|
||||
}
|
@ -58,6 +58,7 @@ internal object MessageConvert {
|
||||
//MsgConstant.KELEMTYPEMULTIFORWARD to XmlMultiMsgConverter,
|
||||
//MsgConstant.KELEMTYPESTRUCTLONGMSG to XmlLongMsgConverter,
|
||||
MsgConstant.KELEMTYPEFACEBUBBLE to BubbleFaceConverter,
|
||||
MsgConstant.KELEMTYPEINLINEKEYBOARD to InlineKeyboardConverter,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,10 @@ package moe.fuqiuluo.qqinterface.servlet.msg.convert
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonArray
|
||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
||||
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
@ -12,6 +16,7 @@ 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
|
||||
import moe.fuqiuluo.shamrock.tools.json
|
||||
|
||||
internal sealed class MessageElemConverter: IMessageConvert {
|
||||
@ -135,14 +140,19 @@ internal sealed class MessageElemConverter: IMessageConvert {
|
||||
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.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(md5)
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(md5)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(md5)
|
||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(originalUrl, md5)
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(originalUrl, md5)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(originalUrl, md5)
|
||||
else -> unknownChatType(chatType)
|
||||
},
|
||||
"subType" to image.picSubType,
|
||||
@ -201,7 +211,15 @@ internal sealed class MessageElemConverter: IMessageConvert {
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val video = element.videoElement
|
||||
val md5 = video.fileName.split(".")[0]
|
||||
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",
|
||||
@ -210,7 +228,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
|
||||
"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)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||
else -> unknownChatType(chatType)
|
||||
}
|
||||
).also {
|
||||
@ -487,6 +505,53 @@ internal sealed class MessageElemConverter: IMessageConvert {
|
||||
}
|
||||
}
|
||||
|
||||
data object InlineKeyboardConverter: MessageElemConverter() {
|
||||
override suspend fun convert(
|
||||
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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun unknownChatType(chatType: Int) {
|
||||
throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||
}
|
||||
|
@ -19,18 +19,35 @@ import moe.fuqiuluo.shamrock.tools.slice
|
||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
||||
import mqq.app.MobileQQ
|
||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
|
||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
|
||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
|
||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody
|
||||
import mqq.app.MobileQQ
|
||||
import tencent.im.cs.cmd0x346.cmd0x346
|
||||
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
|
||||
import tencent.im.oidb.cmd0xe37.cmd0xe37
|
||||
import tencent.im.oidb.oidb_sso
|
||||
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() {
|
||||
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 {
|
||||
val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, ProtoBuf.encodeToByteArray(
|
||||
Oidb0xfc2ReqBody(
|
||||
@ -142,24 +159,48 @@ internal object RichProtoSvc: BaseSvc() {
|
||||
}
|
||||
|
||||
fun getGroupPicDownUrl(
|
||||
md5: String
|
||||
originalUrl: String,
|
||||
md5: 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(
|
||||
originalUrl: String,
|
||||
md5: String
|
||||
): String {
|
||||
return "https://c2cpicdw.qpic.cn/offpic_new/0/123-0-${md5.uppercase()}/0?term=2"
|
||||
if (originalUrl.isNotEmpty()) {
|
||||
return "https://$C2C_PIC$originalUrl"
|
||||
}
|
||||
return "https://$C2C_PIC/offpic_new/0/123-0-${md5.uppercase()}/0?term=2"
|
||||
}
|
||||
|
||||
fun getGuildPicDownUrl(md5: String): String {
|
||||
return "https://gchat.qpic.cn/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
|
||||
fun getGuildPicDownUrl(
|
||||
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(
|
||||
peerId: String,
|
||||
md5Hex: String,
|
||||
md5: ByteArray,
|
||||
fileUUId: String
|
||||
): String {
|
||||
return suspendCancellableCoroutine {
|
||||
@ -175,7 +216,7 @@ internal object RichProtoSvc: BaseSvc() {
|
||||
downReq.troopUin = peerId
|
||||
downReq.clientType = 2
|
||||
downReq.fileId = fileUUId
|
||||
downReq.md5 = md5Hex.hex2ByteArray()
|
||||
downReq.md5 = md5
|
||||
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
|
||||
downReq.subBusiType = 0
|
||||
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
|
||||
@ -202,7 +243,7 @@ internal object RichProtoSvc: BaseSvc() {
|
||||
|
||||
suspend fun getGroupVideoDownUrl(
|
||||
peerId: String,
|
||||
md5Hex: String,
|
||||
md5: ByteArray,
|
||||
fileUUId: String
|
||||
): String {
|
||||
return suspendCancellableCoroutine {
|
||||
@ -218,7 +259,7 @@ internal object RichProtoSvc: BaseSvc() {
|
||||
downReq.troopUin = peerId
|
||||
downReq.clientType = 2
|
||||
downReq.fileId = fileUUId
|
||||
downReq.md5 = md5Hex.hex2ByteArray()
|
||||
downReq.md5 = md5
|
||||
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
|
||||
downReq.subBusiType = 0
|
||||
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
|
||||
@ -321,12 +362,4 @@ internal object RichProtoSvc: BaseSvc() {
|
||||
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 java.io.File
|
||||
import moe.fuqiuluo.qqinterface.servlet.transfile.ResourceType.*
|
||||
import moe.fuqiuluo.shamrock.helper.TransfileHelper
|
||||
|
||||
internal object Transfer: FileTransfer() {
|
||||
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
|
||||
@ -84,11 +85,14 @@ internal object Transfer: FileTransfer() {
|
||||
file: File,
|
||||
wait: Boolean = true
|
||||
): 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()
|
||||
picUpExtraInfo.mIsRaw = true
|
||||
picUpExtraInfo.mIsRaw = false
|
||||
picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY
|
||||
it.mPicSendSource = 8
|
||||
it.mExtraObj = picUpExtraInfo
|
||||
it.mIsPresend = true
|
||||
it.delayShowProgressTimeInMs = 2000
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,10 +101,13 @@ internal object Transfer: FileTransfer() {
|
||||
file: File,
|
||||
wait: Boolean = true
|
||||
): 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()
|
||||
picUpExtraInfo.mIsRaw = true
|
||||
//picUpExtraInfo.mIsRaw = !TransfileHelper.isGifFile(file)
|
||||
picUpExtraInfo.mIsRaw = false
|
||||
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
|
||||
it.mPicSendSource = 8
|
||||
it.delayShowProgressTimeInMs = 2000
|
||||
it.mExtraObj = picUpExtraInfo
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package moe.fuqiuluo.shamrock.helper
|
||||
|
||||
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")
|
||||
|
||||
|
@ -16,7 +16,8 @@ import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.MessageMaker
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.MessageElementMaker
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.MsgElementMaker
|
||||
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
||||
import moe.fuqiuluo.shamrock.helper.db.MessageMapping
|
||||
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
|
||||
@ -26,8 +27,8 @@ import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.tools.json
|
||||
import moe.fuqiuluo.shamrock.tools.jsonArray
|
||||
import protobuf.message.MessageElement
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.math.abs
|
||||
|
||||
internal object MessageHelper {
|
||||
@ -39,7 +40,7 @@ internal object MessageHelper {
|
||||
fromId: String = peerId
|
||||
): SendMsgResult {
|
||||
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) {
|
||||
error("消息合成失败,请查看日志或者检查输入。")
|
||||
} else if (it.second.isEmpty()) {
|
||||
@ -82,7 +83,7 @@ internal object MessageHelper {
|
||||
callback: IOperateCallback
|
||||
): Result<SendMsgResult> {
|
||||
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("消息合成失败,请查看日志或者检查输入。")
|
||||
}.second.filter {
|
||||
it.elementType != -1
|
||||
@ -166,7 +167,7 @@ internal object MessageHelper {
|
||||
fromId: String = peerId
|
||||
): SendMsgResult {
|
||||
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("消息合成失败,请查看日志或者检查输入。")
|
||||
}.second.filter {
|
||||
it.elementType != -1
|
||||
@ -224,7 +225,7 @@ internal object MessageHelper {
|
||||
fromId: String = peerId
|
||||
): SendMsgResult {
|
||||
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("消息合成失败,请查看日志或者检查输入。")
|
||||
}.second.filter {
|
||||
it.elementType != -1
|
||||
@ -233,7 +234,7 @@ internal object MessageHelper {
|
||||
return if (!message.isEmpty()) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
return suspendCancellableCoroutine {
|
||||
service.sendMsg(contact, uniseq.qqMsgId, msg) { code, why ->
|
||||
service.sendMsg(contact, uniseq.qqMsgId, msg) { _, _ ->
|
||||
it.resume(uniseq.copy(msgTime = System.currentTimeMillis()))
|
||||
}
|
||||
}
|
||||
@ -276,12 +277,41 @@ 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>()
|
||||
var hasActionMsg = false
|
||||
messageList.forEach {
|
||||
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) {
|
||||
try {
|
||||
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
|
||||
@ -353,6 +383,21 @@ internal object MessageHelper {
|
||||
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
|
||||
|
||||
fun decodeCQCode(code: String): JsonArray {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package moe.fuqiuluo.shamrock.helper
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.RandomAccessFile
|
||||
|
||||
internal object TransfileHelper {
|
||||
private val extensionMap = mapOf(
|
||||
@ -94,4 +96,15 @@ internal object TransfileHelper {
|
||||
val extension = name.substring(index)
|
||||
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)
|
||||
fun insert(mapping: MessageMapping)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
fun insertNotExist(mapping: MessageMapping)
|
||||
|
||||
@Query("UPDATE message_mapping_v2 SET msgSeq = :msgSeq WHERE msgHashId = :hash")
|
||||
fun updateMsgSeqByMsgHash(hash: Int, msgSeq: Int)
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
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 moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||
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.tools.EmptyJsonString
|
||||
import moe.fuqiuluo.symbols.OneBotHandler
|
||||
@ -17,10 +20,20 @@ internal object GetCookies: IActionHandler() {
|
||||
}
|
||||
|
||||
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 {
|
||||
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,5 +1,6 @@
|
||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
@ -18,8 +19,10 @@ internal object GetGProChannelList: IActionHandler() {
|
||||
return invoke(guildId.toULong(), refresh, echo = session.echo)
|
||||
}
|
||||
|
||||
operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String {
|
||||
val result = GProSvc.getChannelList(guildId, refresh)
|
||||
suspend operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String {
|
||||
val result = withTimeoutOrNull(5000) {
|
||||
GProSvc.getChannelList(guildId, refresh)
|
||||
} ?: return error("timeout", echo)
|
||||
result.onFailure {
|
||||
return error(it.message ?: "unable to fetch channel list", echo)
|
||||
}
|
||||
|
@ -59,6 +59,16 @@ internal object GetHistoryMsg: IActionHandler() {
|
||||
val msgList = ArrayList<MessageDetail>().apply {
|
||||
addAll(result.data!!.map { msg ->
|
||||
val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId)
|
||||
MessageHelper.saveMsgMappingNotExist(
|
||||
hash = msgHash,
|
||||
qqMsgId = msg.msgId,
|
||||
chatType = msg.chatType,
|
||||
subChatType = msg.chatType,
|
||||
peerId = peerId,
|
||||
msgSeq = msg.msgSeq.toInt(),
|
||||
time = msg.msgTime,
|
||||
subPeerId = msg.channelId ?: peerId
|
||||
)
|
||||
MessageDetail(
|
||||
time = msg.msgTime.toInt(),
|
||||
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
|
||||
|
@ -35,8 +35,8 @@ internal object GetImage: IActionHandler() {
|
||||
image.size,
|
||||
image.fileName,
|
||||
when(image.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(fileMd5)
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(fileMd5)
|
||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl("", fileMd5)
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl("", fileMd5)
|
||||
else -> error("Not supported chat type: ${image.chatType}, convertMsgElementsToMsgSegment::Pic")
|
||||
}
|
||||
), echo = echo)
|
||||
|
@ -52,11 +52,7 @@ internal object GetTroopMemberInfo : IActionHandler() {
|
||||
area = info.alias ?: "",
|
||||
lastSentTime = info.last_active_time,
|
||||
level = info.level,
|
||||
role = when {
|
||||
GroupSvc.getOwner(groupId).toString() == uin -> MemberRole.Owner
|
||||
uin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin
|
||||
else -> MemberRole.Member
|
||||
},
|
||||
role = GroupSvc.getMemberRole(groupId.toLong(), uin.toLong()),
|
||||
unfriendly = false,
|
||||
title = info.mUniqueTitle ?: "",
|
||||
titleExpireTime = info.mUniqueTitleExpire,
|
||||
|
@ -54,12 +54,13 @@ internal object GetTroopMemberList : IActionHandler() {
|
||||
area = info.alias ?: "",
|
||||
lastSentTime = info.last_active_time,
|
||||
level = info.level,
|
||||
role = when {
|
||||
role = GroupSvc.getMemberRole(groupId.toLong(), info.memberuin.toLong())
|
||||
/*when {
|
||||
GroupSvc.getOwner(groupId)
|
||||
.toString() == info.memberuin -> MemberRole.Owner
|
||||
info.memberuin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin
|
||||
else -> MemberRole.Member
|
||||
},
|
||||
}*/,
|
||||
unfriendly = false,
|
||||
title = info.mUniqueTitle ?: "",
|
||||
titleExpireTime = info.mUniqueTitleExpire,
|
||||
|
@ -2,6 +2,7 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
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.IActionHandler
|
||||
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 {
|
||||
if (!GroupSvc.isAdmin(groupId)) {
|
||||
if (!GroupSvc.isAdmin(groupId) && userId != TicketSvc.getUin()) {
|
||||
return logic("you are not admin", echo)
|
||||
}
|
||||
return if(GroupSvc.modifyGroupMemberCard(groupId.toLong(), userId.toLong(), card))
|
||||
|
@ -1,7 +1,6 @@
|
||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo
|
||||
import kotlinx.serialization.json.*
|
||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||
@ -14,8 +13,11 @@ import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||
import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult
|
||||
import moe.fuqiuluo.shamrock.tools.*
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
||||
import moe.fuqiuluo.symbols.OneBotHandler
|
||||
import protobuf.message.*
|
||||
import protobuf.message.longmsg.PushMsgBody
|
||||
import java.util.*
|
||||
import kotlin.random.Random
|
||||
|
||||
@OneBotHandler("send_forward_msg")
|
||||
internal object SendForwardMessage : IActionHandler() {
|
||||
@ -37,14 +39,26 @@ internal object SendForwardMessage : IActionHandler() {
|
||||
return noParam("detail_type/message_type", session.echo)
|
||||
}
|
||||
}
|
||||
val peerId = when(chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam("group_id", session.echo)
|
||||
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id") ?: return noParam("user_id", session.echo)
|
||||
val peerId = when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam(
|
||||
"group_id",
|
||||
session.echo
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id")
|
||||
?: return noParam("user_id", session.echo)
|
||||
|
||||
else -> error("unknown chat type: $chatType")
|
||||
}
|
||||
val fromId = when(chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("group_id") ?: return noParam("group_id", session.echo)
|
||||
MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam("user_id", session.echo)
|
||||
val fromId = when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("group_id")
|
||||
?: return noParam("group_id", session.echo)
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam(
|
||||
"user_id",
|
||||
session.echo
|
||||
)
|
||||
|
||||
else -> error("unknown chat type: $chatType")
|
||||
}
|
||||
return if (session.isArray("messages")) {
|
||||
@ -68,103 +82,184 @@ internal object SendForwardMessage : IActionHandler() {
|
||||
echo: JsonElement = EmptyJsonString
|
||||
): String {
|
||||
kotlin.runCatching {
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
val sessionService = kernelService.wrapperSession
|
||||
val msgService = sessionService.msgService
|
||||
val selfUin = TicketSvc.getUin()
|
||||
var uid: String? = null
|
||||
var groupUin: String? = null
|
||||
|
||||
val multiNodes = messages.map {
|
||||
if (it.asJsonObject["type"].asStringOrNull != "node") {
|
||||
LogCenter.log("包含非node类型节点", Level.WARN)
|
||||
return@map null
|
||||
}
|
||||
if (it.asJsonObject["data"] !is JsonObject) {
|
||||
LogCenter.log("data字段错误", Level.WARN)
|
||||
return@map null
|
||||
}
|
||||
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)
|
||||
var i = -1
|
||||
val desc = MutableList(messages.size) { "" }
|
||||
|
||||
val msgs = messages.map { msg ->
|
||||
val data = msg.asJsonObject["data"].asJsonObject
|
||||
if (data.containsKey("id")) {
|
||||
val record = MsgSvc.getMsg(data["id"].asInt).getOrElse {
|
||||
LogCenter.log("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it", Level.WARN)
|
||||
return@map null
|
||||
}
|
||||
}.let { node ->
|
||||
val content = node.second.map { msg ->
|
||||
when (msg.asJsonObject["type"].asStringOrNull ?: "text") {
|
||||
"at" -> {
|
||||
buildJsonObject {
|
||||
put("type", "text")
|
||||
putJsonObject("data") {
|
||||
put(
|
||||
"text", "@${
|
||||
msg.asJsonObject["data"].asJsonObject["name"].asStringOrNull.ifNullOrEmpty(
|
||||
msg.asJsonObject["data"].asJsonObject["qq"].asString
|
||||
)
|
||||
}"
|
||||
)
|
||||
uid = record.peerUid
|
||||
if (record.chatType == MsgConstant.KCHATTYPEGROUP) groupUin = record.peerUin.toString()
|
||||
PushMsgBody(
|
||||
head = MessageHead(
|
||||
peerUid = record.senderUid,
|
||||
groupInfo = if (record.chatType == MsgConstant.KCHATTYPEGROUP) GroupInfo(
|
||||
groupCode = record.peerUin.toULong(),
|
||||
memberCard = record.sendMemberName,
|
||||
u1 = 2
|
||||
) else null
|
||||
),
|
||||
content = MessageContent(
|
||||
msgType = record.msgType,
|
||||
msgViaRandom = record.msgId,
|
||||
msgSeq = record.msgSeq,
|
||||
msgTime = record.msgTime,
|
||||
u2 = 1,
|
||||
u6 = 0,
|
||||
u7 = 0,
|
||||
forwardHead = ForwardHead(
|
||||
u1 = 0,
|
||||
u2 = 0,
|
||||
u3 = if (record.chatType == MsgConstant.KCHATTYPEGROUP) 0 else 2,
|
||||
u4 = "",
|
||||
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.peerName + ": "
|
||||
}.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(
|
||||
peerUid = data["uid"]?.asString ?: TicketSvc.getUid()
|
||||
),
|
||||
content = MessageContent(
|
||||
msgType = 529,
|
||||
msgViaRandom = 4,
|
||||
msgSeq = data["seq"]?.asLong ?: 0,
|
||||
msgTime = System.currentTimeMillis() / 1000,
|
||||
u2 = 1,
|
||||
u6 = 0,
|
||||
u7 = 0,
|
||||
forwardHead = ForwardHead(
|
||||
u1 = 0,
|
||||
u2 = 0,
|
||||
u3 = 2,
|
||||
u4 = "",
|
||||
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] += "[合并转发消息]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"voice" -> {
|
||||
buildJsonObject {
|
||||
put("type", "text")
|
||||
putJsonObject("data") {
|
||||
put("text", "[语音]")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"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
|
||||
).also {
|
||||
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
||||
}.second
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
LogCenter.log("消息节点缺少id或content字段", Level.WARN)
|
||||
null
|
||||
}
|
||||
}.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)
|
||||
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply {
|
||||
multiNodes.forEach { add(MultiMsgInfo(it.first, it.second)) }
|
||||
}.also { it.reverse() }, from, to, MsgSvc.MessageCallback(peerId, uniseq.msgHashId))
|
||||
val resid = MsgSvc.sendMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs)
|
||||
.getOrElse { return logic(it.message ?: "", echo) }
|
||||
val uniseq = UUID.randomUUID().toString().uppercase()
|
||||
|
||||
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() + "\n",
|
||||
"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(
|
||||
ForwardMessageResult(
|
||||
msgId = uniseq.msgHashId,
|
||||
forwardId = ""
|
||||
msgId = result.msgHashId,
|
||||
forwardId = resid
|
||||
), echo = echo
|
||||
)
|
||||
}.onFailure {
|
||||
|
@ -120,18 +120,14 @@ internal object SendMessage: IActionHandler() {
|
||||
//if (!ContactHelper.checkContactAvailable(chatType, peerId)) {
|
||||
// return logic("contact is not found", echo = echo)
|
||||
//}
|
||||
val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt)
|
||||
if (result.isFailure) {
|
||||
return logic(result.exceptionOrNull()?.message ?: "", echo)
|
||||
}
|
||||
val sendMsgResult = result.getOrThrow()
|
||||
if (sendMsgResult.msgHashId <= 0) {
|
||||
val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt).getOrElse { return logic(it.message ?: "", echo) }
|
||||
if (result.msgHashId <= 0) {
|
||||
return logic("send message failed", echo = echo)
|
||||
}
|
||||
recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) }
|
||||
recallDuration?.let { autoRecall(result.msgHashId, it) }
|
||||
return ok(MessageResult(
|
||||
msgId = sendMsgResult.msgHashId,
|
||||
time = (sendMsgResult.msgTime * 0.001).toLong()
|
||||
msgId = result.msgHashId,
|
||||
time = (result.msgTime * 0.001).toLong()
|
||||
), echo)
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,7 @@ internal object UploadGroupFile : IActionHandler() {
|
||||
|
||||
// 根据文件大小调整超时时间
|
||||
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 contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId)
|
||||
suspendCancellableCoroutine<FileTransNotifyInfo?> {
|
||||
|
@ -5,6 +5,7 @@ import com.tencent.mobileqq.transfile.TransferRequest
|
||||
import com.tencent.mobileqq.transfile.api.ITransFileController
|
||||
import io.ktor.server.routing.Routing
|
||||
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.tools.fetchPost
|
||||
import moe.fuqiuluo.shamrock.tools.respond
|
||||
@ -15,7 +16,7 @@ import kotlin.random.Random
|
||||
import kotlin.random.nextLong
|
||||
|
||||
fun Routing.registerBDH() {
|
||||
post("/upload_group_image") {
|
||||
if(ShamrockConfig.isDev()) post("/upload_group_image") {
|
||||
val troop = fetchPost("troop")
|
||||
val picBytes = Base64.decode(fetchPost("pic"), Base64.DEFAULT)
|
||||
val md5Str = MD5.getMd5Hex(picBytes)
|
||||
@ -46,5 +47,4 @@ fun Routing.registerBDH() {
|
||||
.transferAsync(transferRequest)
|
||||
respond(isOk = true, Status.Ok, "$md5Str.jpg")
|
||||
}
|
||||
|
||||
}
|
@ -5,7 +5,9 @@ import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.response.respondText
|
||||
import io.ktor.server.routing.Routing
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
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.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") {
|
||||
val uin = fetchOrThrow("uin")
|
||||
val ticket = when(val id = fetchOrThrow("id").toInt()) {
|
||||
32 -> TicketSvc.getStWeb(uin)
|
||||
else -> {
|
||||
respond(true, Status.Ok, data = TicketSvc.getTicket(uin, id)?.let {
|
||||
mapOf(
|
||||
"sig" to (it._sig?.toHexString() ?: "null"),
|
||||
"key" to (it._sig_key?.toHexString() ?: "null")
|
||||
).json.asJsonObject
|
||||
} ?: EmptyJsonObject)
|
||||
respond(true, Status.Ok, data = getTicket(uin, id))
|
||||
return@getOrPost
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
internal object HttpService: HttpTransmitServlet() {
|
||||
private val jobList = arrayListOf<Job>()
|
||||
private val subscribes = arrayListOf<Job>()
|
||||
|
||||
override fun submitFlowJob(job: Job) {
|
||||
// HTTP 回调不会触发断连,无需释放之前的JOB
|
||||
jobList.add(job)
|
||||
override fun subscribe(job: Job) {
|
||||
subscribes.add(job)
|
||||
}
|
||||
|
||||
override fun cancelFlowJobs() {
|
||||
jobList.removeIf {
|
||||
override fun unsubscribe() {
|
||||
subscribes.removeIf {
|
||||
it.cancel()
|
||||
return@removeIf true
|
||||
}
|
||||
}
|
||||
|
||||
override fun initTransmitter() {
|
||||
if (jobList.isNotEmpty()) return
|
||||
submitFlowJob(GlobalScope.launch {
|
||||
override fun init() {
|
||||
if (subscribes.isNotEmpty()) return
|
||||
subscribe(GlobalScope.launch {
|
||||
GlobalEventTransmitter.onMessageEvent { (record, event) ->
|
||||
val respond = pushTo(event) ?: return@onMessageEvent
|
||||
handleQuicklyReply(record, event.messageId, respond.bodyAsText())
|
||||
}
|
||||
})
|
||||
submitFlowJob(GlobalScope.launch {
|
||||
subscribe(GlobalScope.launch {
|
||||
GlobalEventTransmitter.onNoticeEvent { event ->
|
||||
pushTo(event)
|
||||
}
|
||||
|
||||
})
|
||||
submitFlowJob(GlobalScope.launch {
|
||||
subscribe(GlobalScope.launch {
|
||||
GlobalEventTransmitter.onRequestEvent {
|
||||
pushTo(it)
|
||||
}
|
||||
|
@ -16,28 +16,28 @@ internal class WebSocketClientService(
|
||||
heartbeatInterval: Long,
|
||||
wsHeaders: Map<String, String>
|
||||
) : WebSocketClientServlet(address, heartbeatInterval, wsHeaders) {
|
||||
private val eventJobList = mutableSetOf<Job>()
|
||||
private val subscribes = mutableSetOf<Job>()
|
||||
|
||||
init {
|
||||
startHeartbeatTimer()
|
||||
}
|
||||
|
||||
override fun submitFlowJob(job: Job) {
|
||||
eventJobList.add(job)
|
||||
override fun subscribe(job: Job) {
|
||||
subscribes.add(job)
|
||||
}
|
||||
|
||||
override fun initTransmitter() {
|
||||
submitFlowJob(GlobalScope.launch {
|
||||
override fun init() {
|
||||
subscribe(GlobalScope.launch {
|
||||
GlobalEventTransmitter.onMessageEvent { (_, event) ->
|
||||
pushTo(event)
|
||||
}
|
||||
})
|
||||
submitFlowJob(GlobalScope.launch {
|
||||
subscribe(GlobalScope.launch {
|
||||
GlobalEventTransmitter.onNoticeEvent { event ->
|
||||
pushTo(event)
|
||||
}
|
||||
})
|
||||
submitFlowJob(GlobalScope.launch {
|
||||
subscribe(GlobalScope.launch {
|
||||
GlobalEventTransmitter.onRequestEvent { event ->
|
||||
pushTo(event)
|
||||
}
|
||||
@ -45,8 +45,8 @@ internal class WebSocketClientService(
|
||||
LogCenter.log("WebSocketClientService: 初始化服务", Level.WARN)
|
||||
}
|
||||
|
||||
override fun cancelFlowJobs() {
|
||||
eventJobList.removeIf { job ->
|
||||
override fun unsubscribe() {
|
||||
subscribes.removeIf { job ->
|
||||
job.cancel()
|
||||
return@removeIf true
|
||||
}
|
||||
|
@ -26,24 +26,24 @@ internal class WebSocketService(
|
||||
port: Int,
|
||||
heartbeatInterval: Long,
|
||||
): WebSocketTransmitServlet(host, port, heartbeatInterval) {
|
||||
private val eventJobList = mutableSetOf<Job>()
|
||||
private val subscribes = mutableSetOf<Job>()
|
||||
|
||||
override fun submitFlowJob(job: Job) {
|
||||
eventJobList.add(job)
|
||||
override fun subscribe(job: Job) {
|
||||
subscribes.add(job)
|
||||
}
|
||||
|
||||
override fun initTransmitter() {
|
||||
submitFlowJob(GlobalScope.launch {
|
||||
override fun init() {
|
||||
subscribe(GlobalScope.launch {
|
||||
GlobalEventTransmitter.onMessageEvent { (_, event) ->
|
||||
pushTo(event)
|
||||
}
|
||||
})
|
||||
submitFlowJob(GlobalScope.launch {
|
||||
subscribe(GlobalScope.launch {
|
||||
GlobalEventTransmitter.onNoticeEvent { event ->
|
||||
pushTo(event)
|
||||
}
|
||||
})
|
||||
submitFlowJob(GlobalScope.launch {
|
||||
subscribe(GlobalScope.launch {
|
||||
GlobalEventTransmitter.onRequestEvent { event ->
|
||||
pushTo(event)
|
||||
}
|
||||
@ -51,8 +51,8 @@ internal class WebSocketService(
|
||||
LogCenter.log("WebSocketService: 初始化服务", Level.WARN)
|
||||
}
|
||||
|
||||
override fun cancelFlowJobs() {
|
||||
eventJobList.removeIf { job ->
|
||||
override fun unsubscribe() {
|
||||
subscribes.removeIf { job ->
|
||||
job.cancel()
|
||||
return@removeIf true
|
||||
}
|
||||
@ -60,8 +60,10 @@ internal class WebSocketService(
|
||||
}
|
||||
|
||||
override fun onOpen(conn: WebSocket, handshake: ClientHandshake) {
|
||||
val token = ShamrockConfig.getActiveWebSocketConfig()?.token ?: ShamrockConfig.getToken()
|
||||
if (token.isNotBlank()) {
|
||||
val token = ShamrockConfig.getActiveWebSocketConfig()?.tokens
|
||||
?: ShamrockConfig.getActiveWebSocketConfig()?.token?.split(",", "|", ",")
|
||||
?: listOf(ShamrockConfig.getToken())
|
||||
if (token.isNotEmpty()) {
|
||||
var accessToken = handshake.getFieldValue("access_token")
|
||||
.ifNullOrEmpty(handshake.getFieldValue("ticket"))
|
||||
.ifNullOrEmpty(handshake.getFieldValue("Authorization"))
|
||||
@ -69,8 +71,7 @@ internal class WebSocketService(
|
||||
if (accessToken.startsWith("Bearer ", ignoreCase = true)) {
|
||||
accessToken = accessToken.substring(7)
|
||||
}
|
||||
val tokenList = token.split(",", "|", ",")
|
||||
if (!tokenList.contains(accessToken)) {
|
||||
if (!token.contains(accessToken)) {
|
||||
conn.close()
|
||||
LogCenter.log({ "WSServer连接错误(${conn.remoteSocketAddress.address.hostAddress}:${conn.remoteSocketAddress.port}) 没有提供正确的token, $accessToken。" }, Level.ERROR)
|
||||
return
|
||||
|
@ -1,24 +1,20 @@
|
||||
package moe.fuqiuluo.shamrock.remote.service.api
|
||||
|
||||
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 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 oicq.wlogin_sdk.tools.MD5
|
||||
|
||||
internal interface BaseTransmitServlet {
|
||||
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
|
||||
get() = AppRuntimeFetcher.appRuntime as QQAppInterface
|
||||
|
@ -1,9 +1,15 @@
|
||||
@file:OptIn(DelicateCoroutinesApi::class)
|
||||
|
||||
package moe.fuqiuluo.shamrock.remote.service.api
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.CardSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
||||
@ -48,7 +54,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
||||
private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message)
|
||||
|
||||
/**
|
||||
* 消息 手淫器
|
||||
* 消息
|
||||
*/
|
||||
object MessageTransmitter {
|
||||
/**
|
||||
@ -86,11 +92,11 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
||||
.ifEmpty { record.sendMemberName }
|
||||
.ifEmpty { record.peerName },
|
||||
card = record.sendMemberName,
|
||||
role = when (record.senderUin) {
|
||||
role = GroupSvc.getMemberRole(record.peerUin, record.senderUin)/*when (record.senderUin) {
|
||||
GroupSvc.getOwner(record.peerUin.toString()) -> MemberRole.Owner
|
||||
in GroupSvc.getAdminList(record.peerUin.toString()) -> MemberRole.Admin
|
||||
else -> MemberRole.Member
|
||||
},
|
||||
}*/,
|
||||
title = "",
|
||||
level = "",
|
||||
)
|
||||
@ -108,7 +114,9 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
||||
rawMsg: String,
|
||||
msgHash: Int,
|
||||
postType: PostType,
|
||||
tempSource: MessageTempSource = MessageTempSource.Unknown
|
||||
tempSource: MessageTempSource = MessageTempSource.Unknown,
|
||||
groupId: Long = Long.MIN_VALUE,
|
||||
fromNick: String? = null
|
||||
): Boolean {
|
||||
val botUin = app.longAccountUin
|
||||
var nickName = record.sendNickName
|
||||
@ -142,7 +150,9 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
||||
title = "",
|
||||
level = "",
|
||||
),
|
||||
tmpSource = tempSource.id
|
||||
tmpSource = tempSource.id,
|
||||
groupId = groupId,
|
||||
fromNickName = fromNick
|
||||
)
|
||||
)
|
||||
return true
|
||||
@ -172,6 +182,8 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
||||
postType = postType,
|
||||
messageType = MsgType.Guild,
|
||||
subType = MsgSubType.Channel,
|
||||
guildId = record.guildId,
|
||||
channelId = record.channelId,
|
||||
messageId = msgHash,
|
||||
targetId = record.peerUin,
|
||||
peerId = botUin,
|
||||
@ -186,7 +198,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
||||
userId = record.senderUid.toLong(),
|
||||
nickname = nickName,
|
||||
card = record.sendMemberName,
|
||||
role = MemberRole.Member,
|
||||
role = MemberRole.Member, // TODO(GUILD ROLE)
|
||||
title = record.sendNickName,
|
||||
level = record.roleId.toString(),
|
||||
tinyId = record.senderUid
|
||||
@ -554,19 +566,29 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
||||
|
||||
@ShamrockDsl
|
||||
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) {
|
||||
messageEventFlow.collect(collector)
|
||||
messageEventFlow.collect {
|
||||
GlobalScope.launch {
|
||||
collector.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ShamrockDsl
|
||||
suspend inline fun onNoticeEvent(collector: FlowCollector<NoticeEvent>) {
|
||||
noticeEventFlow
|
||||
.collect(collector)
|
||||
noticeEventFlow.collect {
|
||||
GlobalScope.launch {
|
||||
collector.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ShamrockDsl
|
||||
suspend inline fun onRequestEvent(collector: FlowCollector<RequestEvent>) {
|
||||
requestEventFlow
|
||||
.collect(collector)
|
||||
requestEventFlow.collect {
|
||||
GlobalScope.launch {
|
||||
collector.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@ import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
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.tools.GlobalClient
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
@ -22,12 +21,12 @@ import java.net.SocketException
|
||||
internal abstract class HttpTransmitServlet : BaseTransmitServlet {
|
||||
override val address: String by lazy { ShamrockConfig.getWebHookAddress() }
|
||||
|
||||
override fun allowTransmit(): Boolean {
|
||||
override fun transmitAccess(): Boolean {
|
||||
return ShamrockConfig.allowWebHook()
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> pushTo(body: T): HttpResponse? {
|
||||
if (!allowTransmit()) return null
|
||||
if (!transmitAccess()) return null
|
||||
try {
|
||||
if (address.startsWith("http://") || address.startsWith("https://")) {
|
||||
val response = GlobalClient.post(address) {
|
||||
|
@ -45,7 +45,7 @@ internal abstract class WebSocketClientServlet(
|
||||
private var firstOpen = true
|
||||
private val sendLock = Mutex()
|
||||
|
||||
override fun allowTransmit(): Boolean {
|
||||
override fun transmitAccess(): Boolean {
|
||||
return ShamrockConfig.openWebSocketClient()
|
||||
}
|
||||
|
||||
@ -90,9 +90,9 @@ internal abstract class WebSocketClientServlet(
|
||||
if (firstOpen) {
|
||||
firstOpen = false
|
||||
} else {
|
||||
cancelFlowJobs()
|
||||
unsubscribe()
|
||||
}
|
||||
initTransmitter()
|
||||
init()
|
||||
}
|
||||
|
||||
override fun onClose(code: Int, reason: String?, remote: Boolean) {
|
||||
@ -105,18 +105,18 @@ internal abstract class WebSocketClientServlet(
|
||||
}
|
||||
}
|
||||
LogCenter.log("WebSocketClient onClose: $code, $reason, $remote")
|
||||
cancelFlowJobs()
|
||||
unsubscribe()
|
||||
connectedClients.remove(url)
|
||||
}
|
||||
|
||||
override fun onError(ex: Exception?) {
|
||||
LogCenter.log("WebSocketClient onError: ${ex?.message}")
|
||||
cancelFlowJobs()
|
||||
unsubscribe()
|
||||
connectedClients.remove(url)
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> pushTo(body: T) {
|
||||
if (!allowTransmit() || isClosed || isClosing) return
|
||||
if (!transmitAccess() || isClosed || isClosing) return
|
||||
try {
|
||||
sendLock.withLock {
|
||||
send(GlobalJson.encodeToString(body))
|
||||
|
@ -41,10 +41,14 @@ internal abstract class WebSocketTransmitServlet(
|
||||
private val sendLock = Mutex()
|
||||
protected val eventReceivers: MutableList<WebSocket> = Collections.synchronizedList(mutableListOf<WebSocket>())
|
||||
|
||||
init {
|
||||
connectionLostTimeout = 0
|
||||
}
|
||||
|
||||
override val address: String
|
||||
get() = "-"
|
||||
|
||||
override fun allowTransmit(): Boolean {
|
||||
override fun transmitAccess(): Boolean {
|
||||
return ShamrockConfig.openWebSocket()
|
||||
}
|
||||
|
||||
@ -125,16 +129,16 @@ internal abstract class WebSocketTransmitServlet(
|
||||
|
||||
override fun onError(conn: WebSocket, ex: Exception?) {
|
||||
LogCenter.log("WSServer Error: " + ex?.stackTraceToString(), Level.ERROR)
|
||||
cancelFlowJobs()
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
LogCenter.log("WSServer start running on ws://${getAddress()}!")
|
||||
initTransmitter()
|
||||
init()
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> pushTo(body: T) {
|
||||
if(!allowTransmit()) return
|
||||
if(!transmitAccess()) return
|
||||
try {
|
||||
sendLock.withLock {
|
||||
broadcastTextEvent(GlobalJson.encodeToString(body))
|
||||
|
@ -18,6 +18,7 @@ data class ConnectionConfig(
|
||||
@SerialName("address") val address: String? = null,
|
||||
@SerialName("port") var port: Int? = null,
|
||||
@SerialName("token") val token: String? = null,
|
||||
@SerialName("tokens") val tokens: List<String>? = 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("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口
|
||||
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.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
|
||||
@ -126,6 +127,10 @@ internal object ShamrockConfig {
|
||||
return mmkv.getBoolean("enable_self_msg", false)
|
||||
}
|
||||
|
||||
fun forbidUselessProcess(): Boolean {
|
||||
return mmkv.getBoolean("forbid_useless_process", false)
|
||||
}
|
||||
|
||||
fun openWebSocketClient(): Boolean {
|
||||
return mmkv.getBoolean("ws_client", false)
|
||||
}
|
||||
|
@ -6,5 +6,12 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
internal data class Credentials(
|
||||
@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
|
||||
)
|
@ -52,8 +52,10 @@ internal data class MessageEvent (
|
||||
@SerialName("message_type") val messageType: MsgType,
|
||||
@SerialName("sub_type") val subType: MsgSubType,
|
||||
@SerialName("message_id") val messageId: Int,
|
||||
@SerialName("group_id") val groupId: Long = 0,
|
||||
@SerialName("target_id") val targetId: Long = 0,
|
||||
@SerialName("group_id") val groupId: Long = Long.MIN_VALUE,
|
||||
@SerialName("guild_id") val guildId: String? = null,
|
||||
@SerialName("channel_id") val channelId: String? = null,
|
||||
@SerialName("target_id") val targetId: Long = Long.MIN_VALUE,
|
||||
@SerialName("peer_id") val peerId: Long,
|
||||
@SerialName("user_id") val userId: Long,
|
||||
@SerialName("anonymous") val anonymous: Anonymous? = null,
|
||||
@ -61,7 +63,8 @@ internal data class MessageEvent (
|
||||
@SerialName("raw_message") val rawMessage: String,
|
||||
@SerialName("font") val font: Int,
|
||||
@SerialName("sender") val sender: Sender,
|
||||
@SerialName("temp_source") val tmpSource: Int = -1
|
||||
@SerialName("temp_source") val tmpSource: Int = Int.MIN_VALUE,
|
||||
@SerialName("from_nick") val fromNickName: String? = null
|
||||
)
|
||||
|
||||
enum class MessageTempSource(val id: Int) {
|
||||
@ -86,7 +89,9 @@ internal data class Anonymous(
|
||||
internal enum class MemberRole {
|
||||
@SerialName("owner") Owner,
|
||||
@SerialName("admin") Admin,
|
||||
@SerialName("member") Member
|
||||
@SerialName("member") Member,
|
||||
@SerialName("stranger") Stranger,
|
||||
@SerialName("unknown") Unknown
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
@ -7,6 +7,7 @@ import com.tencent.qqnt.kernel.nativeinterface.*
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toCQCode
|
||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
||||
@ -44,11 +45,11 @@ internal object AioListener : IKernelMsgListener {
|
||||
it.value(record)
|
||||
messageLessListenerMap.remove(it.key)
|
||||
}
|
||||
if (record.msgSeq < 0) return
|
||||
if (record.msgSeq < 0) return
|
||||
|
||||
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
|
||||
|
||||
val peerId = when(record.chatType) {
|
||||
val peerId = when (record.chatType) {
|
||||
MsgConstant.KCHATTYPEGUILD -> record.guildId
|
||||
else -> record.peerUin.toString()
|
||||
}
|
||||
@ -87,12 +88,14 @@ internal object AioListener : IKernelMsgListener {
|
||||
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
|
||||
}
|
||||
|
||||
if(!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(
|
||||
if (!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(
|
||||
record, record.elements, rawMsg, msgHash, postType
|
||||
)) {
|
||||
)
|
||||
) {
|
||||
LogCenter.log("群消息推送失败 -> 推送目标可能不存在", Level.WARN)
|
||||
}
|
||||
}
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> {
|
||||
LogCenter.log("私聊消息(private = ${record.senderUin}, id = [$msgHash | ${record.msgId} | ${record.msgSeq}], msg = $rawMsg)")
|
||||
ShamrockConfig.getPrivateRule()?.let { rule ->
|
||||
@ -100,9 +103,10 @@ internal object AioListener : IKernelMsgListener {
|
||||
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
|
||||
}
|
||||
|
||||
if(!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
|
||||
if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
|
||||
record, record.elements, rawMsg, msgHash, postType
|
||||
)) {
|
||||
)
|
||||
) {
|
||||
LogCenter.log("私聊消息推送失败 -> MessageTransmitter", Level.WARN)
|
||||
}
|
||||
}
|
||||
@ -110,24 +114,40 @@ internal object AioListener : IKernelMsgListener {
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
|
||||
if (!ShamrockConfig.allowTempSession()) return
|
||||
|
||||
LogCenter.log("私聊临时消息(private = ${record.senderUin}, id = $msgHash, msg = $rawMsg)")
|
||||
ShamrockConfig.getPrivateRule()?.let { rule ->
|
||||
if (!rule.black.isNullOrEmpty() && rule.black.contains(record.senderUin)) return
|
||||
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
|
||||
}
|
||||
|
||||
if(!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
|
||||
record, record.elements, rawMsg, msgHash, tempSource = MessageTempSource.Group, postType = postType
|
||||
)) {
|
||||
var groupCode = 0L
|
||||
var fromNick = ""
|
||||
MsgSvc.getTempChatInfo(record.chatType, record.senderUid).onSuccess {
|
||||
groupCode = it.groupCode.toLong()
|
||||
fromNick = it.fromNick
|
||||
}
|
||||
|
||||
LogCenter.log("私聊临时消息(private = ${record.senderUin}, groupId=$groupCode, id = $msgHash, msg = $rawMsg)")
|
||||
|
||||
if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
|
||||
record,
|
||||
record.elements,
|
||||
rawMsg,
|
||||
msgHash,
|
||||
tempSource = MessageTempSource.Group,
|
||||
postType = postType,
|
||||
groupId = groupCode,
|
||||
fromNick = fromNick
|
||||
)
|
||||
) {
|
||||
LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
|
||||
}
|
||||
}
|
||||
|
||||
MsgConstant.KCHATTYPEGUILD -> {
|
||||
LogCenter.log("频道消息(guildId = ${record.guildId}, sender=${record.senderUid}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)")
|
||||
if(!GlobalEventTransmitter.MessageTransmitter
|
||||
.transGuildMessage(record, record.elements, rawMsg, msgHash, postType = postType)
|
||||
) {
|
||||
LogCenter.log("频道消息(guildId = ${record.guildId}, sender = ${record.senderUid}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)")
|
||||
if (!GlobalEventTransmitter.MessageTransmitter
|
||||
.transGuildMessage(record, record.elements, rawMsg, msgHash, postType = postType)
|
||||
) {
|
||||
LogCenter.log("频道消息推送失败 -> MessageTransmitter", Level.WARN)
|
||||
}
|
||||
}
|
||||
@ -149,8 +169,7 @@ internal object AioListener : IKernelMsgListener {
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
|
||||
|
||||
val peerId = when(record.chatType) {
|
||||
val peerId = when (record.chatType) {
|
||||
MsgConstant.KCHATTYPEGUILD -> record.guildId
|
||||
else -> record.peerUin.toString()
|
||||
}
|
||||
@ -174,8 +193,6 @@ internal object AioListener : IKernelMsgListener {
|
||||
|
||||
override fun onMsgInfoListUpdate(msgList: ArrayList<MsgRecord>?) {
|
||||
msgList?.forEach { record ->
|
||||
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return@forEach// TODO: 频道消息暂不处理
|
||||
|
||||
if (record.sendStatus == MsgConstant.KSENDSTATUSFAILED
|
||||
|| record.sendStatus == MsgConstant.KSENDSTATUSSENDING
|
||||
) {
|
||||
@ -184,7 +201,7 @@ internal object AioListener : IKernelMsgListener {
|
||||
|
||||
GlobalScope.launch {
|
||||
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
|
||||
val peerId = when(record.chatType) {
|
||||
val peerId = when (record.chatType) {
|
||||
MsgConstant.KCHATTYPEGUILD -> record.guildId
|
||||
else -> record.peerUin.toString()
|
||||
}
|
||||
@ -449,6 +466,7 @@ internal object AioListener : IKernelMsgListener {
|
||||
override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) {
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
@file:OptIn(DelicateCoroutinesApi::class, ExperimentalSerializationApi::class)
|
||||
|
||||
package moe.fuqiuluo.shamrock.remote.service.listener
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
@ -19,6 +18,7 @@ import moe.fuqiuluo.qqinterface.servlet.FriendSvc.requestFriendSystemMsgNew
|
||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc.requestGroupSystemMsgNew
|
||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin
|
||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeSubType
|
||||
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeType
|
||||
@ -31,9 +31,10 @@ import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.tools.readBuf32Long
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.PacketHandler
|
||||
import protobuf.message.MessageContentHead
|
||||
import protobuf.message.MessageContent
|
||||
import protobuf.message.MessageHead
|
||||
import protobuf.message.RichMessage
|
||||
import protobuf.message.MessageBody
|
||||
import protobuf.message.multimedia.RichMediaForPicData
|
||||
import protobuf.push.C2CCommonTipsEvent
|
||||
import protobuf.push.C2CRecallEvent
|
||||
import protobuf.push.FriendApplyEvent
|
||||
@ -46,6 +47,9 @@ import protobuf.push.GroupInvitedApplyEvent
|
||||
import protobuf.push.GroupListChangeEvent
|
||||
import protobuf.push.MessagePush
|
||||
import protobuf.push.MessagePushClientInfo
|
||||
import java.util.regex.Pattern
|
||||
|
||||
private val RKEY_PATTERN = Pattern.compile("rkey=([A-Za-z0-9_-]+)")
|
||||
|
||||
internal object PrimitiveListener {
|
||||
fun registerListener() {
|
||||
@ -65,7 +69,7 @@ internal object PrimitiveListener {
|
||||
if (
|
||||
push.msgBody == null ||
|
||||
push.msgBody!!.contentHead == null ||
|
||||
push.msgBody!!.richMsg == null ||
|
||||
push.msgBody!!.body == null ||
|
||||
push.msgBody!!.contentHead!!.msgTime == null
|
||||
) return
|
||||
val msgBody = push.msgBody!!
|
||||
@ -73,29 +77,30 @@ internal object PrimitiveListener {
|
||||
val msgType = contentHead.msgType
|
||||
val subType = contentHead.msgSubType
|
||||
val msgTime = contentHead.msgTime!!
|
||||
val richMsg = msgBody.richMsg!!
|
||||
val body = msgBody.body!!
|
||||
try {
|
||||
when (msgType) {
|
||||
33 -> onGroupMemIncreased(msgTime, richMsg)
|
||||
34 -> onGroupMemberDecreased(msgTime, richMsg)
|
||||
44 -> onGroupAdminChange(msgTime, richMsg)
|
||||
84 -> onGroupApply(msgTime, contentHead, richMsg)
|
||||
87 -> onInviteGroup(msgTime, msgBody.msgHead!!, richMsg)
|
||||
33 -> onGroupMemIncreased(msgTime, body)
|
||||
34 -> onGroupMemberDecreased(msgTime, body)
|
||||
44 -> onGroupAdminChange(msgTime, body)
|
||||
82 -> onGroupMessage(msgTime, body)
|
||||
84 -> onGroupApply(msgTime, contentHead, body)
|
||||
87 -> onInviteGroup(msgTime, msgBody.msgHead!!, body)
|
||||
528 -> when (subType) {
|
||||
35 -> onFriendApply(msgTime, push.clientInfo!!, richMsg)
|
||||
39 -> onCardChange(msgTime, richMsg)
|
||||
35 -> onFriendApply(msgTime, push.clientInfo!!, body)
|
||||
39 -> onCardChange(msgTime, body)
|
||||
// invite
|
||||
68 -> onGroupApply(msgTime, contentHead, richMsg)
|
||||
138 -> onC2CRecall(msgTime, richMsg)
|
||||
290 -> onC2CPoke(msgTime, richMsg)
|
||||
68 -> onGroupApply(msgTime, contentHead, body)
|
||||
138 -> onC2CRecall(msgTime, body)
|
||||
290 -> onC2CPoke(msgTime, body)
|
||||
}
|
||||
|
||||
732 -> when (subType) {
|
||||
12 -> onGroupBan(msgTime, richMsg)
|
||||
16 -> onGroupUniqueTitleChange(msgTime, richMsg)
|
||||
17 -> onGroupRecall(msgTime, richMsg)
|
||||
20 -> onGroupPokeAndGroupSign(msgTime, richMsg)
|
||||
21 -> onEssenceMessage(msgTime, push.clientInfo, richMsg)
|
||||
12 -> onGroupBan(msgTime, body)
|
||||
16 -> onGroupUniqueTitleChange(msgTime, body)
|
||||
17 -> onGroupRecall(msgTime, body)
|
||||
20 -> onGroupPokeAndGroupSign(msgTime, body)
|
||||
21 -> onEssenceMessage(msgTime, push.clientInfo, body)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@ -103,7 +108,27 @@ internal object PrimitiveListener {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onC2CPoke(msgTime: Long, richMsg: RichMessage) {
|
||||
private fun onGroupMessage(msgTime: Long, body: MessageBody) {
|
||||
runCatching {
|
||||
body.rich?.elements?.filter {
|
||||
it.commElem != null && it.commElem!!.type == 48
|
||||
}?.map {
|
||||
ProtoBuf.decodeFromByteArray<RichMediaForPicData>(it.commElem!!.data!!)
|
||||
}?.forEach {
|
||||
it.display?.show?.download?.url?.let {
|
||||
RKEY_PATTERN.matcher(it).takeIf {
|
||||
it.find()
|
||||
}?.group(1)?.let { rkey ->
|
||||
LogCenter.log("更新NT RKEY成功:$rkey")
|
||||
RichProtoSvc.multiMediaRKey = rkey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onC2CPoke(msgTime: Long, richMsg: MessageBody) {
|
||||
val event = ProtoBuf.decodeFromByteArray<C2CCommonTipsEvent>(richMsg.rawBuffer!!)
|
||||
if (event.params == null) return
|
||||
|
||||
@ -129,7 +154,7 @@ internal object PrimitiveListener {
|
||||
private suspend fun onFriendApply(
|
||||
msgTime: Long,
|
||||
clientInfo: MessagePushClientInfo,
|
||||
richMsg: RichMessage
|
||||
richMsg: MessageBody
|
||||
) {
|
||||
val event = ProtoBuf.decodeFromByteArray<FriendApplyEvent>(richMsg.rawBuffer!!)
|
||||
if (event.head == null) return
|
||||
@ -162,7 +187,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
|
||||
|
||||
private suspend fun onCardChange(msgTime: Long, richMsg: RichMessage) {
|
||||
private suspend fun onCardChange(msgTime: Long, richMsg: MessageBody) {
|
||||
LogCenter.log("群名片事件异常,请尝试提交issue!", Level.WARN)
|
||||
/*try {
|
||||
val readPacket = ByteReadPacket(richMsg.rawBuffer!!)
|
||||
@ -206,7 +231,7 @@ internal object PrimitiveListener {
|
||||
}*/
|
||||
}
|
||||
|
||||
private suspend fun onGroupUniqueTitleChange(msgTime: Long, richMsg: RichMessage) {
|
||||
private suspend fun onGroupUniqueTitleChange(msgTime: Long, richMsg: MessageBody) {
|
||||
val event = runCatching {
|
||||
ProtoBuf.decodeFromByteArray<GroupCommonTipsEvent>(richMsg.rawBuffer!!)
|
||||
}.getOrElse {
|
||||
@ -250,7 +275,7 @@ internal object PrimitiveListener {
|
||||
private suspend fun onEssenceMessage(
|
||||
msgTime: Long,
|
||||
clientInfo: MessagePushClientInfo?,
|
||||
richMsg: RichMessage
|
||||
richMsg: MessageBody
|
||||
) {
|
||||
if (clientInfo == null) return
|
||||
val event = runCatching {
|
||||
@ -299,7 +324,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
|
||||
|
||||
private suspend fun onGroupPokeAndGroupSign(time: Long, richMsg: RichMessage) {
|
||||
private suspend fun onGroupPokeAndGroupSign(time: Long, richMsg: MessageBody) {
|
||||
val event = runCatching {
|
||||
ProtoBuf.decodeFromByteArray<GroupCommonTipsEvent>(richMsg.rawBuffer!!)
|
||||
}.getOrElse {
|
||||
@ -353,7 +378,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onC2CRecall(time: Long, richMsg: RichMessage) {
|
||||
private suspend fun onC2CRecall(time: Long, richMsg: MessageBody) {
|
||||
val event = ProtoBuf.decodeFromByteArray<C2CRecallEvent>(richMsg.rawBuffer!!)
|
||||
val head = event.head!!
|
||||
|
||||
@ -378,7 +403,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onGroupMemIncreased(time: Long, richMsg: RichMessage) {
|
||||
private suspend fun onGroupMemIncreased(time: Long, richMsg: MessageBody) {
|
||||
val event = ProtoBuf.decodeFromByteArray<GroupListChangeEvent>(richMsg.rawBuffer!!)
|
||||
val groupCode = event.groupCode
|
||||
val targetUid = event.memberUid
|
||||
@ -408,7 +433,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onGroupMemberDecreased(time: Long, richMsg: RichMessage) {
|
||||
private suspend fun onGroupMemberDecreased(time: Long, richMsg: MessageBody) {
|
||||
val event = ProtoBuf.decodeFromByteArray<GroupListChangeEvent>(richMsg.rawBuffer!!)
|
||||
val groupCode = event.groupCode
|
||||
val targetUid = event.memberUid
|
||||
@ -441,7 +466,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onGroupAdminChange(msgTime: Long, richMsg: RichMessage) {
|
||||
private suspend fun onGroupAdminChange(msgTime: Long, richMsg: MessageBody) {
|
||||
val event = ProtoBuf.decodeFromByteArray<GroupAdminChangeEvent>(richMsg.rawBuffer!!)
|
||||
val groupCode = event.groupCode
|
||||
if (event.operation == null) return
|
||||
@ -468,7 +493,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onGroupBan(msgTime: Long, richMsg: RichMessage) {
|
||||
private suspend fun onGroupBan(msgTime: Long, richMsg: MessageBody) {
|
||||
val event = ProtoBuf.decodeFromByteArray<GroupBanEvent>(richMsg.rawBuffer!!)
|
||||
val groupCode = event.groupCode.toLong()
|
||||
val operatorUid = event.operatorUid
|
||||
@ -493,7 +518,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onGroupRecall(time: Long, richMsg: RichMessage) {
|
||||
private suspend fun onGroupRecall(time: Long, richMsg: MessageBody) {
|
||||
val event = runCatching {
|
||||
ProtoBuf.decodeFromByteArray<GroupCommonTipsEvent>(richMsg.rawBuffer!!)
|
||||
}.getOrElse {
|
||||
@ -527,19 +552,18 @@ internal object PrimitiveListener {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onGroupApply(time: Long, contentHead: MessageContentHead, richMsg: RichMessage) {
|
||||
private suspend fun onGroupApply(time: Long, contentHead: MessageContent, richMsg: MessageBody) {
|
||||
when (contentHead.msgType) {
|
||||
84 -> {
|
||||
val event = ProtoBuf.decodeFromByteArray<GroupApplyEvent>(richMsg.rawBuffer!!)
|
||||
val groupCode = event.groupCode
|
||||
val applierUid = event.applierUid
|
||||
val reason = event.applyMsg ?: ""
|
||||
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
|
||||
var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
|
||||
if (applier == getLongUin()) {
|
||||
return
|
||||
}
|
||||
val msgSeq = contentHead.msgSeq
|
||||
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
|
||||
val flag = try {
|
||||
var reqs = requestGroupSystemMsgNew(10, 1)
|
||||
val riskReqs = requestGroupSystemMsgNew(5, 2)
|
||||
@ -548,10 +572,14 @@ internal object PrimitiveListener {
|
||||
it.msg_time.get() == time && it.msg?.group_code?.get() == groupCode
|
||||
}
|
||||
val seq = req?.msg_seq?.get() ?: time
|
||||
if (applier == 0L) {
|
||||
applier = req?.req_uin?.get() ?: 0L
|
||||
}
|
||||
"$seq;$groupCode;$applier"
|
||||
} catch (err: Throwable) {
|
||||
"$time;$groupCode;$applier"
|
||||
}
|
||||
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
|
||||
if (!GlobalEventTransmitter.RequestTransmitter
|
||||
.transGroupApply(time, applier, applierUid, reason, groupCode, flag, RequestSubType.Add)
|
||||
) {
|
||||
@ -562,7 +590,7 @@ internal object PrimitiveListener {
|
||||
val event = ProtoBuf.decodeFromByteArray<GroupInvitedApplyEvent>(richMsg.rawBuffer!!)
|
||||
val groupCode = event.applyInfo?.groupCode ?: return
|
||||
val applierUid = event.applyInfo?.applierUid ?: return
|
||||
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
|
||||
var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
|
||||
if (applier == getLongUin()) {
|
||||
return
|
||||
}
|
||||
@ -570,7 +598,6 @@ internal object PrimitiveListener {
|
||||
// todo
|
||||
return
|
||||
}
|
||||
LogCenter.log("邀请入群申请($groupCode): $applier")
|
||||
val flag = try {
|
||||
var reqs = requestGroupSystemMsgNew(10, 1)
|
||||
val riskReqs = requestGroupSystemMsgNew(5, 2)
|
||||
@ -579,10 +606,14 @@ internal object PrimitiveListener {
|
||||
it.msg_time.get() == time
|
||||
}
|
||||
val seq = req?.msg_seq?.get() ?: time
|
||||
if (applier == 0L) {
|
||||
applier = req?.req_uin?.get() ?: 0L
|
||||
}
|
||||
"$seq;$groupCode;$applier"
|
||||
} catch (err: Throwable) {
|
||||
"$time;$groupCode;$applier"
|
||||
}
|
||||
LogCenter.log("邀请入群申请($groupCode): $applier")
|
||||
if (!GlobalEventTransmitter.RequestTransmitter
|
||||
.transGroupApply(time, applier, applierUid, "", groupCode, flag, RequestSubType.Add)
|
||||
) {
|
||||
@ -592,7 +623,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onInviteGroup(time: Long, msgHead: MessageHead, richMsg: RichMessage) {
|
||||
private suspend fun onInviteGroup(time: Long, msgHead: MessageHead, richMsg: MessageBody) {
|
||||
val event = ProtoBuf.decodeFromByteArray<GroupInviteEvent>(richMsg.rawBuffer!!)
|
||||
val groupCode = event.groupCode
|
||||
val invitorUid = event.inviterUid
|
||||
|
@ -65,16 +65,14 @@ val Map<String, Any>.json: JsonObject
|
||||
get() {
|
||||
val map = hashMapOf<String, JsonElement>()
|
||||
forEach { (key, any) ->
|
||||
if (any != null) {
|
||||
when (any) {
|
||||
is JsonElement -> map[key] = any
|
||||
is Number -> map[key] = any.json
|
||||
is String -> map[key] = any.json
|
||||
is Boolean -> map[key] = any.json
|
||||
is Map<*, *> -> map[key] = (any as Map<String, Any>).json
|
||||
is Collection<*> -> map[key] = (any as Collection<Any>).json
|
||||
else -> error("unknown object type: ${any::class.java}")
|
||||
}
|
||||
when (any) {
|
||||
is JsonElement -> map[key] = any
|
||||
is Number -> map[key] = any.json
|
||||
is String -> map[key] = any.json
|
||||
is Boolean -> map[key] = any.json
|
||||
is Map<*, *> -> map[key] = (any as Map<String, Any>).json
|
||||
is Collection<*> -> map[key] = (any as Collection<Any>).json
|
||||
else -> error("unknown object type: ${any::class.java}")
|
||||
}
|
||||
}
|
||||
return map.jsonObject
|
||||
|
@ -1,10 +1,14 @@
|
||||
package moe.fuqiuluo.shamrock.xposed
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Process
|
||||
import de.robv.android.xposed.IXposedHookLoadPackage
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import de.robv.android.xposed.XposedBridge.log
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
||||
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
||||
import moe.fuqiuluo.shamrock.xposed.loader.KeepAlive
|
||||
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
|
||||
@ -15,12 +19,14 @@ import moe.fuqiuluo.shamrock.xposed.hooks.runFirstActions
|
||||
import mqq.app.MobileQQ
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Modifier
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
internal const val PACKAGE_NAME_QQ = "com.tencent.mobileqq"
|
||||
internal const val PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi"
|
||||
internal const val PACKAGE_NAME_QQ_LITE = "com.tencent.qqlite"
|
||||
internal const val PACKAGE_NAME_TIM = "com.tencent.tim"
|
||||
private const val PACKAGE_NAME_QQ = "com.tencent.mobileqq"
|
||||
private const val PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi"
|
||||
private const val PACKAGE_NAME_QQ_LITE = "com.tencent.qqlite"
|
||||
private const val PACKAGE_NAME_TIM = "com.tencent.tim"
|
||||
|
||||
private val uselessProcess = listOf("peak", "tool", "mini", "qzone")
|
||||
|
||||
internal class XposedEntry: IXposedHookLoadPackage {
|
||||
companion object {
|
||||
@ -121,9 +127,7 @@ internal class XposedEntry: IXposedHookLoadPackage {
|
||||
System.setProperty("qxbot_flag", "1")
|
||||
} else return
|
||||
|
||||
log("Process Name = " + MobileQQ.getMobileQQ().qqProcessName)
|
||||
|
||||
PlatformUtils.isTim()
|
||||
val processName = MobileQQ.getMobileQQ().qqProcessName
|
||||
|
||||
// MSG LISTENER 进程运行在主进程
|
||||
// API 也应该开放在主进程
|
||||
@ -134,6 +138,22 @@ internal class XposedEntry: IXposedHookLoadPackage {
|
||||
MMKVFetcher.initMMKV(ctx)
|
||||
}
|
||||
|
||||
runCatching {
|
||||
if (ShamrockConfig.forbidUselessProcess()) {
|
||||
if(uselessProcess.any {
|
||||
processName.contains(it, ignoreCase = true)
|
||||
}) {
|
||||
log("[Shamrock] Useless process detected: $processName, exit.")
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(0)
|
||||
}
|
||||
} else {
|
||||
log("[Shamrock] Useless process detection is disabled.")
|
||||
}
|
||||
}
|
||||
|
||||
log("Process Name = $processName")
|
||||
|
||||
runFirstActions(ctx)
|
||||
}
|
||||
}
|
||||
|
@ -2,24 +2,17 @@ package moe.fuqiuluo.shamrock.xposed.helper
|
||||
|
||||
import com.tencent.qqnt.kernel.api.IKernelService
|
||||
import com.tencent.qqnt.kernel.api.impl.MsgService
|
||||
import com.tencent.qqnt.kernel.nativeinterface.IKernelGroupService
|
||||
import com.tencent.qqnt.kernel.nativeinterface.IKernelGuildService
|
||||
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback
|
||||
import com.tencent.qqnt.kernel.nativeinterface.IQQNTWrapperSession
|
||||
import de.robv.android.xposed.XC_MethodHook
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.remote.service.PacketReceiver
|
||||
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
|
||||
import moe.fuqiuluo.shamrock.remote.service.listener.GroupEventListener
|
||||
import moe.fuqiuluo.shamrock.remote.service.listener.KernelGuildListener
|
||||
import moe.fuqiuluo.shamrock.remote.service.listener.PrimitiveListener
|
||||
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
import kotlin.reflect.jvm.javaMethod
|
||||
|
||||
internal object NTServiceFetcher {
|
||||
private lateinit var iKernelService: IKernelService
|
||||
@ -30,7 +23,7 @@ internal object NTServiceFetcher {
|
||||
lock.withLock {
|
||||
val msgService = service.msgService ?: return
|
||||
val sessionService = service.wrapperSession ?: return
|
||||
val groupService = sessionService.groupService ?: return
|
||||
//val groupService = sessionService.groupService ?: return
|
||||
|
||||
val curHash = service.hashCode() + msgService.hashCode()
|
||||
if (isInitForNt(curHash)) return
|
||||
@ -43,7 +36,7 @@ internal object NTServiceFetcher {
|
||||
this.iKernelService = service
|
||||
|
||||
|
||||
initNTKernelListener(msgService, groupService)
|
||||
initNTKernelListener(msgService)
|
||||
antiBackgroundMode(sessionService)
|
||||
//hookGuildListener(sessionService)
|
||||
}
|
||||
@ -66,7 +59,7 @@ internal object NTServiceFetcher {
|
||||
return hash == curKernelHash
|
||||
}
|
||||
|
||||
private fun initNTKernelListener(msgService: MsgService, groupService: IKernelGroupService) {
|
||||
private fun initNTKernelListener(msgService: MsgService) {
|
||||
if (!PlatformUtils.isMainProcess()) return
|
||||
|
||||
try {
|
||||
|
@ -1,12 +1,50 @@
|
||||
@file:Suppress("UNUSED_VARIABLE", "LocalVariableName")
|
||||
|
||||
package moe.fuqiuluo.shamrock.xposed.hooks
|
||||
|
||||
import android.content.Context
|
||||
import com.tencent.mobileqq.perf.block.BinderMethodProxy
|
||||
import com.tencent.mobileqq.qmmkv.MMKVOptionEntity
|
||||
import com.tencent.mobileqq.qroute.QRoute
|
||||
import com.tencent.qqnt.aio.api.IAIOPicDownloaderProvider
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import epic.EIPCClient
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.beforeHook
|
||||
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||
import moe.fuqiuluo.shamrock.tools.toInnerValuesString
|
||||
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
|
||||
import moe.fuqiuluo.symbols.Process
|
||||
import moe.fuqiuluo.symbols.XposedHook
|
||||
import java.lang.reflect.Modifier
|
||||
|
||||
@XposedHook(priority = -1)
|
||||
@XposedHook(priority = -1, process = Process.ALL)
|
||||
internal class HookForDebug: IAction {
|
||||
override fun invoke(ctx: Context) {
|
||||
/*
|
||||
//val LibraDownloader = QRoute.api(IAIOPicDownloaderProvider::class.java).provideDownloader().javaClass
|
||||
//LibraDownloader.hookMethod("downLoad").before {
|
||||
// val option = it.args[0]
|
||||
// LogCenter.log("LibraDownloader.downLoad(${option.toInnerValuesString()})")
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
/*val NtDnsManager = LuoClassloader.load("com.tencent.qqnt.dns.NtDnsManager")!!
|
||||
val NtDnsInternal = NtDnsManager.declaredMethods.first {
|
||||
!Modifier.isStatic(it.modifiers) && it.parameterCount == 0
|
||||
}.returnType
|
||||
XposedBridge.hookMethod(NtDnsInternal.declaredMethods.first {
|
||||
it.parameterCount == 2
|
||||
&& it.parameterTypes[0] == String::class.java
|
||||
&& it.parameterTypes[1] == Int::class.java
|
||||
&& it.returnType == ArrayList::class.java
|
||||
}, beforeHook {
|
||||
val domain = it.args[0] as String
|
||||
val type = it.args[1] as Int
|
||||
LogCenter.log("NtDnsManager: reqDomain2IpList($domain, $type)")
|
||||
LogCenter.log(Exception().stackTraceToString())
|
||||
})*/
|
||||
/*
|
||||
val httpEngineService = AppRuntimeFetcher.appRuntime
|
||||
.getRuntimeService(IHttpEngineService::class.java, "all")
|
||||
httpEngineService.javaClass.hookMethod("sendReq").before {
|
||||
@ -19,7 +57,17 @@ internal class HookForDebug: IAction {
|
||||
LogCenter.log("请求地址: ${req.mReqUrl}")
|
||||
LogCenter.log("请求: ${req.toInnerValuesString(NetReq::class.java)}")
|
||||
}
|
||||
}
|
||||
BinderMethodProxy::class.java.hookMethod("callServer").before {
|
||||
val action = it.args[2] as String
|
||||
if (action == "reqDomain2IpList") {
|
||||
LogCenter.log(Exception().stackTraceToString())
|
||||
}
|
||||
}
|
||||
EIPCClient::class.java.hookMethod("callServer").before {
|
||||
val module = it.args[0] as String
|
||||
val action = it.args[1] as String
|
||||
if (action == "reqDomain2IpList" || module.contains("dns", ignoreCase = true)) {
|
||||
LogCenter.log(Exception().stackTraceToString())
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
}
|
@ -38,7 +38,7 @@ internal class InitRemoteService : IAction {
|
||||
|
||||
|
||||
if (ShamrockConfig.allowWebHook()) {
|
||||
HttpService.initTransmitter()
|
||||
HttpService.init()
|
||||
}
|
||||
|
||||
val runtime = AppRuntimeFetcher.appRuntime
|
||||
|
Reference in New Issue
Block a user